From 25f972f30fe5bb83a27968adb8e7aed903a2a006 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 26 Feb 2020 18:10:15 +0100 Subject: [PATCH] Refactor direct and add-on paths. Enables use of Plex music playlists synched to Kodi --- resources/lib/context_entry.py | 2 +- resources/lib/itemtypes/movies.py | 39 +++----------- resources/lib/itemtypes/music.py | 30 +---------- resources/lib/itemtypes/tvshows.py | 55 ++++++------------- resources/lib/kodimonitor.py | 2 +- resources/lib/playback.py | 2 +- resources/lib/playlists/kodi_pl.py | 59 +++++++++++++++------ resources/lib/plex_api/file.py | 85 +++++++++++++++++++++--------- resources/lib/widgets.py | 2 +- 9 files changed, 136 insertions(+), 140 deletions(-) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 3d302dca..bbdd2c60 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -142,7 +142,7 @@ class ContextMenu(object): v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type]) playqueue.clear() app.PLAYSTATE.context_menu_play = True - handle = self.api.path(force_addon=True) + handle = self.api.fullpath(force_addon=True)[0] handle = 'RunPlugin(%s)' % handle xbmc.executebuiltin(handle.encode('utf-8')) diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 971da6e6..1c17f395 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase from ..plex_api import API -from .. import app, variables as v, plex_functions as PF, utils +from .. import app, variables as v, plex_functions as PF LOG = getLogger('PLEX.movies') @@ -36,35 +36,12 @@ class Movie(ItemBase): update_item = False kodi_id = self.kodidb.new_movie_id() - # GET THE FILE AND PATH ##### - do_indirect = not app.SYNC.direct_paths - if app.SYNC.direct_paths: - # Direct paths is set the Kodi way - playurl = api.file_path(force_first_media=True) - if playurl is None: - # Something went wrong, trying to use non-direct paths - do_indirect = True - else: - playurl = api.validate_playurl(playurl, api.plex_type) - if playurl is None: - return False - if '\\' in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: - # Network share - filename = playurl.rsplit("/", 1)[1] - path = utils.rreplace(playurl, filename, "", 1) - kodi_pathid = self.kodidb.add_path(path, - content='movies', - scraper='metadata.local') - if do_indirect: - # Set plugin path and media flags using real filename - filename = api.file_name(force_first_media=True) - path = 'plugin://%s.movies/' % v.ADDON_ID - filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' - % (path, plex_id, v.PLEX_TYPE_MOVIE, filename)) - playurl = filename + fullpath, path, filename = api.fullpath() + if app.SYNC.direct_paths and not fullpath.startswith('http'): + kodi_pathid = self.kodidb.add_path(path, + content='movies', + scraper='metadata.local') + else: kodi_pathid = self.kodidb.get_path(path) if update_item: @@ -150,7 +127,7 @@ class Movie(ItemBase): api.list_to_string(api.studios()), api.trailer(), api.list_to_string(api.countries()), - playurl, + fullpath, kodi_pathid, api.premiere_date(), api.userrating()) diff --git a/resources/lib/itemtypes/music.py b/resources/lib/itemtypes/music.py index 15b98bfa..7e2721cc 100644 --- a/resources/lib/itemtypes/music.py +++ b/resources/lib/itemtypes/music.py @@ -7,7 +7,7 @@ from .common import ItemBase from ..plex_api import API from ..plex_db import PlexDB, PLEXDB_LOCK from ..kodi_db import KodiMusicDB, KODIDB_LOCK -from .. import plex_functions as PF, db, timing, app, variables as v, utils +from .. import plex_functions as PF, db, timing, app, variables as v LOG = getLogger('PLEX.music') @@ -520,33 +520,7 @@ class Song(MusicMixin, ItemBase): if entry.tag == 'Mood': moods.append(entry.attrib['tag']) mood = api.list_to_string(moods) - - # GET THE FILE AND PATH ##### - do_indirect = not app.SYNC.direct_paths - if app.SYNC.direct_paths: - # Direct paths is set the Kodi way - playurl = api.file_path(force_first_media=True) - if playurl is None: - # Something went wrong, trying to use non-direct paths - do_indirect = True - else: - playurl = api.validate_playurl(playurl, api.plex_type) - if playurl is None: - return False - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: - # Network share - filename = playurl.rsplit("/", 1)[1] - path = utils.rreplace(playurl, filename, "", 1) - if do_indirect: - # Plex works a bit differently - path = "%s%s" % (app.CONN.server, xml[0][0].get('key')) - path = api.attach_plex_token_to_url(path) - filename = path.rsplit('/', 1)[1] - path = path.replace(filename, '') - + _, path, filename = api.fullpath() # UPDATE THE SONG ##### if update_item: LOG.info("UPDATE song plex_id: %s - %s", plex_id, title) diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index 50c25e49..f1b6ca46 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase, process_path from ..plex_api import API -from .. import plex_functions as PF, app, variables as v, utils +from .. import plex_functions as PF, app, variables as v LOG = getLogger('PLEX.tvshows') @@ -420,43 +420,22 @@ class Episode(TvShowMixin, ItemBase): return parent_id = season['kodi_id'] if season else None - # GET THE FILE AND PATH ##### - do_indirect = not app.SYNC.direct_paths - if app.SYNC.direct_paths: - playurl = api.file_path(force_first_media=True) - if playurl is None: - do_indirect = True - else: - playurl = api.validate_playurl(playurl, v.PLEX_TYPE_EPISODE) - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: - # Network share - filename = playurl.rsplit("/", 1)[1] - path = utils.rreplace(playurl, filename, "", 1) - parent_path_id = self.kodidb.parent_path_id(path) - kodi_pathid = self.kodidb.add_path(path, - id_parent_path=parent_path_id) - if do_indirect: - # Set plugin path - do NOT use "intermediate" paths for the show - # as with direct paths! - filename = api.file_name(force_first_media=True) - path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, api.show_id()) - filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' - % (path, plex_id, v.PLEX_TYPE_EPISODE, filename)) - playurl = filename + fullpath, path, filename = api.fullpath() + if app.SYNC.direct_paths and not fullpath.startswith('http'): + parent_path_id = self.kodidb.parent_path_id(path) + kodi_pathid = self.kodidb.add_path(path, + id_parent_path=parent_path_id) + else: # Root path tvshows/ already saved in Kodi DB kodi_pathid = self.kodidb.add_path(path) - if not app.SYNC.direct_paths: - # need to set a 2nd file entry for a path without plex show id - # This fixes e.g. context menu and widgets working as they - # should - # A dirty hack, really - path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID - # filename_2 is exactly the same as filename - # so WITH plex show id! - kodi_pathid_2 = self.kodidb.add_path(path_2) + # need to set a 2nd file entry for a path without plex show id + # This fixes e.g. context menu and widgets working as they + # should + # A dirty hack, really + path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID + # filename_2 is exactly the same as filename + # so WITH plex show id! + kodi_pathid_2 = self.kodidb.add_path(path_2) # UPDATE THE EPISODE ##### if update_item: @@ -512,7 +491,7 @@ class Episode(TvShowMixin, ItemBase): api.title(), airs_before_season, airs_before_episode, - playurl, + fullpath, kodi_pathid, uniqueid, kodi_fileid, # and NOT kodi_fileid_2 @@ -594,7 +573,7 @@ class Episode(TvShowMixin, ItemBase): grandparent_id, airs_before_season, airs_before_episode, - playurl, + fullpath, kodi_pathid, uniqueid, parent_id, diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index ba41cc4d..ad7749b5 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -575,7 +575,7 @@ def _notify_upnext(item): full_artwork=True) } _complete_artwork_keys(info[key]) - info['play_info'] = {'handle': next_api.path(force_addon=True)} + info['play_info'] = {'handle': next_api.fullpath(force_addon=True)[0]} sender = v.ADDON_ID.encode('utf-8') method = 'upnext_data'.encode('utf-8') data = binascii.hexlify(json.dumps(info)) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 2b30e741..310f8b0f 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -350,7 +350,7 @@ def _prep_playlist_stack(xml, resume): api.part = part if kodi_id is None: # Need to redirect again to PKC to conclude playback - path = api.path(force_addon=True, force_first_media=True) + path = api.fullpath(force_addon=True)[0] # Using different paths than the ones saved in the Kodi DB # fixes Kodi immediately resuming the video if one restarts # the same video again after playback diff --git a/resources/lib/playlists/kodi_pl.py b/resources/lib/playlists/kodi_pl.py index 8753326d..39cfa5de 100644 --- a/resources/lib/playlists/kodi_pl.py +++ b/resources/lib/playlists/kodi_pl.py @@ -117,21 +117,8 @@ def _write_playlist_to_file(playlist, xml): Returns None or raises PlaylistError """ text = '#EXTCPlayListM3U::M3U\n' - for element in xml: - api = API(element) - if api.plex_type == v.PLEX_TYPE_EPISODE: - if api.season_number() is not None and api.index() is not None: - text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n' - % (api.runtime(), api.show_title(), - api.season_number(), api.index(), - api.title(), api.path())) - else: - # Only append the TV show name - text += ('#EXTINF:%s,%s - %s\n%s\n' - % (api.runtime(), api.show_title(), api.title(), api.path())) - else: - text += ('#EXTINF:%s,%s\n%s\n' - % (api.runtime(), api.title(), api.path())) + for xml_element in xml: + text += _m3u_element(xml_element) text += '\n' text = text.encode(v.M3U_ENCODING, 'ignore') try: @@ -142,3 +129,45 @@ def _write_playlist_to_file(playlist, xml): LOG.error('Error message %s: %s', err.errno, err.strerror) raise PlaylistError('Cannot write Kodi playlist to path for %s' % playlist) + + +def _m3u_element(xml_element): + api = API(xml_element) + if api.plex_type == v.PLEX_TYPE_EPISODE: + if api.season_number() is not None and api.index() is not None: + return '#EXTINF:{},{} S{:2d}E{:2d} - {}\n{}\n'.format( + api.runtime(), + api.show_title(), + api.season_number(), + api.index(), + api.title(), + api.fullpath(force_addon=True)[0]) + else: + # Only append the TV show name + return '#EXTINF:{},{} - {}\n{}\n'.format( + api.runtime(), + api.show_title(), + api.title(), + api.fullpath(force_addon=True)[0]) + elif api.plex_type == v.PLEX_TYPE_SONG: + if api.index() is not None: + return '#EXTINF:{},{:02d}. {} - {}\n{}\n'.format( + api.runtime(), + api.index(), + api.grandparent_title(), + api.title(), + api.fullpath(force_first_media=True, + omit_check=True)[0]) + else: + return '#EXTINF:{},{} - {}\n{}\n'.format( + api.runtime(), + api.grandparent_title(), + api.title(), + api.fullpath(force_first_media=True, + omit_check=True)[0]) + else: + return '#EXTINF:{},{}\n{}\n'.format( + api.runtime(), + api.title(), + api.fullpath(force_first_media=True, + omit_check=True)[0]) diff --git a/resources/lib/plex_api/file.py b/resources/lib/plex_api/file.py index 51ef98f1..c3b561c9 100644 --- a/resources/lib/plex_api/file.py +++ b/resources/lib/plex_api/file.py @@ -41,43 +41,80 @@ def _transcode_image_path(key, AuthToken, path, width, height): class File(object): - def path(self, force_first_media=True, force_addon=False, - direct_paths=None): + def fullpath(self, force_first_media=True, force_addon=False, + direct_paths=None, omit_check=False, force_check=False): """ - Returns a "fully qualified path": add-on paths or direct paths - depending on the current settings. Will NOT valide the playurl - Returns unicode or None if something went wrong. + Returns a "fully qualified path" add-on paths or direct paths + depending on the current settings as the tupple + (fullpath, path, filename) + as unicode. Add-on paths are returned as a fallback. Returns None + if something went wrong. - Pass direct_path=True if you're calling from another Plex python - instance - because otherwise direct paths will evaluate to False! + firce_first_media=False prompts the user to choose which version of the + media should be returned, if several are present + force_addon=True will always return the add-on path + direct_path=True if you're calling from another Plex python + instance - because otherwise direct paths will + evaluate to False! """ - direct_paths = direct_paths or app.SYNC.direct_paths - filename = self.file_path(force_first_media=force_first_media) + direct_paths = app.SYNC.direct_paths if direct_paths is None \ + else direct_paths if (not direct_paths or force_addon or self.plex_type == v.PLEX_TYPE_CLIP): - if filename and '/' in filename: - filename = filename.rsplit('/', 1) - elif filename: - filename = filename.rsplit('\\', 1) - try: - filename = filename[1] - except (TypeError, IndexError): - filename = None - # Set plugin path and media flags using real filename + if self.plex_type == v.PLEX_TYPE_SONG: + return self._music_addon_paths(force_first_media) if self.plex_type == v.PLEX_TYPE_EPISODE: # need to include the plex show id in the path path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/' % self.grandparent_id()) else: path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type] - path = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' + # Filename in Kodi will end with actual filename - hopefully + # this is useful for other add-ons + filename = self.file_path(force_first_media=force_first_media) + try: + if '/' in filename: + filename = filename.rsplit('/', 1)[1] + else: + filename = filename.rsplit('\\', 1)[1] + except (TypeError, IndexError): + return + entirepath = ('%s?mode=play&plex_id=%s&plex_type=%s&filename=%s' % (path, self.plex_id, self.plex_type, filename)) + # For Kodi DB, we need to safe the ENTIRE path for filenames + filename = entirepath else: - # Direct paths is set the Kodi way - path = self.validate_playurl(filename, - self.plex_type, - omit_check=True) - return path + entirepath = self.validate_playurl( + self.file_path(force_first_media=force_first_media), + self.plex_type, + force_check=force_check, + omit_check=omit_check) + try: + if '/' in entirepath: + filename = entirepath.rsplit('/', 1)[1] + else: + filename = entirepath.rsplit('\\', 1)[1] + except (TypeError, IndexError): + # Fallback to add-on paths + return self.fullpath(force_first_media=force_first_media, + force_addon=True) + path = utils.rreplace(entirepath, filename, "", 1) + return entirepath, path, filename + + def _music_addon_paths(self, force_first_media): + """ + For songs only. Normal add-on paths plugin://... don't work with the + Kodi music DB, hence use a "direct" url to the music file on the PMS. + """ + if self.mediastream is None and force_first_media is False: + if self.mediastream_number() is None: + return + streamno = 0 if force_first_media else self.mediastream + entirepath = "%s%s" % (app.CONN.server, + self.xml[streamno][self.part].get('key')) + entirepath = self.attach_plex_token_to_url(entirepath) + path, filename = entirepath.rsplit('/', 1) + return entirepath, path + '/', filename def directory_path(self, section_id=None, plex_type=None, old_key=None, synched=True): diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 368e2977..0252de12 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -223,7 +223,7 @@ def _generate_content(api): elif plex_type == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: - url = api.path() + url = api.fullpath(force_first_media=True)[0] if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE: # Hack - Item is not synched to the Kodi database # We CANNOT use paths that show up in the Kodi paths table!