Merge pull request #1134 from croneter/fix-music-playlists

Refactor direct and add-on paths. Enables use of Plex music playlists synched to Kodi
This commit is contained in:
croneter 2020-02-27 17:16:09 +01:00 committed by GitHub
commit 0f90abab59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 142 additions and 140 deletions

View file

@ -142,7 +142,7 @@ class ContextMenu(object):
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type]) v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
playqueue.clear() playqueue.clear()
app.PLAYSTATE.context_menu_play = True 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 handle = 'RunPlugin(%s)' % handle
xbmc.executebuiltin(handle.encode('utf-8')) xbmc.executebuiltin(handle.encode('utf-8'))

View file

@ -5,7 +5,7 @@ from logging import getLogger
from .common import ItemBase from .common import ItemBase
from ..plex_api import API 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') LOG = getLogger('PLEX.movies')
@ -36,35 +36,12 @@ class Movie(ItemBase):
update_item = False update_item = False
kodi_id = self.kodidb.new_movie_id() kodi_id = self.kodidb.new_movie_id()
# GET THE FILE AND PATH ##### fullpath, path, filename = api.fullpath()
do_indirect = not app.SYNC.direct_paths if app.SYNC.direct_paths and not fullpath.startswith('http'):
if app.SYNC.direct_paths: kodi_pathid = self.kodidb.add_path(path,
# Direct paths is set the Kodi way content='movies',
playurl = api.file_path(force_first_media=True) scraper='metadata.local')
if playurl is None: else:
# 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
kodi_pathid = self.kodidb.get_path(path) kodi_pathid = self.kodidb.get_path(path)
if update_item: if update_item:
@ -150,7 +127,7 @@ class Movie(ItemBase):
api.list_to_string(api.studios()), api.list_to_string(api.studios()),
api.trailer(), api.trailer(),
api.list_to_string(api.countries()), api.list_to_string(api.countries()),
playurl, fullpath,
kodi_pathid, kodi_pathid,
api.premiere_date(), api.premiere_date(),
api.userrating()) api.userrating())

View file

@ -7,7 +7,7 @@ from .common import ItemBase
from ..plex_api import API from ..plex_api import API
from ..plex_db import PlexDB, PLEXDB_LOCK from ..plex_db import PlexDB, PLEXDB_LOCK
from ..kodi_db import KodiMusicDB, KODIDB_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') LOG = getLogger('PLEX.music')
@ -520,33 +520,7 @@ class Song(MusicMixin, ItemBase):
if entry.tag == 'Mood': if entry.tag == 'Mood':
moods.append(entry.attrib['tag']) moods.append(entry.attrib['tag'])
mood = api.list_to_string(moods) mood = api.list_to_string(moods)
_, path, filename = api.fullpath()
# 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, '')
# UPDATE THE SONG ##### # UPDATE THE SONG #####
if update_item: if update_item:
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title) LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)

View file

@ -5,7 +5,7 @@ from logging import getLogger
from .common import ItemBase, process_path from .common import ItemBase, process_path
from ..plex_api import API 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') LOG = getLogger('PLEX.tvshows')
@ -420,43 +420,22 @@ class Episode(TvShowMixin, ItemBase):
return return
parent_id = season['kodi_id'] if season else None parent_id = season['kodi_id'] if season else None
# GET THE FILE AND PATH ##### fullpath, path, filename = api.fullpath()
do_indirect = not app.SYNC.direct_paths if app.SYNC.direct_paths and not fullpath.startswith('http'):
if app.SYNC.direct_paths: parent_path_id = self.kodidb.parent_path_id(path)
playurl = api.file_path(force_first_media=True) kodi_pathid = self.kodidb.add_path(path,
if playurl is None: id_parent_path=parent_path_id)
do_indirect = True else:
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
# Root path tvshows/ already saved in Kodi DB # Root path tvshows/ already saved in Kodi DB
kodi_pathid = self.kodidb.add_path(path) 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
# 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
# This fixes e.g. context menu and widgets working as they # should
# should # A dirty hack, really
# A dirty hack, really path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID # filename_2 is exactly the same as filename
# filename_2 is exactly the same as filename # so WITH plex show id!
# so WITH plex show id! kodi_pathid_2 = self.kodidb.add_path(path_2)
kodi_pathid_2 = self.kodidb.add_path(path_2)
# UPDATE THE EPISODE ##### # UPDATE THE EPISODE #####
if update_item: if update_item:
@ -512,7 +491,7 @@ class Episode(TvShowMixin, ItemBase):
api.title(), api.title(),
airs_before_season, airs_before_season,
airs_before_episode, airs_before_episode,
playurl, fullpath,
kodi_pathid, kodi_pathid,
uniqueid, uniqueid,
kodi_fileid, # and NOT kodi_fileid_2 kodi_fileid, # and NOT kodi_fileid_2
@ -594,7 +573,7 @@ class Episode(TvShowMixin, ItemBase):
grandparent_id, grandparent_id,
airs_before_season, airs_before_season,
airs_before_episode, airs_before_episode,
playurl, fullpath,
kodi_pathid, kodi_pathid,
uniqueid, uniqueid,
parent_id, parent_id,

View file

@ -575,7 +575,7 @@ def _notify_upnext(item):
full_artwork=True) full_artwork=True)
} }
_complete_artwork_keys(info[key]) _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') sender = v.ADDON_ID.encode('utf-8')
method = 'upnext_data'.encode('utf-8') method = 'upnext_data'.encode('utf-8')
data = binascii.hexlify(json.dumps(info)) data = binascii.hexlify(json.dumps(info))

View file

@ -84,4 +84,10 @@ def check_migration():
from . import library_sync from . import library_sync
library_sync.force_full_sync() library_sync.force_full_sync()
if not utils.compare_version(last_migration, '2.11.3'):
LOG.info('Migrating to version 2.11.2')
# Re-sync all playlists to Kodi
from .playlists import remove_synced_playlists
remove_synced_playlists()
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION) utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)

View file

@ -350,7 +350,7 @@ def _prep_playlist_stack(xml, resume):
api.part = part api.part = part
if kodi_id is None: if kodi_id is None:
# Need to redirect again to PKC to conclude playback # 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 # Using different paths than the ones saved in the Kodi DB
# fixes Kodi immediately resuming the video if one restarts # fixes Kodi immediately resuming the video if one restarts
# the same video again after playback # the same video again after playback

View file

@ -117,21 +117,8 @@ def _write_playlist_to_file(playlist, xml):
Returns None or raises PlaylistError Returns None or raises PlaylistError
""" """
text = '#EXTCPlayListM3U::M3U\n' text = '#EXTCPlayListM3U::M3U\n'
for element in xml: for xml_element in xml:
api = API(element) text += _m3u_element(xml_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()))
text += '\n' text += '\n'
text = text.encode(v.M3U_ENCODING, 'ignore') text = text.encode(v.M3U_ENCODING, 'ignore')
try: try:
@ -142,3 +129,45 @@ def _write_playlist_to_file(playlist, xml):
LOG.error('Error message %s: %s', err.errno, err.strerror) LOG.error('Error message %s: %s', err.errno, err.strerror)
raise PlaylistError('Cannot write Kodi playlist to path for %s' raise PlaylistError('Cannot write Kodi playlist to path for %s'
% playlist) % 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])

View file

@ -41,43 +41,80 @@ def _transcode_image_path(key, AuthToken, path, width, height):
class File(object): class File(object):
def path(self, force_first_media=True, force_addon=False, def fullpath(self, force_first_media=True, force_addon=False,
direct_paths=None): direct_paths=None, omit_check=False, force_check=False):
""" """
Returns a "fully qualified path": add-on paths or direct paths Returns a "fully qualified path" add-on paths or direct paths
depending on the current settings. Will NOT valide the playurl depending on the current settings as the tupple
Returns unicode or None if something went wrong. (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 firce_first_media=False prompts the user to choose which version of the
instance - because otherwise direct paths will evaluate to False! 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 direct_paths = app.SYNC.direct_paths if direct_paths is None \
filename = self.file_path(force_first_media=force_first_media) else direct_paths
if (not direct_paths or force_addon or if (not direct_paths or force_addon or
self.plex_type == v.PLEX_TYPE_CLIP): self.plex_type == v.PLEX_TYPE_CLIP):
if filename and '/' in filename: if self.plex_type == v.PLEX_TYPE_SONG:
filename = filename.rsplit('/', 1) return self._music_addon_paths(force_first_media)
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_EPISODE: if self.plex_type == v.PLEX_TYPE_EPISODE:
# need to include the plex show id in the path # need to include the plex show id in the path
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/' path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
% self.grandparent_id()) % self.grandparent_id())
else: else:
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type] 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)) % (path, self.plex_id, self.plex_type, filename))
# For Kodi DB, we need to safe the ENTIRE path for filenames
filename = entirepath
else: else:
# Direct paths is set the Kodi way entirepath = self.validate_playurl(
path = self.validate_playurl(filename, self.file_path(force_first_media=force_first_media),
self.plex_type, self.plex_type,
omit_check=True) force_check=force_check,
return path 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, def directory_path(self, section_id=None, plex_type=None, old_key=None,
synched=True): synched=True):

View file

@ -223,7 +223,7 @@ def _generate_content(api):
elif plex_type == v.PLEX_TYPE_PHOTO: elif plex_type == v.PLEX_TYPE_PHOTO:
url = api.get_picture_path() url = api.get_picture_path()
else: else:
url = api.path() url = api.fullpath(force_first_media=True)[0]
if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE: if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE:
# Hack - Item is not synched to the Kodi database # Hack - Item is not synched to the Kodi database
# We CANNOT use paths that show up in the Kodi paths table! # We CANNOT use paths that show up in the Kodi paths table!