Rewire partial playlist sync, part 2

This commit is contained in:
croneter 2018-07-10 21:19:08 +02:00
parent f2d782c15a
commit e0108eeb89
3 changed files with 127 additions and 150 deletions

View file

@ -65,16 +65,8 @@ def delete_plex_playlist(playlist):
entry in the Plex playlist table. entry in the Plex playlist table.
Returns None or raises PL.PlaylistError Returns None or raises PL.PlaylistError
""" """
if (state.SYNC_SPECIFIC_KODI_PLAYLISTS and LOG.debug('Deleting playlist from PMS: %s', playlist)
not sync_kodi_playlist(playlist.kodi_path)): PL.delete_playlist_from_pms(playlist)
# We might have already synced this playlist BEFORE user chose to only
# sync specific playlists. Let's NOT delete all those playlists.
# However, delete it from our database of synced playlists.
LOG.debug('Not deleting playlist since user chose not to sync: %s',
playlist)
else:
LOG.debug('Deleting playlist from PMS: %s', playlist)
PL.delete_playlist_from_pms(playlist)
update_plex_table(playlist, delete=True) update_plex_table(playlist, delete=True)
@ -151,12 +143,11 @@ def update_plex_table(playlist, delete=False):
Pass delete=True to delete the playlist entry Pass delete=True to delete the playlist entry
""" """
if delete:
with plexdb.Get_Plex_DB() as plex_db:
plex_db.delete_playlist_entry(playlist)
return
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
plex_db.insert_playlist_entry(playlist) if delete:
plex_db.delete_playlist_entry(playlist)
else:
plex_db.insert_playlist_entry(playlist)
def _playlist_file_to_plex_ids(playlist): def _playlist_file_to_plex_ids(playlist):
@ -168,7 +159,8 @@ def _playlist_file_to_plex_ids(playlist):
if playlist.kodi_extension == 'm3u': if playlist.kodi_extension == 'm3u':
plex_ids = m3u_to_plex_ids(playlist) plex_ids = m3u_to_plex_ids(playlist)
else: else:
LOG.error('Unknown playlist extension: %s', playlist.kodi_extension) LOG.error('Unsupported playlist extension: %s',
playlist.kodi_extension)
raise PL.PlaylistError raise PL.PlaylistError
return plex_ids return plex_ids
@ -247,20 +239,13 @@ def _write_playlist_to_file(playlist, xml):
try: try:
with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f: with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
f.write(text) f.write(text)
except (OSError, IOError) as err: except EnvironmentError as err:
LOG.error('Could not write Kodi playlist file: %s', playlist) LOG.error('Could not write Kodi playlist file: %s', playlist)
LOG.error('Error message %s: %s', err.errno, err.strerror) LOG.error('Error message %s: %s', err.errno, err.strerror)
raise PL.PlaylistError('Cannot write Kodi playlist to path for %s' raise PL.PlaylistError('Cannot write Kodi playlist to path for %s'
% playlist) % playlist)
def change_plex_playlist_name(playlist, new_name):
"""
TODO - Renames the existing playlist with new_name [unicode]
"""
pass
def plex_id_from_playlist_path(path): def plex_id_from_playlist_path(path):
""" """
Given the Kodi playlist path [unicode], this will return the Plex id [str] Given the Kodi playlist path [unicode], this will return the Plex id [str]
@ -281,37 +266,38 @@ def playlist_object_from_db(path=None, kodi_hash=None, plex_id=None):
""" """
playlist = PL.Playlist_Object() playlist = PL.Playlist_Object()
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
playlist = plex_db.retrieve_playlist(playlist, plex_id, path, kodi_hash) playlist = plex_db.retrieve_playlist(playlist,
plex_id,
path, kodi_hash)
return playlist return playlist
def _kodi_playlist_identical(xml_element):
"""
Feed with one playlist xml element from the PMS. Will return True if PKC
already synced this playlist, False if not or if the Play playlist has
changed in the meantime
"""
pass
def process_websocket(plex_id, 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)
try: if playlist and status == 9:
if playlist and status == 9: # Won't be able to download metadata of the deleted playlist
if sync_plex_playlist(playlist=playlist):
LOG.debug('Plex deletion of playlist detected: %s', playlist) LOG.debug('Plex deletion of playlist detected: %s', playlist)
delete_kodi_playlist(playlist) try:
elif playlist: delete_kodi_playlist(playlist)
xml = PL.get_pms_playlist_metadata(plex_id) except PL.PlaylistError:
if xml is None: pass
LOG.error('Could not download playlist %s', plex_id) return
return xml = PL.get_pms_playlist_metadata(plex_id)
api = API(xml[0]) if xml is None:
LOG.debug('Could not download playlist %s, probably deleted',
plex_id)
return
if not sync_plex_playlist(xml=xml[0]):
return
api = API(xml[0])
try:
if playlist:
if api.updated_at() == playlist.plex_updatedat: if api.updated_at() == playlist.plex_updatedat:
LOG.debug('Playlist with id %s already synced: %s', LOG.debug('Playlist with id %s already synced: %s',
plex_id, playlist) plex_id, playlist)
@ -323,7 +309,7 @@ def process_websocket(plex_id, status):
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 = sync_plex_playlist(xml=xml[0]) create = True
# To the actual work # To the actual work
if create: if create:
create_kodi_playlist(plex_id) create_kodi_playlist(plex_id)
@ -362,18 +348,15 @@ def _full_sync():
old_plex_ids.remove(api.plex_id()) old_plex_ids.remove(api.plex_id())
except ValueError: except ValueError:
pass pass
if not sync_plex_playlist(xml=xml_playlist):
continue
playlist = playlist_object_from_db(plex_id=api.plex_id()) playlist = playlist_object_from_db(plex_id=api.plex_id())
try: try:
if not playlist: if not playlist:
if not sync_plex_playlist(xml=xml_playlist):
continue
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()) create_kodi_playlist(api.plex_id())
continue
elif playlist.plex_updatedat != api.updated_at(): elif playlist.plex_updatedat != api.updated_at():
if not sync_plex_playlist(xml=xml_playlist):
continue
LOG.debug('Detected changed Plex playlist %s: %s', LOG.debug('Detected changed Plex playlist %s: %s',
api.plex_id(), api.title()) api.plex_id(), api.title())
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
@ -389,56 +372,45 @@ def _full_sync():
try: try:
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
except PL.PlaylistError: except PL.PlaylistError:
pass LOG.debug('Skipping deletion of playlist %s: %s',
api.plex_id(), api.title())
# Look at all supported Kodi playlists. Check whether they are in the DB. # Look at all supported Kodi playlists. Check whether they are in the DB.
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
old_kodi_hashes = plex_db.kodi_hashes_all_playlists() old_kodi_paths = plex_db.all_kodi_playlist_paths()
master_paths = [v.PLAYLIST_PATH_VIDEO] for root, _, files in path_ops.walk(v.PLAYLIST_PATH):
if state.ENABLE_MUSIC: for f in files:
master_paths.append(v.PLAYLIST_PATH_MUSIC) path = path_ops.path.join(root, f)
for master_path in master_paths: try:
for root, _, files in path_ops.walk(master_path): old_kodi_paths.remove(path)
for f in files: except ValueError:
try: pass
extension = f.rsplit('.', 1)[1].lower() if not sync_kodi_playlist(path):
except IndexError: continue
continue kodi_hash = utils.generate_file_md5(path)
if extension not in SUPPORTED_FILETYPES: playlist = playlist_object_from_db(path=path)
continue if playlist and playlist.kodi_hash == kodi_hash:
path = path_ops.path.join(root, f) continue
kodi_hash = utils.generate_file_md5(path) try:
playlist = playlist_object_from_db(kodi_hash=kodi_hash) if not playlist:
playlist_2 = playlist_object_from_db(path=path) LOG.debug('New Kodi playlist detected: %s', path)
if playlist:
# Nothing changed at all - neither path nor content
old_kodi_hashes.remove(kodi_hash)
continue
if not sync_kodi_playlist(path):
continue
try:
playlist = PL.Playlist_Object() playlist = PL.Playlist_Object()
playlist.kodi_path = path playlist.kodi_path = path
playlist.kodi_hash = kodi_hash playlist.kodi_hash = kodi_hash
if playlist_2: create_plex_playlist(playlist)
LOG.debug('Changed Kodi playlist %s detected: %s', else:
playlist_2.plex_name, path) LOG.debug('Changed Kodi playlist detected: %s', playlist)
playlist.id = playlist_2.id delete_plex_playlist(playlist)
playlist.plex_name = playlist_2.plex_name playlist.kodi_hash = kodi_hash
delete_plex_playlist(playlist_2) create_plex_playlist(playlist)
create_plex_playlist(playlist)
else:
LOG.debug('New Kodi playlist detected: %s', path)
# Make sure that we delete any playlist with other hash
create_plex_playlist(playlist)
except PL.PlaylistError:
LOG.info('Skipping Kodi playlist %s', path)
for kodi_hash in old_kodi_hashes:
playlist = playlist_object_from_db(kodi_hash=kodi_hash)
if playlist:
try:
delete_plex_playlist(playlist)
except PL.PlaylistError: except PL.PlaylistError:
pass LOG.info('Skipping Kodi playlist %s', path)
for kodi_path in old_kodi_paths:
playlist = playlist_object_from_db(kodi_path=kodi_path)
try:
delete_plex_playlist(playlist)
except PL.PlaylistError:
LOG.debug('Skipping deletion of playlist %s: %s',
playlist.plex_id, playlist.plex_name)
LOG.info('Playlist full sync done') LOG.info('Playlist full sync done')
return True return True
@ -448,22 +420,26 @@ def sync_kodi_playlist(path):
Returns True if we should sync this Kodi playlist with path [unicode] to Returns True if we should sync this Kodi playlist with path [unicode] to
Plex based on the playlist file name and the user settings, False otherwise Plex based on the playlist file name and the user settings, False otherwise
""" """
if path.startswith(v.PLAYLIST_PATH_MIXED):
return False
try:
extension = path.rsplit('.', 1)[1].lower()
except IndexError:
return False
if extension not in SUPPORTED_FILETYPES:
return False
if not state.SYNC_SPECIFIC_KODI_PLAYLISTS: if not state.SYNC_SPECIFIC_KODI_PLAYLISTS:
return True return True
playlist = PL.Playlist_Object() playlist = PL.Playlist_Object()
try: playlist.kodi_path = path
playlist.kodi_path = path prefix = utils.settings('syncSpecificKodiPlaylistsPrefix').lower()
except PL.PlaylistError: if playlist.kodi_filename.lower().startswith(prefix):
pass return True
else:
prefix = utils.settings('syncSpecificKodiPlaylistsPrefix').lower()
if playlist.kodi_filename.lower().startswith(prefix):
return True
LOG.debug('User chose to not sync Kodi playlist %s', path) LOG.debug('User chose to not sync Kodi playlist %s', path)
return False return False
def sync_plex_playlist(plex_id=None, xml=None): def sync_plex_playlist(plex_id=None, xml=None, playlist=None):
""" """
Returns True if we should sync this specific Plex playlist due to the Returns True if we should sync this specific Plex playlist due to the
user settings (including a disabled music library), False if not. user settings (including a disabled music library), False if not.
@ -472,20 +448,28 @@ def sync_plex_playlist(plex_id=None, xml=None):
""" """
if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS: if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
return True return True
if xml is None: if playlist:
xml = PL.get_pms_playlist_metadata(plex_id) # Mainly once we DELETED a Plex playlist that we're NOT supposed
if xml is None: # to sync
LOG.error('Could not get Plex metadata for playlist %s', plex_id) name = playlist.plex_name
return False typus = playlist.type
api = API(xml[0])
else: else:
api = API(xml) if xml is None:
if (not state.ENABLE_MUSIC and xml = PL.get_pms_playlist_metadata(plex_id)
api.playlist_type() == v.PLEX_PLAYLIST_TYPE_AUDIO): if xml is None:
LOG.info('Could not get Plex metadata for playlist %s',
plex_id)
return False
api = API(xml[0])
else:
api = API(xml)
name = api.title()
typus = api.playlist_type()
if (not state.ENABLE_MUSIC and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
LOG.debug('Not synching Plex audio playlist') LOG.debug('Not synching Plex audio playlist')
return False return False
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower() prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
if api.title() and api.title().lower().startswith(prefix): if name and name.lower().startswith(prefix):
return True return True
LOG.debug('User chose to not sync Plex playlist') LOG.debug('User chose to not sync Plex playlist')
return False return False
@ -507,17 +491,6 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
if not state.SYNC_PLAYLISTS: if not state.SYNC_PLAYLISTS:
# Sync is deactivated # Sync is deactivated
return return
try:
_, extension = event.src_path.rsplit('.', 1)
except ValueError:
return
if extension.lower() not in SUPPORTED_FILETYPES:
return
if event.src_path.startswith(v.PLAYLIST_PATH_MIXED):
return
if (not state.ENABLE_MUSIC and
event.src_path.startswith(v.PLAYLIST_PATH_MUSIC)):
return
path = event.dest_path if event.event_type == events.EVENT_TYPE_MOVED \ path = event.dest_path if event.event_type == events.EVENT_TYPE_MOVED \
else event.src_path else event.src_path
if not sync_kodi_playlist(path): if not sync_kodi_playlist(path):
@ -534,8 +507,8 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
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 and old_playlist.kodi_hash == kodi_hash = utils.generate_file_md5(event.src_path)
utils.generate_file_md5(event.src_path)): if old_playlist and old_playlist.kodi_hash == kodi_hash:
LOG.debug('Playlist already in DB - skipping') LOG.debug('Playlist already in DB - skipping')
return return
elif old_playlist: elif old_playlist:
@ -544,7 +517,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
return 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 = kodi_hash
try: try:
create_plex_playlist(playlist) create_plex_playlist(playlist)
except PL.PlaylistError: except PL.PlaylistError:
@ -553,38 +526,43 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
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)
kodi_hash = utils.generate_file_md5(event.src_path)
if old_playlist and old_playlist.kodi_hash == kodi_hash:
LOG.debug('Nothing modified, playlist already in DB - skipping')
return
new_playlist = PL.Playlist_Object() new_playlist = PL.Playlist_Object()
if old_playlist: if old_playlist:
# Retain the name! Might've come from Plex # Retain the name! Might've come from Plex
# (rename should fire on_moved) # (rename should fire on_moved)
new_playlist.plex_name = old_playlist.plex_name new_playlist.plex_name = old_playlist.plex_name
delete_plex_playlist(old_playlist)
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 = kodi_hash
try: try:
if not old_playlist: create_plex_playlist(new_playlist)
LOG.debug('Old playlist not found, creating a new one')
try:
create_plex_playlist(new_playlist)
except PL.PlaylistError:
pass
elif old_playlist.kodi_hash == new_playlist.kodi_hash:
LOG.debug('Old and new playlist are identical - nothing to do')
else:
delete_plex_playlist(old_playlist)
create_plex_playlist(new_playlist)
except PL.PlaylistError: except PL.PlaylistError:
pass pass
def on_moved(self, event): def on_moved(self, event):
LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path) LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path)
kodi_hash = utils.generate_file_md5(event.dest_path)
# First check whether we don't already have destination playlist in
# our DB. Just in case....
old_playlist = playlist_object_from_db(path=event.dest_path)
if old_playlist:
LOG.warning('Found target playlist already in our DB!')
new_event = events.FileModifiedEvent(event.dest_path)
self.on_modified(new_event)
return
# All good
old_playlist = playlist_object_from_db(path=event.src_path) old_playlist = playlist_object_from_db(path=event.src_path)
if not old_playlist: if not old_playlist:
LOG.error('Did not have source path in the DB %s', event.src_path) LOG.debug('Did not have source path in the DB %s', event.src_path)
else: else:
delete_plex_playlist(old_playlist) delete_plex_playlist(old_playlist)
new_playlist = PL.Playlist_Object() new_playlist = PL.Playlist_Object()
new_playlist.kodi_path = event.dest_path new_playlist.kodi_path = event.dest_path
new_playlist.kodi_hash = utils.generate_file_md5(event.dest_path) new_playlist.kodi_hash = kodi_hash
try: try:
create_plex_playlist(new_playlist) create_plex_playlist(new_playlist)
except PL.PlaylistError: except PL.PlaylistError:
@ -594,7 +572,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
LOG.debug('on_deleted: %s', event.src_path) LOG.debug('on_deleted: %s', event.src_path)
playlist = playlist_object_from_db(path=event.src_path) playlist = playlist_object_from_db(path=event.src_path)
if not playlist: if not playlist:
LOG.error('Playlist not found in DB for path %s', event.src_path) LOG.info('Playlist not found in DB for path %s', event.src_path)
else: else:
delete_plex_playlist(playlist) delete_plex_playlist(playlist)

View file

@ -414,12 +414,12 @@ class Plex_DB_Functions():
answ.append(entry[0]) answ.append(entry[0])
return answ return answ
def kodi_hashes_all_playlists(self): def all_kodi_playlist_paths(self):
""" """
Returns a list of all Kodi hashes of playlists. Returns a list of all Kodi playlist paths.
""" """
answ = [] answ = []
self.plexcursor.execute('SELECT kodi_hash FROM playlists') self.plexcursor.execute('SELECT kodi_path FROM playlists')
for entry in self.plexcursor.fetchall(): for entry in self.plexcursor.fetchall():
answ.append(entry[0]) answ.append(entry[0])
return answ return answ

View file

@ -1011,19 +1011,18 @@ def delete_nodes():
def generate_file_md5(path): def generate_file_md5(path):
""" """
Generates the md5 hash value for the file located at path [unicode]. Generates the md5 hash value for the file located at path [unicode].
The hash includes the path and is thus different for the same file for The hash does not include the path and filename and is thus identical for
different filenames. a file that was moved/changed name.
Returns a unique string containing only hexadecimal digits Returns a unique unicode containing only hexadecimal digits
""" """
m = hashlib.md5() m = hashlib.md5()
m.update(path.encode('utf-8'))
with open(path_ops.encode_path(path), 'rb') as f: with open(path_ops.encode_path(path), 'rb') as f:
while True: while True:
piece = f.read(32768) piece = f.read(32768)
if not piece: if not piece:
break break
m.update(piece) m.update(piece)
return m.hexdigest() return m.hexdigest().decode('utf-8')
############################################################################### ###############################################################################