From c29b47319f24f6cb0031142d5a48b78708ed3bed Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Jul 2018 21:44:08 +0200 Subject: [PATCH 01/10] Drop directory filesystem events immediately --- resources/lib/playlists.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index d8bd1712..ac23dc91 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -571,6 +571,22 @@ class PlaylistEventhandler(events.FileSystemEventHandler): pass +class PlaylistQueue(OrderedSetQueue): + """ + OrderedSetQueue that drops all directory events immediately + """ + def _put(self, item): + if item[0].is_directory: + self.unfinished_tasks -= 1 + elif item not in self._set_of_items: + Queue.Queue._put(self, item) + self._set_of_items.add(item) + else: + # `put` increments `unfinished_tasks` even if we did not put + # anything into the queue here + self.unfinished_tasks -= 1 + + class PlaylistObserver(Observer): """ PKC implementation, overriding the dispatcher. PKC will wait for the @@ -582,7 +598,7 @@ class PlaylistObserver(Observer): super(PlaylistObserver, self).__init__(*args, **kwargs) # Drop the same events that get into the queue even if there are other # events in between these similar events - self._event_queue = OrderedSetQueue() + self._event_queue = PlaylistQueue() @staticmethod def _pkc_similar_events(event1, event2): From 0166aaf7ba637ca5b2863d1220db1f3902f0b101 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Jul 2018 21:48:07 +0200 Subject: [PATCH 02/10] Decrease filesystem safety margin to 1 second --- resources/lib/playlists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index ac23dc91..dcf3e0a5 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -20,7 +20,7 @@ from . import state LOG = getLogger('PLEX.playlists') # Safety margin for playlist filesystem operations -FILESYSTEM_TIMEOUT = 3 +FILESYSTEM_TIMEOUT = 1 # These filesystem events are considered similar SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED) From d44e782543bff2f53a99eaeebd99c6d7eeda0709 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 18:21:50 +0200 Subject: [PATCH 03/10] Fix playlists getting recreated and deleted in an endless loop --- resources/lib/librarysync.py | 3 +- resources/lib/playlist_func.py | 12 ++++++++ resources/lib/playlists.py | 54 +++++++++++++++++++++------------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 41fc10e0..64e44764 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1189,8 +1189,7 @@ class LibrarySync(Thread): if typus == 'playlist': if not state.SYNC_PLAYLISTS: continue - playlists.process_websocket(plex_id=str(item['itemID']), - updated_at=str(item['updatedAt']), + playlists.process_websocket(plex_id=unicode(item['itemID']), status=status) elif status == 9: # Immediately and always process deletions (as the PMS will diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index c688f857..9bd668c0 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -736,6 +736,18 @@ def get_all_playlists(): 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): """ Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index dcf3e0a5..822bfbb6 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -78,10 +78,10 @@ def delete_plex_playlist(playlist): 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 - playlist table entry. + Creates a new Kodi playlist file. Will also add (or modify an existing) + Plex playlist table entry. 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 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 should actually indeed be synced """ - xml = PL.get_PMS_playlist(PL.Playlist_Object(), playlist_id=plex_id) - if xml is None: - LOG.error('Could not get Plex playlist %s', plex_id) + xml_metadata = PL.get_pms_playlist_metadata(plex_id) + if xml_metadata is None: + LOG.error('Could not get Plex playlist metadata %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: prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower() 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 playlist = PL.Playlist_Object() 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: return 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) + # Derive filename close to Plex playlist name name = utils.valid_filename(playlist.plex_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): @@ -126,8 +127,11 @@ def create_kodi_playlist(plex_id=None, updated_at=None): occurance)) LOG.debug('Kodi playlist path: %s', path) playlist.kodi_path = path - # Derive filename close to Plex playlist name - _write_playlist_to_file(playlist, xml) + xml_playlist = PL.get_PMS_playlist(playlist, playlist_id=plex_id) + 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) update_plex_table(playlist) LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist) @@ -303,10 +307,11 @@ def _kodi_playlist_identical(xml_element): pass -def process_websocket(plex_id, updated_at, status): +def process_websocket(plex_id, status): """ Hit by librarysync to process websocket messages concerning playlists """ + create = False with state.LOCK_PLAYLISTS: 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: LOG.debug('Plex deletion of playlist detected: %s', 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: - LOG.debug('Change of Plex playlist detected: %s', playlist) - delete_kodi_playlist(playlist) - create = True + xml = PL.get_pms_playlist_metadata(plex_id) + if not xml: + 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: LOG.debug('Creation of new Plex playlist detected: %s', plex_id) create = True # To the actual work if create: - create_kodi_playlist(plex_id=plex_id, updated_at=updated_at) + create_kodi_playlist(plex_id) except PL.PlaylistError: pass @@ -367,7 +379,7 @@ def _full_sync(): if not playlist: LOG.debug('New Plex playlist %s discovered: %s', api.plex_id(), api.title()) - create_kodi_playlist(api.plex_id(), api.updated_at()) + create_kodi_playlist(api.plex_id()) continue elif playlist.plex_updatedat != api.updated_at(): LOG.debug('Detected changed Plex playlist %s: %s', @@ -376,7 +388,7 @@ def _full_sync(): delete_kodi_playlist(playlist) else: update_plex_table(playlist, delete=True) - create_kodi_playlist(api.plex_id(), api.updated_at()) + create_kodi_playlist(api.plex_id()) except PL.PlaylistError: LOG.info('Skipping playlist %s: %s', api.plex_id(), api.title()) try: From 641520dcbb839c6c48bd200557ab9b83a391fc72 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 18:32:11 +0200 Subject: [PATCH 04/10] Optimize code --- resources/lib/playlists.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index 822bfbb6..5ec2727c 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -493,9 +493,6 @@ class PlaylistEventhandler(events.FileSystemEventHandler): if not state.SYNC_PLAYLISTS: # Sync is deactivated return - if event.is_directory: - # todo: take care of folder renames - return try: _, extension = event.src_path.rsplit('.', 1) except ValueError: @@ -590,13 +587,9 @@ class PlaylistQueue(OrderedSetQueue): def _put(self, item): if item[0].is_directory: self.unfinished_tasks -= 1 - elif item not in self._set_of_items: - Queue.Queue._put(self, item) - self._set_of_items.add(item) else: - # `put` increments `unfinished_tasks` even if we did not put - # anything into the queue here - self.unfinished_tasks -= 1 + # Can't use super as OrderedSetQueue is old style class + OrderedSetQueue._put(self, item) class PlaylistObserver(Observer): From 33afc448fdc4deb298fc919f5ebe1a7450fb9f03 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 18:57:09 +0200 Subject: [PATCH 05/10] Clarify some comments --- resources/lib/playlists.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index 5ec2727c..ed272919 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -595,14 +595,15 @@ class PlaylistQueue(OrderedSetQueue): class PlaylistObserver(Observer): """ PKC implementation, overriding the dispatcher. PKC will wait for the - duration timeout (in seconds) before dispatching. A new event will reset - the timer. + duration timeout (in seconds) AFTER receiving a filesystem event. A new + ("non-similar") event will reset the timer. Creating and modifying will be regarded as equal. """ def __init__(self, *args, **kwargs): super(PlaylistObserver, self).__init__(*args, **kwargs) # 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 + # completely self._event_queue = PlaylistQueue() @staticmethod @@ -612,7 +613,7 @@ class PlaylistObserver(Observer): elif (event1.src_path == event2.src_path and event1.event_type in SIMILAR_EVENTS and 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 False @@ -637,14 +638,13 @@ class PlaylistObserver(Observer): if self._pkc_similar_events(new_event, event): continue else: - # At least on Windows, a dir modified event will be - # triggered once the writing process is done. Fine though yield event, watch event, watch = new_event, new_watch yield event, watch def dispatch_events(self, event_queue, timeout): for event, watch in self._dispatch_iterator(event_queue, timeout): + # This is copy-paste of original code with self._lock: # To allow unschedule/stop and safe removal of event handlers # within event handlers itself, check if the handler is still From e015770dd12973259941452e3b165f5e2e4b4ea2 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 18:59:40 +0200 Subject: [PATCH 06/10] Optimize code --- resources/lib/playlists.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index ed272919..49db5352 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -514,9 +514,8 @@ class PlaylistEventhandler(events.FileSystemEventHandler): events.EVENT_TYPE_CREATED: self.on_created, events.EVENT_TYPE_DELETED: self.on_deleted, } - event_type = event.event_type with state.LOCK_PLAYLISTS: - _method_map[event_type](event) + _method_map[event.event_type](event) def on_created(self, event): LOG.debug('on_created: %s', event.src_path) From 70d809f1791bbc69bd15570c6e95c35692a278db Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 19:10:52 +0200 Subject: [PATCH 07/10] Add some safety nets for playlist sync --- resources/lib/playlists.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index 49db5352..0cf648c7 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -520,9 +520,14 @@ class PlaylistEventhandler(events.FileSystemEventHandler): def on_created(self, event): LOG.debug('on_created: %s', 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') 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.kodi_path = event.src_path playlist.kodi_hash = utils.generate_file_md5(event.src_path) @@ -531,20 +536,13 @@ class PlaylistEventhandler(events.FileSystemEventHandler): except PL.PlaylistError: 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): LOG.debug('on_modified: %s', event.src_path) old_playlist = playlist_object_from_db(path=event.src_path) new_playlist = PL.Playlist_Object() 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.kodi_path = event.src_path new_playlist.kodi_hash = utils.generate_file_md5(event.src_path) @@ -578,6 +576,14 @@ class PlaylistEventhandler(events.FileSystemEventHandler): except PL.PlaylistError: 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): """ From 6f38472b17dd041ec117c06d86dd038f0407452c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 19:16:33 +0200 Subject: [PATCH 08/10] Fix FutureWarning --- resources/lib/playlists.py | 2 +- resources/lib/plex_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index 0cf648c7..5be1f82a 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -321,7 +321,7 @@ def process_websocket(plex_id, status): delete_kodi_playlist(playlist) elif playlist: xml = PL.get_pms_playlist_metadata(plex_id) - if not xml: + if xml is None: LOG.error('Could not download playlist %s', plex_id) return api = API(xml[0]) diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index fc2202fc..7a67e220 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -1218,7 +1218,7 @@ class API(object): Pass in the collection id of e.g. the movie's metadata """ xml = PF.collections(self.library_section_id()) - if not xml: + if xml is None: return [] return [(i.get('index'), i.get('ratingKey')) for i in xml] From 662dbba2e8f187bb0cbfd50e1adb660ef34763f1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 19:32:39 +0200 Subject: [PATCH 09/10] Fix playlist sync settings not disappearing --- resources/settings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/settings.xml b/resources/settings.xml index 6375e13b..dc8525d1 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -66,9 +66,9 @@ - + - + From 6d571a9bb56c1b7523994aa3b94a2ed29c0c2b9c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 7 Jul 2018 19:34:01 +0200 Subject: [PATCH 10/10] Beta version bump 2.2.10 --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 9 ++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb1342de..63041a0b 100644 --- a/README.md +++ b/README.md @@ -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) -[![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) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 4bc10a9e..294c96ae 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -73,7 +73,14 @@ Нативна інтеграція Plex в Kodi Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик! Використовуйте на свій ризик - version 2.2.9 (beta only): + 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 - Fix and optimize startup of playlist sync - Hide certain playlist settings under certain conditions diff --git a/changelog.txt b/changelog.txt index 3cf3eb1e..64a96a39 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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): - Hopefully fix Kodi and Plex playlists getting out of sync - Fix and optimize startup of playlist sync @@ -1176,4 +1183,4 @@ version 1.0.1 - Overhaul userclient version 1.0.0 -- initial release \ No newline at end of file +- initial release