Merge branch 'beta-version'

This commit is contained in:
croneter 2018-07-07 19:34:27 +02:00
commit 321930b2f0
8 changed files with 104 additions and 53 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-2.1.3-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) [![stable version](https://img.shields.io/badge/stable_version-2.1.3-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
[![beta version](https://img.shields.io/badge/beta_version-2.2.9-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![beta version](https://img.shields.io/badge/beta_version-2.2.10-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.9" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.2.10" provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
@ -73,7 +73,14 @@
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary> <summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description> <description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer> <disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
<news>version 2.2.9 (beta only): <news>version 2.2.10 (beta only):
- Fix playlists getting recreated and deleted in an endless loop
- Add some safety nets for playlist sync
- Fix FutureWarning
- Fix playlist sync settings not disappearing
- Optimize code
version 2.2.9 (beta only):
- Hopefully fix Kodi and Plex playlists getting out of sync - Hopefully fix Kodi and Plex playlists getting out of sync
- Fix and optimize startup of playlist sync - Fix and optimize startup of playlist sync
- Hide certain playlist settings under certain conditions - Hide certain playlist settings under certain conditions

View file

@ -1,3 +1,10 @@
version 2.2.10 (beta only):
- Fix playlists getting recreated and deleted in an endless loop
- Add some safety nets for playlist sync
- Fix FutureWarning
- Fix playlist sync settings not disappearing
- Optimize code
version 2.2.9 (beta only): version 2.2.9 (beta only):
- Hopefully fix Kodi and Plex playlists getting out of sync - Hopefully fix Kodi and Plex playlists getting out of sync
- Fix and optimize startup of playlist sync - Fix and optimize startup of playlist sync

View file

@ -1189,8 +1189,7 @@ class LibrarySync(Thread):
if typus == 'playlist': if typus == 'playlist':
if not state.SYNC_PLAYLISTS: if not state.SYNC_PLAYLISTS:
continue continue
playlists.process_websocket(plex_id=str(item['itemID']), playlists.process_websocket(plex_id=unicode(item['itemID']),
updated_at=str(item['updatedAt']),
status=status) status=status)
elif status == 9: elif status == 9:
# Immediately and always process deletions (as the PMS will # Immediately and always process deletions (as the PMS will

View file

@ -736,6 +736,18 @@ def get_all_playlists():
return xml return xml
def get_pms_playlist_metadata(plex_id):
"""
Returns an xml with the entire metadata like updatedAt.
"""
xml = DU().downloadUrl('{server}/playlists/%s' % plex_id)
try:
xml.attrib
except AttributeError:
xml = None
return xml
def get_PMS_playlist(playlist, playlist_id=None): def get_PMS_playlist(playlist, playlist_id=None):
""" """
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we

View file

@ -20,7 +20,7 @@ from . import state
LOG = getLogger('PLEX.playlists') LOG = getLogger('PLEX.playlists')
# Safety margin for playlist filesystem operations # Safety margin for playlist filesystem operations
FILESYSTEM_TIMEOUT = 3 FILESYSTEM_TIMEOUT = 1
# These filesystem events are considered similar # These filesystem events are considered similar
SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED) SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED)
@ -78,10 +78,10 @@ def delete_plex_playlist(playlist):
update_plex_table(playlist, delete=True) update_plex_table(playlist, delete=True)
def create_kodi_playlist(plex_id=None, updated_at=None): def create_kodi_playlist(plex_id):
""" """
Creates a new Kodi playlist file. Will also add (or modify an existing) Plex Creates a new Kodi playlist file. Will also add (or modify an existing)
playlist table entry. Plex playlist table entry.
Assumes that the Plex playlist is indeed new. A NEW Kodi playlist will be Assumes that the Plex playlist is indeed new. A NEW Kodi playlist will be
created in any case (not replaced). Thus make sure that the "same" playlist created in any case (not replaced). Thus make sure that the "same" playlist
is deleted from both disk and the Plex database. is deleted from both disk and the Plex database.
@ -90,15 +90,15 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
Be aware that user settings will be checked whether this Plex playlist Be aware that user settings will be checked whether this Plex playlist
should actually indeed be synced should actually indeed be synced
""" """
xml = PL.get_PMS_playlist(PL.Playlist_Object(), playlist_id=plex_id) xml_metadata = PL.get_pms_playlist_metadata(plex_id)
if xml is None: if xml_metadata is None:
LOG.error('Could not get Plex playlist %s', plex_id) LOG.error('Could not get Plex playlist metadata %s', plex_id)
raise PL.PlaylistError('Could not get Plex playlist %s' % plex_id) raise PL.PlaylistError('Could not get Plex playlist %s' % plex_id)
api = API(xml) api = API(xml_metadata[0])
if state.SYNC_SPECIFIC_PLEX_PLAYLISTS: if state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower() prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
if api.title() and not api.title().lower().startswith(prefix): if api.title() and not api.title().lower().startswith(prefix):
LOG.debug('User chose to not sync playlist %s', api.title()) LOG.debug('User chose to not sync Plex playlist %s', api.title())
return return
playlist = PL.Playlist_Object() playlist = PL.Playlist_Object()
playlist.id = api.plex_id() playlist.id = api.plex_id()
@ -106,8 +106,9 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
if not state.ENABLE_MUSIC and playlist.type == v.KODI_PLAYLIST_TYPE_AUDIO: if not state.ENABLE_MUSIC and playlist.type == v.KODI_PLAYLIST_TYPE_AUDIO:
return return
playlist.plex_name = api.title() playlist.plex_name = api.title()
playlist.plex_updatedat = updated_at playlist.plex_updatedat = api.updated_at()
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist) LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
# Derive filename close to Plex playlist name
name = utils.valid_filename(playlist.plex_name) name = utils.valid_filename(playlist.plex_name)
path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name) path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
while path_ops.exists(path) or playlist_object_from_db(path=path): while path_ops.exists(path) or playlist_object_from_db(path=path):
@ -126,8 +127,11 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
occurance)) occurance))
LOG.debug('Kodi playlist path: %s', path) LOG.debug('Kodi playlist path: %s', path)
playlist.kodi_path = path playlist.kodi_path = path
# Derive filename close to Plex playlist name xml_playlist = PL.get_PMS_playlist(playlist, playlist_id=plex_id)
_write_playlist_to_file(playlist, xml) if xml_playlist is None:
LOG.error('Could not get Plex playlist %s', plex_id)
raise PL.PlaylistError('Could not get Plex playlist %s' % plex_id)
_write_playlist_to_file(playlist, xml_playlist)
playlist.kodi_hash = utils.generate_file_md5(path) playlist.kodi_hash = utils.generate_file_md5(path)
update_plex_table(playlist) update_plex_table(playlist)
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist) LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
@ -303,10 +307,11 @@ def _kodi_playlist_identical(xml_element):
pass pass
def process_websocket(plex_id, updated_at, status): def process_websocket(plex_id, status):
""" """
Hit by librarysync to process websocket messages concerning playlists Hit by librarysync to process websocket messages concerning playlists
""" """
create = False create = False
with state.LOCK_PLAYLISTS: with state.LOCK_PLAYLISTS:
playlist = playlist_object_from_db(plex_id=plex_id) playlist = playlist_object_from_db(plex_id=plex_id)
@ -314,20 +319,27 @@ def process_websocket(plex_id, updated_at, status):
if playlist and status == 9: if playlist and status == 9:
LOG.debug('Plex deletion of playlist detected: %s', playlist) LOG.debug('Plex deletion of playlist detected: %s', playlist)
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
elif playlist and playlist.plex_updatedat == updated_at:
LOG.debug('Playlist with id %s already synced: %s',
plex_id, playlist)
elif playlist: elif playlist:
LOG.debug('Change of Plex playlist detected: %s', playlist) xml = PL.get_pms_playlist_metadata(plex_id)
delete_kodi_playlist(playlist) if xml is None:
create = True LOG.error('Could not download playlist %s', plex_id)
return
api = API(xml[0])
if api.updated_at() == playlist.plex_updatedat:
LOG.debug('Playlist with id %s already synced: %s',
plex_id, playlist)
else:
LOG.debug('Change of Plex playlist detected: %s',
playlist)
delete_kodi_playlist(playlist)
create = True
elif not playlist and not status == 9: elif not playlist and not status == 9:
LOG.debug('Creation of new Plex playlist detected: %s', LOG.debug('Creation of new Plex playlist detected: %s',
plex_id) plex_id)
create = True create = True
# To the actual work # To the actual work
if create: if create:
create_kodi_playlist(plex_id=plex_id, updated_at=updated_at) create_kodi_playlist(plex_id)
except PL.PlaylistError: except PL.PlaylistError:
pass pass
@ -367,7 +379,7 @@ def _full_sync():
if not playlist: if not playlist:
LOG.debug('New Plex playlist %s discovered: %s', LOG.debug('New Plex playlist %s discovered: %s',
api.plex_id(), api.title()) api.plex_id(), api.title())
create_kodi_playlist(api.plex_id(), api.updated_at()) create_kodi_playlist(api.plex_id())
continue continue
elif playlist.plex_updatedat != api.updated_at(): elif playlist.plex_updatedat != api.updated_at():
LOG.debug('Detected changed Plex playlist %s: %s', LOG.debug('Detected changed Plex playlist %s: %s',
@ -376,7 +388,7 @@ def _full_sync():
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
else: else:
update_plex_table(playlist, delete=True) update_plex_table(playlist, delete=True)
create_kodi_playlist(api.plex_id(), api.updated_at()) create_kodi_playlist(api.plex_id())
except PL.PlaylistError: except PL.PlaylistError:
LOG.info('Skipping playlist %s: %s', api.plex_id(), api.title()) LOG.info('Skipping playlist %s: %s', api.plex_id(), api.title())
try: try:
@ -481,9 +493,6 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
if not state.SYNC_PLAYLISTS: if not state.SYNC_PLAYLISTS:
# Sync is deactivated # Sync is deactivated
return return
if event.is_directory:
# todo: take care of folder renames
return
try: try:
_, extension = event.src_path.rsplit('.', 1) _, extension = event.src_path.rsplit('.', 1)
except ValueError: except ValueError:
@ -505,16 +514,20 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
events.EVENT_TYPE_CREATED: self.on_created, events.EVENT_TYPE_CREATED: self.on_created,
events.EVENT_TYPE_DELETED: self.on_deleted, events.EVENT_TYPE_DELETED: self.on_deleted,
} }
event_type = event.event_type
with state.LOCK_PLAYLISTS: with state.LOCK_PLAYLISTS:
_method_map[event_type](event) _method_map[event.event_type](event)
def on_created(self, event): def on_created(self, event):
LOG.debug('on_created: %s', event.src_path) LOG.debug('on_created: %s', event.src_path)
old_playlist = playlist_object_from_db(path=event.src_path) old_playlist = playlist_object_from_db(path=event.src_path)
if old_playlist: if (old_playlist and old_playlist.kodi_hash ==
utils.generate_file_md5(event.src_path)):
LOG.debug('Playlist already in DB - skipping') LOG.debug('Playlist already in DB - skipping')
return return
elif old_playlist:
LOG.debug('Playlist already in DB but it has been changed')
self.on_modified(event)
return
playlist = PL.Playlist_Object() playlist = PL.Playlist_Object()
playlist.kodi_path = event.src_path playlist.kodi_path = event.src_path
playlist.kodi_hash = utils.generate_file_md5(event.src_path) playlist.kodi_hash = utils.generate_file_md5(event.src_path)
@ -523,20 +536,13 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
except PL.PlaylistError: except PL.PlaylistError:
pass pass
def on_deleted(self, event):
LOG.debug('on_deleted: %s', event.src_path)
playlist = playlist_object_from_db(path=event.src_path)
if not playlist:
LOG.error('Playlist not found in DB for path %s', event.src_path)
else:
delete_plex_playlist(playlist)
def on_modified(self, event): def on_modified(self, event):
LOG.debug('on_modified: %s', event.src_path) LOG.debug('on_modified: %s', event.src_path)
old_playlist = playlist_object_from_db(path=event.src_path) old_playlist = playlist_object_from_db(path=event.src_path)
new_playlist = PL.Playlist_Object() new_playlist = PL.Playlist_Object()
if old_playlist: if old_playlist:
# Retain the name! Might've vom from Plex # Retain the name! Might've come from Plex
# (rename should fire on_moved)
new_playlist.plex_name = old_playlist.plex_name new_playlist.plex_name = old_playlist.plex_name
new_playlist.kodi_path = event.src_path new_playlist.kodi_path = event.src_path
new_playlist.kodi_hash = utils.generate_file_md5(event.src_path) new_playlist.kodi_hash = utils.generate_file_md5(event.src_path)
@ -570,19 +576,40 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
except PL.PlaylistError: except PL.PlaylistError:
pass pass
def on_deleted(self, event):
LOG.debug('on_deleted: %s', event.src_path)
playlist = playlist_object_from_db(path=event.src_path)
if not playlist:
LOG.error('Playlist not found in DB for path %s', event.src_path)
else:
delete_plex_playlist(playlist)
class PlaylistQueue(OrderedSetQueue):
"""
OrderedSetQueue that drops all directory events immediately
"""
def _put(self, item):
if item[0].is_directory:
self.unfinished_tasks -= 1
else:
# Can't use super as OrderedSetQueue is old style class
OrderedSetQueue._put(self, item)
class PlaylistObserver(Observer): class PlaylistObserver(Observer):
""" """
PKC implementation, overriding the dispatcher. PKC will wait for the PKC implementation, overriding the dispatcher. PKC will wait for the
duration timeout (in seconds) before dispatching. A new event will reset duration timeout (in seconds) AFTER receiving a filesystem event. A new
the timer. ("non-similar") event will reset the timer.
Creating and modifying will be regarded as equal. Creating and modifying will be regarded as equal.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PlaylistObserver, self).__init__(*args, **kwargs) super(PlaylistObserver, self).__init__(*args, **kwargs)
# Drop the same events that get into the queue even if there are other # Drop the same events that get into the queue even if there are other
# events in between these similar events # events in between these similar events. Ignore directory events
self._event_queue = OrderedSetQueue() # completely
self._event_queue = PlaylistQueue()
@staticmethod @staticmethod
def _pkc_similar_events(event1, event2): def _pkc_similar_events(event1, event2):
@ -591,7 +618,7 @@ class PlaylistObserver(Observer):
elif (event1.src_path == event2.src_path and elif (event1.src_path == event2.src_path and
event1.event_type in SIMILAR_EVENTS and event1.event_type in SIMILAR_EVENTS and
event2.event_type in SIMILAR_EVENTS): event2.event_type in SIMILAR_EVENTS):
# Ignore a consecutive firing of created and modified events # Set created and modified events to equal
return True return True
return False return False
@ -616,14 +643,13 @@ class PlaylistObserver(Observer):
if self._pkc_similar_events(new_event, event): if self._pkc_similar_events(new_event, event):
continue continue
else: else:
# At least on Windows, a dir modified event will be
# triggered once the writing process is done. Fine though
yield event, watch yield event, watch
event, watch = new_event, new_watch event, watch = new_event, new_watch
yield event, watch yield event, watch
def dispatch_events(self, event_queue, timeout): def dispatch_events(self, event_queue, timeout):
for event, watch in self._dispatch_iterator(event_queue, timeout): for event, watch in self._dispatch_iterator(event_queue, timeout):
# This is copy-paste of original code
with self._lock: with self._lock:
# To allow unschedule/stop and safe removal of event handlers # To allow unschedule/stop and safe removal of event handlers
# within event handlers itself, check if the handler is still # within event handlers itself, check if the handler is still

View file

@ -1218,7 +1218,7 @@ class API(object):
Pass in the collection id of e.g. the movie's metadata Pass in the collection id of e.g. the movie's metadata
""" """
xml = PF.collections(self.library_section_id()) xml = PF.collections(self.library_section_id())
if not xml: if xml is None:
return [] return []
return [(i.get('index'), i.get('ratingKey')) for i in xml] return [(i.get('index'), i.get('ratingKey')) for i in xml]

View file

@ -66,9 +66,9 @@
<setting type="sep" /> <setting type="sep" />
<setting id="enablePlaylistSync" type="bool" label="30020" default="true" visible="true"/><!-- Sync Plex playlists --> <setting id="enablePlaylistSync" type="bool" label="30020" default="true" visible="true"/><!-- Sync Plex playlists -->
<setting id="syncSpecificKodiPlaylists" type="bool" label="30023" default="false" visible="eq(-1,true)" /><!-- Only sync specific Kodi playlists to Plex --> <setting id="syncSpecificKodiPlaylists" type="bool" label="30023" default="false" visible="eq(-1,true)" /><!-- Only sync specific Kodi playlists to Plex -->
<setting id="syncSpecificKodiPlaylistsPrefix" type="text" label="30027" default="sync_" visible="eq(-1,true)"/><!-- Prefix in Kodi playlist name to trigger sync --> <setting id="syncSpecificKodiPlaylistsPrefix" type="text" label="30027" default="sync_" visible="eq(-1,true) + eq(-2,true)"/><!-- Prefix in Kodi playlist name to trigger sync -->
<setting id="syncSpecificPlexPlaylists" type="bool" label="30021" default="false" visible="eq(-3,true)"/><!-- Only sync specific Plex playlists to Kodi --> <setting id="syncSpecificPlexPlaylists" type="bool" label="30021" default="false" visible="eq(-3,true)"/><!-- Only sync specific Plex playlists to Kodi -->
<setting id="syncSpecificPlexPlaylistsPrefix" type="text" label="30026" default="sync_" visible="eq(-1,true)" /><!-- Prefix in Plex playlist name to trigger sync --> <setting id="syncSpecificPlexPlaylistsPrefix" type="text" label="30026" default="sync_" visible="eq(-1,true) + eq(-4,true)" /><!-- Prefix in Plex playlist name to trigger sync -->
<setting type="sep" /> <setting type="sep" />
<setting type="lsep" label="39052" /><!-- Background Sync --> <setting type="lsep" label="39052" /><!-- Background Sync -->
<setting id="enableBackgroundSync" type="bool" label="39026" default="true" visible="true"/> <setting id="enableBackgroundSync" type="bool" label="39026" default="true" visible="true"/>