From 2f96749fc7501ac0fb584e0d12d34d2bfd7558f4 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 24 Oct 2018 07:08:32 +0200 Subject: [PATCH] Rewire llibrary sync, part 5 --- default.py | 4 - .../resource.language.en_gb/strings.po | 4 - resources/lib/entrypoint.py | 2 - resources/lib/library_sync/__init__.py | 1 + resources/lib/library_sync/full_sync.py | 19 +- .../lib/library_sync/process_metadata.py | 36 +- resources/lib/library_sync/time.py | 108 ++ resources/lib/librarysync.py | 1229 ++--------------- resources/lib/plex_functions.py | 2 +- resources/lib/utils.py | 4 +- 10 files changed, 258 insertions(+), 1151 deletions(-) create mode 100644 resources/lib/library_sync/time.py diff --git a/default.py b/default.py index f65259ee..f48e0651 100644 --- a/default.py +++ b/default.py @@ -123,10 +123,6 @@ class Main(): elif mode == 'chooseServer': entrypoint.choose_pms_server() - elif mode == 'refreshplaylist': - log.info('Requesting playlist/nodes refresh') - utils.plex_command('RUN_LIB_SCAN', 'views') - elif mode == 'deviceid': self.deviceid() diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 3d3e3e4b..021b9630 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1036,10 +1036,6 @@ msgctxt "#39201" msgid "Settings" msgstr "" -msgctxt "#39203" -msgid "Refresh Plex playlists/nodes" -msgstr "" - msgctxt "#39204" msgid "Perform manual library sync" msgstr "" diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 661b534a..174bca9c 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -170,8 +170,6 @@ def show_main_menu(content_type=None): # some extra entries for settings and stuff directory_item(utils.lang(39201), "plugin://%s?mode=settings" % v.ADDON_ID) - directory_item(utils.lang(39203), - "plugin://%s?mode=refreshplaylist" % v.ADDON_ID) directory_item(utils.lang(39204), "plugin://%s?mode=manualsync" % v.ADDON_ID) xbmcplugin.endOfDirectory(int(argv[1])) diff --git a/resources/lib/library_sync/__init__.py b/resources/lib/library_sync/__init__.py index 9c5071d9..0514ee5a 100644 --- a/resources/lib/library_sync/__init__.py +++ b/resources/lib/library_sync/__init__.py @@ -2,3 +2,4 @@ from __future__ import absolute_import, division, unicode_literals from .full_sync import start, PLAYLIST_SYNC_ENABLED +from .time import sync_pms_time diff --git a/resources/lib/library_sync/full_sync.py b/resources/lib/library_sync/full_sync.py index b2b9ddc9..7398b453 100644 --- a/resources/lib/library_sync/full_sync.py +++ b/resources/lib/library_sync/full_sync.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -import time from .get_metadata import GetMetadataTask from . import common, process_metadata, sections @@ -24,12 +23,13 @@ LOG = getLogger('PLEX.library_sync.full_sync') class FullSync(backgroundthread.KillableThread, common.libsync_mixin): - def __init__(self, repair, callback): + def __init__(self, repair, callback, show_dialog): """ repair=True: force sync EVERY item """ self.repair = repair self.callback = callback + self.show_dialog = show_dialog self.queue = None self.process_thread = None self.last_sync = None @@ -141,14 +141,18 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin): if self.isCanceled(): return successful = False - self.last_sync = time.time() + self.last_sync = utils.unix_timestamp() + # Delete playlist and video node files from Kodi + utils.delete_playlists() + utils.delete_nodes() + # Get latest Plex libraries and build playlist and video node files if not sections.sync_from_pms(): return try: # Fire up our single processing thread self.queue = backgroundthread.Queue.Queue(maxsize=200) self.processing_thread = process_metadata.ProcessMetadata( - self.queue, self.last_sync) + self.queue, self.last_sync, self.show_dialog) self.processing_thread.start() # Actual syncing - do only new items first @@ -180,12 +184,13 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin): # This will block until the processing thread exits LOG.debug('Waiting for processing thread to exit') self.processing_thread.join() - self.callback(successful) + if self.callback: + self.callback(successful) LOG.info('Done full_sync') -def start(repair, callback): +def start(show_dialog, repair=False, callback=None): """ """ # backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback)) - FullSync(repair, callback).start() + FullSync(repair, callback, show_dialog).start() diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index 0573f660..31464b5c 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -36,34 +36,38 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin): item_class: as used to call functions in itemtypes.py e.g. 'Movies' => itemtypes.Movies() """ - def __init__(self, queue, last_sync): + def __init__(self, queue, last_sync, show_dialog): self.queue = queue self.last_sync = last_sync + self.show_dialog = show_dialog self.total = 0 self.current = 0 self.title = None self.section_name = None + self.dialog = None super(ProcessMetadata, self).__init__() - def update_dialog(self): + def update(self): """ """ - try: - progress = int(float(self.current) / float(self.total) * 100.0) - except ZeroDivisionError: - progress = 0 - self.dialog.update(progress, - self.section_name, - '%s/%s: %s' - % (self.current, self.total, self.title)) + if self.show_dialog: + try: + progress = int(float(self.current) / float(self.total) * 100.0) + except ZeroDivisionError: + progress = 0 + self.dialog.update(progress, + self.section_name, + '%s/%s: %s' + % (self.current, self.total, self.title)) def run(self): """ Do the work """ LOG.debug('Processing thread started') - self.dialog = xbmcgui.DialogProgressBG() - self.dialog.create(utils.lang(39714)) + if self.show_dialog: + self.dialog = xbmcgui.DialogProgressBG() + self.dialog.create(utils.lang(39714)) try: # Init with the very first library section. This will block! section = self.queue.get() @@ -91,13 +95,15 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin): children=xml.children) except: utils.ERROR(txt='process_metadata crashed', - notify=True) + notify=True, + cancel_sync=True) if self.current % 20 == 0: self.title = utils.cast(unicode, xml[0].get('title')) - self.update_dialog() + self.update() self.current += 1 self.queue.task_done() finally: - self.dialog.close() + if self.dialog: + self.dialog.close() LOG.debug('Processing thread terminated') diff --git a/resources/lib/library_sync/time.py b/resources/lib/library_sync/time.py new file mode 100644 index 00000000..c42965b9 --- /dev/null +++ b/resources/lib/library_sync/time.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +import xbmc + +from .. import plex_functions as PF, utils, variables as v, state + +LOG = getLogger('PLEX.library_sync.time') + + +def sync_pms_time(): + """ + PMS does not provide a means to get a server timestamp. This is a work- + around - because the PMS might be in another time zone + + In general, everything saved to Kodi shall be in Kodi time. + + Any info with a PMS timestamp is in Plex time, naturally + """ + LOG.info('Synching time with PMS server') + # Find a PMS item where we can toggle the view state to enforce a + # change in lastViewedAt + + # Get all Plex libraries + sections = PF.get_plex_sections() + try: + sections.attrib + except AttributeError: + LOG.error("Error download PMS views, abort sync_pms_time") + return False + + plex_id = None + typus = ( + (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE,), + (v.PLEX_TYPE_SHOW, v.PLEX_TYPE_EPISODE), + (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_SONG) + ) + for section_type, plex_type in typus: + if plex_id: + break + for section in sections: + if plex_id: + break + if not section.attrib['type'] == section_type: + continue + library_id = section.attrib['key'] + try: + iterator = PF.SectionItems(library_id, {'type': plex_type}) + for item in iterator: + if item.get('viewCount'): + # Don't want to mess with items that have playcount>0 + continue + if item.get('viewOffset'): + # Don't mess with items with a resume point + continue + plex_id = utils.cast(int, item.get('ratingKey')) + LOG.info('Found a %s item to sync with: %s', + plex_type, plex_id) + break + except RuntimeError: + pass + if plex_id is None: + LOG.error("Could not find an item to sync time with") + LOG.error("Aborting PMS-Kodi time sync") + return False + + # Get the Plex item's metadata + xml = PF.GetPlexMetadata(plex_id) + if xml in (None, 401): + LOG.error("Could not download metadata, aborting time sync") + return False + + timestamp = xml[0].get('lastViewedAt') + if timestamp is None: + timestamp = xml[0].get('updatedAt') + LOG.debug('Using items updatedAt=%s', timestamp) + if timestamp is None: + timestamp = xml[0].get('addedAt') + LOG.debug('Using items addedAt=%s', timestamp) + if timestamp is None: + timestamp = 0 + LOG.debug('No timestamp; using 0') + timestamp = utils.cast(int, timestamp) + # Set the timer + koditime = utils.unix_timestamp() + # Toggle watched state + PF.scrobble(plex_id, 'watched') + # Let the PMS process this first! + xbmc.sleep(1000) + # Get updated metadata + xml = PF.GetPlexMetadata(plex_id) + # Toggle watched state back + PF.scrobble(plex_id, 'unwatched') + try: + plextime = xml[0].get('lastViewedAt') + except (IndexError, TypeError, AttributeError): + LOG.error('Could not get lastViewedAt - aborting') + return False + + # Calculate time offset Kodi-PMS + state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime) + utils.settings('kodiplextimeoffset', + value=str(state.KODI_PLEX_TIME_OFFSET)) + LOG.info("Time offset Koditime - Plextime in seconds: %s", + state.KODI_PLEX_TIME_OFFSET) + return True diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 2ba768c1..21618fb9 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -2,71 +2,60 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from threading import Thread -import Queue from random import shuffle -import copy -from collections import OrderedDict import xbmc -from xbmcvfs import exists -from . import utils -from .downloadutils import DownloadUtils as DU -from . import itemtypes -from . import plexdb_functions as plexdb -from . import kodidb_functions as kodidb -from . import artwork -from . import plex_functions as PF +from . import library_sync + from .plex_api import API -from .library_sync import get_metadata, process_metadata, fanart, sync_info -from . import music -from . import variables as v -from . import state - -if (v.PLATFORM != 'Microsoft UWP' and - utils.settings('enablePlaylistSync') == 'true'): - # Xbox cannot use watchdog, a dependency for PKC playlist features - from . import playlists - PLAYLIST_SYNC_ENABLED = True -else: - PLAYLIST_SYNC_ENABLED = False - -############################################################################### +from .downloadutils import DownloadUtils as DU +from . import backgroundthread, utils, path_ops +from . import itemtypes, plex_db, kodidb_functions as kodidb +from . import artwork, plex_functions as PF +from . import variables as v, state LOG = getLogger('PLEX.librarysync') -############################################################################### + +def set_library_scan_toggle(boolean=True): + """ + Make sure to hit this function before starting large scans + """ + if not boolean: + # Deactivate + state.DB_SCAN = False + utils.window('plex_dbScan', clear=True) + else: + state.DB_SCAN = True + utils.window('plex_dbScan', value="true") -@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) -class LibrarySync(Thread): +class LibrarySync(backgroundthread.KillableThread): """ The one and only library sync thread. Spawn only 1! """ def __init__(self): self.items_to_process = [] - self.views = [] self.session_keys = {} + self.sync_successful = False + self.last_full_sync = 0 self.fanartqueue = Queue.Queue() self.fanartthread = fanart.ThreadedProcessFanart(self.fanartqueue) # How long should we wait at least to process new/changed PMS items? - self.vnodes = videonodes.VideoNodes() - self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true' # Show sync dialog even if user deactivated? - self.force_dialog = True + self.force_dialog = False # Need to be set accordingly later - self.compare = None - self.new_items_only = None self.update_kodi_video_library = False self.update_kodi_music_library = False - self.nodes = {} - self.playlists = {} - self.sorted_views = [] - self.old_views = [] - self.updatelist = [] - self.all_plex_ids = {} - self.all_kodi_ids = OrderedDict() - Thread.__init__(self) + # Lock used to wait on a full sync, e.g. on initial sync + self.lock = backgroundthread.threading.Lock() + super(LibrarySync, self).__init__() + + def isCanceled(self): + return xbmc.abortRequested or state.STOP_PKC + + def isSuspended(self): + return state.SUSPEND_LIBRARY_THREAD or state.STOP_SYNC def suspend_item_sync(self): """ @@ -75,7 +64,7 @@ class LibrarySync(Thread): Returns False otherwise. """ - if self.suspended() or self.stopped(): + if self.isSuspended() or self.isCanceled(): return True elif state.SUSPEND_SYNC: return True @@ -103,739 +92,6 @@ class LibrarySync(Thread): message=message, icon='{error}') - @staticmethod - def sync_pms_time(): - """ - PMS does not provide a means to get a server timestamp. This is a work- - around. - - In general, everything saved to Kodi shall be in Kodi time. - - Any info with a PMS timestamp is in Plex time, naturally - """ - LOG.info('Synching time with PMS server') - # Find a PMS item where we can toggle the view state to enforce a - # change in lastViewedAt - - # Get all Plex libraries - sections = PF.get_plex_sections() - try: - sections.attrib - except AttributeError: - LOG.error("Error download PMS views, abort sync_pms_time") - return False - - plex_id = None - for mediatype in (v.PLEX_TYPE_MOVIE, - v.PLEX_TYPE_SHOW, - v.PLEX_TYPE_ARTIST): - if plex_id is not None: - break - for view in sections: - if plex_id is not None: - break - if not view.attrib['type'] == mediatype: - continue - library_id = view.attrib['key'] - items = PF.GetAllPlexLeaves(library_id) - if items in (None, 401): - LOG.error("Could not download section %s", - view.attrib['key']) - continue - for item in items: - if item.attrib.get('viewCount') is not None: - # Don't want to mess with items that have playcount>0 - continue - if item.attrib.get('viewOffset') is not None: - # Don't mess with items with a resume point - continue - plex_id = item.attrib.get('ratingKey') - LOG.info('Found an item to sync with: %s', plex_id) - break - - if plex_id is None: - LOG.error("Could not find an item to sync time with") - LOG.error("Aborting PMS-Kodi time sync") - return False - - # Get the Plex item's metadata - xml = PF.GetPlexMetadata(plex_id) - if xml in (None, 401): - LOG.error("Could not download metadata, aborting time sync") - return False - - timestamp = xml[0].attrib.get('lastViewedAt') - if timestamp is None: - timestamp = xml[0].attrib.get('updatedAt') - LOG.debug('Using items updatedAt=%s', timestamp) - if timestamp is None: - timestamp = xml[0].attrib.get('addedAt') - LOG.debug('Using items addedAt=%s', timestamp) - if timestamp is None: - timestamp = 0 - LOG.debug('No timestamp; using 0') - - # Set the timer - koditime = utils.unix_timestamp() - # Toggle watched state - PF.scrobble(plex_id, 'watched') - # Let the PMS process this first! - xbmc.sleep(1000) - # Get PMS items to find the item we just changed - items = PF.GetAllPlexLeaves(library_id, lastViewedAt=timestamp) - # Toggle watched state back - PF.scrobble(plex_id, 'unwatched') - if items in (None, 401): - LOG.error("Could not download metadata, aborting time sync") - return False - - plextime = None - for item in items: - if item.attrib['ratingKey'] == plex_id: - plextime = item.attrib.get('lastViewedAt') - break - - if plextime is None: - LOG.error('Could not get lastViewedAt - aborting') - return False - - # Calculate time offset Kodi-PMS - state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime) - utils.settings('kodiplextimeoffset', - value=str(state.KODI_PLEX_TIME_OFFSET)) - LOG.info("Time offset Koditime - Plextime in seconds: %s", - str(state.KODI_PLEX_TIME_OFFSET)) - return True - - @staticmethod - def initialize_plex_db(): - """ - Run once during startup to verify that plex db exists. - """ - with plexdb.Get_Plex_DB() as plex_db: - # Create the tables for the plex database - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS sections( - section_id INTEGER PRIMARY KEY, - section_name TEXT, - plex_type TEXT, - kodi_tagid INTEGER, - sync_to_kodi INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS movie( - plex_id INTEGER PRIMARY KEY ASC, - checksum INTEGER UNIQUE, - section_id INTEGER, - kodi_id INTEGER, - kodi_fileid INTEGER, - kodi_pathid INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS show( - plex_id INTEGER PRIMARY KEY ASC, - checksum INTEGER UNIQUE, - section_id INTEGER, - kodi_id INTEGER, - kodi_pathid INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS season( - plex_id INTEGER PRIMARY KEY, - checksum INTEGER UNIQUE, - section_id INTEGER, - show_id INTEGER, # plex_id of the parent show - parent_id INTEGER, # kodi_id of the parent show - kodi_id INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS episode( - plex_id INTEGER PRIMARY KEY, - checksum INTEGER UNIQUE, - section_id INTEGER, - show_id INTEGER, # plex_id of the parent show - grandparent_id INTEGER, # kodi_id of the parent show - season_id INTEGER, # plex_id of the parent season - parent_id INTEGER, # kodi_id of the parent season - kodi_id INTEGER, - kodi_fileid INTEGER, - kodi_pathid INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS artist( - plex_id INTEGER PRIMARY KEY ASC, - checksum INTEGER UNIQUE, - section_id INTEGER, - kodi_id INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS album( - plex_id INTEGER PRIMARY KEY, - checksum INTEGER UNIQUE, - section_id INTEGER, - artist_id INTEGER, # plex_id of the parent artist - parent_id INTEGER, # kodi_id of the parent artist - kodi_id INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS track( - plex_id INTEGER PRIMARY KEY, - checksum INTEGER UNIQUE, - section_id INTEGER, - artist_id INTEGER, # plex_id of the parent artist - grandparent_id INTEGER, # kodi_id of the parent artist - album_id INTEGER, # plex_id of the parent album - parent_id INTEGER, # kodi_id of the parent album - kodi_id INTEGER, - kodi_fileid INTEGER, - kodi_pathid INTEGER, - fanart_synced INTEGER, - last_sync INTEGER) - ''') - plex_db.plexcursor.execute(''' - CREATE TABLE IF NOT EXISTS playlists( - plex_id INTEGER PRIMARY KEY ASC, - plex_name TEXT, - plex_updatedat INTEGER, - kodi_path TEXT, - kodi_type TEXT, - kodi_hash TEXT) - ''') - # Create an index for actors to speed up sync - utils.create_actor_db_index() - - @utils.log_time - def full_sync(self, repair=False): - """ - repair=True: force sync EVERY item - """ - # Reset our keys - self.session_keys = {} - # self.compare == False: we're syncing EVERY item - # True: we're syncing only the delta, e.g. different checksum - self.compare = not repair - - self.new_items_only = True - # This will also update playstates and userratings! - LOG.info('Running fullsync for NEW PMS items with repair=%s', repair) - if self._full_sync() is False: - return False - self.new_items_only = False - # This will NOT update playstates and userratings! - LOG.info('Running fullsync for CHANGED PMS items with repair=%s', - repair) - if not self._full_sync(): - return False - if PLAYLIST_SYNC_ENABLED and not playlists.full_sync(): - return False - return True - - def _full_sync(self): - process = [self.plex_movies, self.plex_tv_show] - if state.ENABLE_MUSIC: - process.append(self.plex_music) - - # Do the processing - for kind in process: - if self.suspend_item_sync() or not kind(): - return False - - # Let kodi update the views in any case, since we're doing a full sync - update_library(video=True, music=state.ENABLE_MUSIC) - - if utils.window('plex_scancrashed') == 'true': - # Show warning if itemtypes.py crashed at some point - utils.messageDialog(utils.lang(29999), utils.lang(39408)) - utils.window('plex_scancrashed', clear=True) - elif utils.window('plex_scancrashed') == '401': - utils.window('plex_scancrashed', clear=True) - if state.PMS_STATUS not in ('401', 'Auth'): - # Plex server had too much and returned ERROR - utils.messageDialog(utils.lang(29999), utils.lang(39409)) - return True - - def _process_view(self, folder_item, kodi_db, plex_db, totalnodes): - vnodes = self.vnodes - folder = folder_item.attrib - mediatype = folder['type'] - # Only process supported formats - if mediatype not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, - v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO): - return totalnodes - - # Prevent duplicate for nodes of the same type - nodes = self.nodes[mediatype] - # Prevent duplicate for playlists of the same type - lists = self.playlists[mediatype] - sorted_views = self.sorted_views - - folderid = folder['key'] - foldername = folder['title'] - viewtype = folder['type'] - - # Get current media folders from plex database - view = plex_db.getView_byId(folderid) - try: - current_viewname = view[0] - current_viewtype = view[1] - current_tagid = view[2] - except TypeError: - LOG.info('Creating viewid: %s in Plex database.', folderid) - tagid = kodi_db.create_tag(foldername) - # Create playlist for the video library - if (foldername not in lists and - mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - utils.playlist_xsp(mediatype, foldername, folderid, viewtype) - lists.append(foldername) - # Create the video node - if foldername not in nodes: - vnodes.viewNode(sorted_views.index(foldername), - foldername, - mediatype, - viewtype, - folderid) - nodes.append(foldername) - totalnodes += 1 - # Add view to plex database - plex_db.addView(folderid, foldername, viewtype, tagid) - else: - LOG.info(' '.join(( - 'Found viewid: %s' % folderid, - 'viewname: %s' % current_viewname, - 'viewtype: %s' % current_viewtype, - 'tagid: %s' % current_tagid))) - - # Remove views that are still valid to delete rest later - try: - self.old_views.remove(folderid) - except ValueError: - # View was just created, nothing to remove - pass - - # View was modified, update with latest info - if current_viewname != foldername: - LOG.info('viewid: %s new viewname: %s', folderid, foldername) - tagid = kodi_db.create_tag(foldername) - - # Update view with new info - plex_db.updateView(foldername, tagid, folderid) - - if plex_db.getView_byName(current_viewname) is None: - # The tag could be a combined view. Ensure there's - # no other tags with the same name before deleting - # playlist. - utils.playlist_xsp(mediatype, - current_viewname, - folderid, - current_viewtype, - True) - # Delete video node - if mediatype != "musicvideos": - vnodes.viewNode( - indexnumber=sorted_views.index(foldername), - tagname=current_viewname, - mediatype=mediatype, - viewtype=current_viewtype, - viewid=folderid, - delete=True) - # Added new playlist - if (foldername not in lists and mediatype in - (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - utils.playlist_xsp(mediatype, - foldername, - folderid, - viewtype) - lists.append(foldername) - # Add new video node - if foldername not in nodes and mediatype != "musicvideos": - vnodes.viewNode(sorted_views.index(foldername), - foldername, - mediatype, - viewtype, - folderid) - nodes.append(foldername) - totalnodes += 1 - - # Update items with new tag - items = plex_db.getItem_byView(folderid) - for item in items: - # Remove the "s" from viewtype for tags - kodi_db.update_tag( - current_tagid, tagid, item[0], current_viewtype[:-1]) - else: - # Validate the playlist exists or recreate it - if (foldername not in lists and mediatype in - (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - utils.playlist_xsp(mediatype, - foldername, - folderid, - viewtype) - lists.append(foldername) - # Create the video node if not already exists - if foldername not in nodes and mediatype != "musicvideos": - vnodes.viewNode(sorted_views.index(foldername), - foldername, - mediatype, - viewtype, - folderid) - nodes.append(foldername) - totalnodes += 1 - return totalnodes - - def maintain_views(self): - """ - Compare the views to Plex - """ - # Get views - sections = PF.get_plex_sections() - try: - sections.attrib - except AttributeError: - LOG.error("Error download PMS views, abort maintain_views") - return False - if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True: - # Will reboot Kodi is new library detected - music.excludefromscan_music_folders(xml=sections) - self.views = [] - vnodes = self.vnodes - - self.nodes = { - v.PLEX_TYPE_MOVIE: [], - v.PLEX_TYPE_SHOW: [], - v.PLEX_TYPE_ARTIST: [], - v.PLEX_TYPE_PHOTO: [] - } - self.playlists = copy.deepcopy(self.nodes) - self.sorted_views = [] - - for view in sections: - if (view.attrib['type'] in - (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO, - v.PLEX_TYPE_ARTIST)): - self.sorted_views.append(view.attrib['title']) - LOG.debug('Sorted views: %s', self.sorted_views) - - # total nodes for window properties - vnodes.clearProperties() - totalnodes = len(self.sorted_views) - - with plexdb.Get_Plex_DB() as plex_db: - # Backup old views to delete them later, if needed (at the end - # of this method, only unused views will be left in oldviews) - self.old_views = plex_db.getViews() - with kodidb.GetKodiDB('video') as kodi_db: - for folder_item in sections: - totalnodes = self._process_view(folder_item, - kodi_db, - plex_db, - totalnodes) - # Add video nodes listings - # Plex: there seem to be no favorites/favorites tag - # vnodes.singleNode(totalnodes, - # "Favorite movies", - # "movies", - # "favourites") - # totalnodes += 1 - # vnodes.singleNode(totalnodes, - # "Favorite tvshows", - # "tvshows", - # "favourites") - # totalnodes += 1 - # vnodes.singleNode(totalnodes, - # "channels", - # "movies", - # "channels") - # totalnodes += 1 - - # Save total - utils.window('Plex.nodes.total', str(totalnodes)) - - # Get rid of old items (view has been deleted on Plex side) - if self.old_views: - self.delete_views() - # update views for all: - with plexdb.Get_Plex_DB() as plex_db: - self.views = plex_db.getAllViewInfo() - LOG.info("Finished processing views. Views saved: %s", self.views) - return True - - def delete_views(self): - LOG.info("Removing views: %s", self.old_views) - delete_items = [] - with plexdb.Get_Plex_DB() as plex_db: - for view in self.old_views: - plex_db.removeView(view) - delete_items.extend(plex_db.get_items_by_viewid(view)) - delete_movies = [] - delete_tv = [] - delete_music = [] - for item in delete_items: - if item['kodi_type'] == v.KODI_TYPE_MOVIE: - delete_movies.append(item) - elif item['kodi_type'] in v.KODI_VIDEOTYPES: - delete_tv.append(item) - elif item['kodi_type'] in v.KODI_AUDIOTYPES: - delete_music.append(item) - - utils.dialog('notification', - heading='{plex}', - message=utils.lang(30052), - icon='{plex}', - sound=False) - with itemtypes.Movies() as movie_db: - for item in delete_movies: - movie_db.remove(item['plex_id']) - with itemtypes.TVShows() as tv_db: - for item in delete_tv: - tv_db.remove(item['plex_id']) - # And for the music DB: - with itemtypes.Music() as music_db: - for item in delete_music: - music_db.remove(item['plex_id']) - - def get_updatelist(self, xml, item_class, method, view_name, view_id, - get_children=False): - """ - THIS METHOD NEEDS TO BE FAST! => e.g. no API calls - - Adds items to self.updatelist as well as self.all_plex_ids dict - - Input: - xml: PMS answer for section items - item_class: 'Movies', 'TVShows', ... see itemtypes.py - method: Method name to be called with this itemtype - see itemtypes.py - view_name: Name of the Plex view (e.g. 'My TV shows') - view_id: Id/Key of Plex library (e.g. '1') - get_children: will get Plex children of the item if True, - e.g. for music albums - - Output: self.updatelist, self.all_plex_ids - self.updatelist APPENDED(!!) list itemids (Plex Keys as - as received from API.plex_id()) - One item in this list is of the form: - 'itemId': xxx, - 'item_class': 'Movies','TVShows', ... - 'method': 'add_update', 'add_updateSeason', ... - 'view_name': xxx, - 'view_id': xxx, - 'title': xxx - 'plex_type': xxx, e.g. 'movie', 'episode' - - self.all_plex_ids APPENDED(!!) dict - = {itemid: checksum} - """ - if self.new_items_only is True: - # Only process Plex items that Kodi does not already have in lib - for item in xml: - plex_id = item.get('ratingKey') - if not plex_id: - # Skipping items 'title=All episodes' without a 'ratingKey' - continue - self.all_plex_ids[plex_id] = "K%s%s" % \ - (plex_id, item.get('updatedAt', '')) - if plex_id not in self.all_kodi_ids: - self.updatelist.append({ - 'plex_id': plex_id, - 'item_class': item_class, - 'method': method, - 'view_name': view_name, - 'view_id': view_id, - 'title': item.get('title', 'Missing Title'), - 'plex_type': item.get('type'), - 'get_children': get_children - }) - elif self.compare: - # Only process the delta - new or changed items - for item in xml: - plex_id = item.get('ratingKey') - if not plex_id: - # Skipping items 'title=All episodes' without a 'ratingKey' - continue - plex_checksum = ("K%s%s" - % (plex_id, item.get('updatedAt', ''))) - self.all_plex_ids[plex_id] = plex_checksum - kodi_checksum = self.all_kodi_ids.get(plex_id) - # Only update if movie is not in Kodi or checksum is - # different - if kodi_checksum != plex_checksum: - self.updatelist.append({ - 'plex_id': plex_id, - 'item_class': item_class, - 'method': method, - 'view_name': view_name, - 'view_id': view_id, - 'title': item.get('title', 'Missing Title'), - 'plex_type': item.get('type'), - 'get_children': get_children - }) - else: - # Initial or repair sync: get all Plex movies - for item in xml: - plex_id = item.get('ratingKey') - if not plex_id: - # Skipping items 'title=All episodes' without a 'ratingKey' - continue - self.all_plex_ids[plex_id] = "K%s%s" \ - % (plex_id, item.get('updatedAt', '')) - self.updatelist.append({ - 'plex_id': plex_id, - 'item_class': item_class, - 'method': method, - 'view_name': view_name, - 'view_id': view_id, - 'title': item.get('title', 'Missing Title'), - 'plex_type': item.get('type'), - 'get_children': get_children - }) - - def process_updatelist(self, item_class): - """ - Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes - them by then calling item_classs.() - - Input: - item_class: 'Movies', 'TVShows', ... - self.updatelist - """ - # Some logging, just in case. - item_number = len(self.updatelist) - if item_number == 0: - return - if (utils.settings('FanartTV') == 'true' and - item_class in ('Movies', 'TVShows')): - for item in self.updatelist: - if item['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): - self.fanartqueue.put({ - 'plex_id': item['plex_id'], - 'plex_type': item['plex_type'], - 'refresh': False - }) - # Run through self.updatelist, get XML metadata per item - # Initiate threads - LOG.debug("Starting sync threads") - download_queue = Queue.Queue() - process_queue = Queue.Queue(maxsize=100) - # To keep track - sync_info.GET_METADATA_COUNT = 0 - sync_info.PROCESS_METADATA_COUNT = 0 - sync_info.PROCESSING_VIEW_NAME = '' - # Populate queue: GetMetadata - for _ in range(0, len(self.updatelist)): - download_queue.put(self.updatelist.pop(0)) - # Spawn GetMetadata threads for downloading - threads = [] - for _ in range(min(state.SYNC_THREAD_NUMBER, item_number)): - thread = get_metadata.ThreadedGetMetadata(download_queue, - process_queue) - thread.setDaemon(True) - thread.start() - threads.append(thread) - LOG.debug("%s download threads spawned", len(threads)) - # Spawn one more thread to process Metadata, once downloaded - thread = process_metadata.ThreadedProcessMetadata(process_queue, - item_class) - thread.setDaemon(True) - thread.start() - threads.append(thread) - # Start one thread to show sync progress ONLY for new PMS items - if self.new_items_only is True and (state.SYNC_DIALOG is True or - self.force_dialog is True): - thread = sync_info.ThreadedShowSyncInfo(item_number, item_class) - thread.setDaemon(True) - thread.start() - threads.append(thread) - - # Wait until finished - download_queue.join() - process_queue.join() - # Kill threads - LOG.debug("Waiting to kill threads") - for thread in threads: - # Threads might already have quit by themselves (e.g. Kodi exit) - try: - thread.stop() - except AttributeError: - pass - LOG.debug("Stop sent to all threads") - # Wait till threads are indeed dead - for thread in threads: - try: - thread.join(1.0) - except: - pass - LOG.debug("Sync threads finished") - - @utils.log_time - def plex_movies(self): - # Initialize - self.all_plex_ids = {} - - item_class = 'Movies' - - views = [x for x in self.views if x['itemtype'] == v.KODI_TYPE_MOVIE] - LOG.info("Processing Plex %s. Libraries: %s", item_class, views) - - self.all_kodi_ids = OrderedDict() - if self.compare: - with plexdb.Get_Plex_DB() as plex_db: - # Get movies from Plex server - # Pull the list of movies and boxsets in Kodi - try: - self.all_kodi_ids = OrderedDict( - plex_db.checksum(v.PLEX_TYPE_MOVIE)) - except ValueError: - self.all_kodi_ids = OrderedDict() - - # PROCESS MOVIES ##### - self.updatelist = [] - for view in views: - if not self.install_sync_done: - state.PATH_VERIFIED = False - if self.suspend_item_sync(): - return False - # Get items per view - all_plexmovies = PF.GetPlexSectionResults(view['id'], args=None) - if all_plexmovies is None: - LOG.info("Couldnt get section items, aborting for view.") - continue - elif all_plexmovies == 401: - return False - # Populate self.updatelist and self.all_plex_ids - self.get_updatelist(all_plexmovies, - item_class, - 'add_update', - view['name'], - view['id']) - self.process_updatelist(item_class) - # Update viewstate for EVERY item - for view in views: - if self.suspend_item_sync(): - return False - self.plex_update_watched(view['id'], item_class) - - # PROCESS DELETES ##### - if self.compare: - # Manual sync, process deletes - with itemtypes.Movies() as movie_db: - for kodimovie in self.all_kodi_ids: - if kodimovie not in self.all_plex_ids: - movie_db.remove(kodimovie) - LOG.info("%s sync is finished.", item_class) - return True - def plex_update_watched(self, viewId, item_class, lastViewedAt=None, updatedAt=None): """ @@ -866,245 +122,17 @@ class LibrarySync(Thread): with getattr(itemtypes, item_class)() as itemtype: itemtype.updateUserdata(xml) - @utils.log_time - def plex_tv_show(self): - # Initialize - self.all_plex_ids = {} - item_class = 'TVShows' - - views = [x for x in self.views if x['itemtype'] == 'show'] - LOG.info("Media folders for %s: %s", item_class, views) - - self.all_kodi_ids = OrderedDict() - if self.compare: - with plexdb.Get_Plex_DB() as plex: - # Pull the list of TV shows already in Kodi - for kind in (v.PLEX_TYPE_SHOW, - v.PLEX_TYPE_SEASON, - v.PLEX_TYPE_EPISODE): - try: - elements = dict(plex.checksum(kind)) - self.all_kodi_ids.update(elements) - # Yet empty/not yet synched - except ValueError: - pass - - # PROCESS TV Shows ##### - self.updatelist = [] - for view in views: - if not self.install_sync_done: - state.PATH_VERIFIED = False - if self.suspend_item_sync(): - return False - # Get items per view - view_id = view['id'] - view_name = view['name'] - all_plex_tv_shows = PF.GetPlexSectionResults(view_id) - if all_plex_tv_shows is None: - LOG.error("Error downloading show xml for view %s", view_id) - continue - elif all_plex_tv_shows == 401: - return False - # Populate self.updatelist and self.all_plex_ids - self.get_updatelist(all_plex_tv_shows, - item_class, - 'add_update', - view_name, - view_id) - LOG.debug("Analyzed view %s with ID %s", view_name, view_id) - - # COPY for later use - all_plex_tv_show_ids = self.all_plex_ids.copy() - - # Process self.updatelist - self.process_updatelist(item_class) - LOG.debug("process_updatelist completed for tv shows") - - # PROCESS TV Seasons ##### - # Cycle through tv shows - for show_id in all_plex_tv_show_ids: - if self.suspend_item_sync(): - return False - # Grab all seasons to tvshow from PMS - seasons = PF.GetAllPlexChildren(show_id) - if seasons is None: - LOG.error("Error download season xml for show %s", show_id) - continue - elif seasons == 401: - return False - # Populate self.updatelist and self.all_plex_ids - self.get_updatelist(seasons, - item_class, - 'add_updateSeason', - view_name, - view_id) - LOG.debug("Analyzed all seasons of TV show with Plex Id %s", - show_id) - - # Process self.updatelist - self.process_updatelist(item_class) - LOG.debug("process_updatelist completed for seasons") - - # PROCESS TV Episodes ##### - # Cycle through tv shows - for view in views: - if self.suspend_item_sync(): - return False - # Grab all episodes to tvshow from PMS - episodes = PF.GetAllPlexLeaves(view['id']) - if episodes is None: - LOG.error("Error downloading episod xml for view %s", - view.get('name')) - continue - elif episodes == 401: - return False - # Populate self.updatelist and self.all_plex_ids - self.get_updatelist(episodes, - item_class, - 'add_updateEpisode', - view_name, - view_id) - LOG.debug("Analyzed all episodes of TV show with Plex Id %s", - view['id']) - - # Process self.updatelist - self.process_updatelist(item_class) - LOG.debug("process_updatelist completed for episodes") - # Refresh season info - # Cycle through tv shows - with itemtypes.TVShows() as tvshow_db: - for show_id in all_plex_tv_show_ids: - xml_show = PF.GetPlexMetadata(show_id) - if xml_show is None or xml_show == 401: - LOG.error('Could not download xml_show') - continue - tvshow_db.refreshSeasonEntry(xml_show, show_id) - LOG.debug("Season info refreshed") - - # Update viewstate: - for view in views: - if self.suspend_item_sync(): - return False - self.plex_update_watched(view['id'], item_class) - - if self.compare: - # Manual sync, process deletes - with itemtypes.TVShows() as tvshow_db: - for item in self.all_kodi_ids: - if item not in self.all_plex_ids: - tvshow_db.remove(item) - LOG.info("%s sync is finished.", item_class) - return True - - @utils.log_time - def plex_music(self): - item_class = 'Music' - - views = [x for x in self.views if x['itemtype'] == v.PLEX_TYPE_ARTIST] - LOG.info("Media folders for %s: %s", item_class, views) - - methods = { - v.PLEX_TYPE_ARTIST: 'add_updateArtist', - v.PLEX_TYPE_ALBUM: 'add_updateAlbum', - v.PLEX_TYPE_SONG: 'add_updateSong' - } - urlArgs = { - v.PLEX_TYPE_ARTIST: {'type': 8}, - v.PLEX_TYPE_ALBUM: {'type': 9}, - v.PLEX_TYPE_SONG: {'type': 10} - } - - # Process artist, then album and tracks last to minimize overhead - # Each album needs to be processed directly with its songs - # Remaining songs without album will be processed last - for kind in (v.PLEX_TYPE_ARTIST, - v.PLEX_TYPE_ALBUM, - v.PLEX_TYPE_SONG): - if self.suspend_item_sync(): - return False - LOG.debug("Start processing music %s", kind) - self.all_kodi_ids = OrderedDict() - self.all_plex_ids = {} - self.updatelist = [] - if not self.process_music(views, - kind, - urlArgs[kind], - methods[kind]): - return False - LOG.debug("Processing of music %s done", kind) - self.process_updatelist(item_class) - LOG.debug("process_updatelist for music %s completed", kind) - - # Update viewstate for EVERY item - for view in views: - if self.suspend_item_sync(): - return False - self.plex_update_watched(view['id'], item_class) - - # reset stuff - self.all_kodi_ids = OrderedDict() - self.all_plex_ids = {} - self.updatelist = [] - LOG.info("%s sync is finished.", item_class) - return True - - def process_music(self, views, kind, urlArgs, method): - # For albums, we need to look at the album's songs simultaneously - get_children = True if kind == v.PLEX_TYPE_ALBUM else False - # Get a list of items already existing in Kodi db - if self.compare: - with plexdb.Get_Plex_DB() as plex_db: - # Pull the list of items already in Kodi - try: - elements = dict(plex_db.checksum(kind)) - self.all_kodi_ids.update(elements) - # Yet empty/nothing yet synched - except ValueError: - pass - for view in views: - if not self.install_sync_done: - state.PATH_VERIFIED = False - if self.suspend_item_sync(): - return False - # Get items per view - items_xml = PF.GetPlexSectionResults(view['id'], args=urlArgs) - if items_xml is None: - LOG.error("Error downloading xml for view %s", view['id']) - continue - elif items_xml == 401: - return False - # Populate self.updatelist and self.all_plex_ids - self.get_updatelist(items_xml, - 'Music', - method, - view['name'], - view['id'], - get_children=get_children) - if self.compare: - # Manual sync, process deletes - with itemtypes.Music() as music_db: - for itemid in self.all_kodi_ids: - if itemid not in self.all_plex_ids: - music_db.remove(itemid) - return True - def process_message(self, message): """ processes json.loads() messages from websocket. Triage what we need to do with "process_" methods """ - try: - if message['type'] == 'playing': - self.process_playing(message['PlaySessionStateNotification']) - elif message['type'] == 'timeline': - self.process_timeline(message['TimelineEntry']) - elif message['type'] == 'activity': - self.process_activity(message['ActivityNotification']) - except: - LOG.error('Processing of Plex Companion message has crashed') - LOG.error('Message was: %s', message) - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) + if message['type'] == 'playing': + self.process_playing(message['PlaySessionStateNotification']) + elif message['type'] == 'timeline': + self.process_timeline(message['TimelineEntry']) + elif message['type'] == 'activity': + self.process_activity(message['ActivityNotification']) def multi_delete(self, liste, delete_list): """ @@ -1145,7 +173,7 @@ class LibrarySync(Thread): now = utils.unix_timestamp() delete_list = [] for i, item in enumerate(self.items_to_process): - if self.stopped() or self.suspended(): + if self.isCanceled() or self.suspended(): # Chances are that Kodi gets shut down break if item['state'] == 9: @@ -1252,7 +280,7 @@ class LibrarySync(Thread): continue status = int(item['state']) if typus == 'playlist': - if not PLAYLIST_SYNC_ENABLED: + if not library_sync.PLAYLIST_SYNC_ENABLED: continue playlists.websocket(plex_id=unicode(item['itemID']), status=status) @@ -1480,17 +508,12 @@ class LibrarySync(Thread): triggered full or repair syncs """ if state.RUN_LIB_SCAN in ("full", "repair"): + set_library_scan_toggle() LOG.info('Full library scan requested, starting') - utils.window('plex_dbScan', value="true") - state.DB_SCAN = True - success = self.maintain_views() - if success and state.RUN_LIB_SCAN == "full": - success = self.full_sync() - elif success: - success = self.full_sync(repair=True) - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False - if success: + self.start_library_sync(show_dialog=True, + repair=state.RUN_LIB_SCAN == 'repair', + block=True) + if self.sync_successful: # Full library sync finished self.show_kodi_note(utils.lang(39407)) elif not self.suspend_item_sync(): @@ -1498,28 +521,6 @@ class LibrarySync(Thread): # ERROR in library sync self.show_kodi_note(utils.lang(39410), icon='error') self.force_dialog = False - # Reset views was requested from somewhere else - elif state.RUN_LIB_SCAN == "views": - LOG.info('Refresh playlist and nodes requested, starting') - utils.window('plex_dbScan', value="true") - state.DB_SCAN = True - # First remove playlists - utils.delete_playlists() - # Remove video nodes - utils.delete_nodes() - # Kick off refresh - if self.maintain_views() is True: - # Ran successfully - LOG.info("Refresh playlists/nodes completed") - # "Plex playlists/nodes refreshed" - self.show_kodi_note(utils.lang(39405)) - else: - # Failed - LOG.error("Refresh playlists/nodes failed") - # "Plex playlists/nodes refresh failed" - self.show_kodi_note(utils.lang(39406), icon="error") - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False elif state.RUN_LIB_SCAN == 'fanart': # Only look for missing fanart (No) # or refresh all fanart (Yes) @@ -1530,48 +531,61 @@ class LibrarySync(Thread): utils.lang(39225)) == 0 self.sync_fanart(missing_only=not refresh, refresh=refresh) elif state.RUN_LIB_SCAN == 'textures': - state.DB_SCAN = True - utils.window('plex_dbScan', value="true") artwork.Artwork().fullTextureCacheSync() - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False else: raise NotImplementedError('Library scan not defined: %s' % state.RUN_LIB_SCAN) - # Reset - state.RUN_LIB_SCAN = None + + def onLibrary_scan_finished(self, successful): + """ + Hit this after the full sync has finished + """ + self.sync_successful = successful + self.last_full_sync = utils.unix_timestamp() + set_library_scan_toggle(boolean=False) + try: + self.lock.release() + except backgroundthread.threading.ThreadError: + pass + + def start_library_sync(self, show_dialog=None, repair=False, block=False): + show_dialog = show_dialog if show_dialog is not None else state.SYNC_DIALOG + if block: + self.lock.acquire() + library_sync.start(show_dialog, repair, self.onLibrary_scan_finished) + # Will block until scan is finished + self.lock.acquire() + self.lock.release() + else: + library_sync.start(show_dialog, repair, self.onLibrary_scan_finished) def run(self): try: self._run_internal() - except Exception as e: + except: state.DB_SCAN = False utils.window('plex_dbScan', clear=True) - LOG.error('LibrarySync thread crashed. Error message: %s', e) - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - # Library sync thread has crashed - utils.messageDialog(utils.lang(29999), utils.lang(39400)) + utils.ERROR(txt='librarysync.py crashed', notify=True) raise def _run_internal(self): LOG.info("---===### Starting LibrarySync ###===---") + install_sync_done = utils.settings('SyncInstallRunDone') == 'true' + + playlist_monitor = None initial_sync_done = False kodi_db_version_checked = False - last_sync = 0 last_processing = 0 last_time_sync = 0 one_day_in_seconds = 60 * 60 * 24 # Link to Websocket queue queue = state.WEBSOCKET_QUEUE - if (not exists(utils.try_encode(v.DB_VIDEO_PATH)) or - not exists(utils.try_encode(v.DB_TEXTURE_PATH)) or - (state.ENABLE_MUSIC and - not exists(utils.try_encode(v.DB_MUSIC_PATH)))): + if (not path_ops.exists(v.DB_VIDEO_PATH) or + not path_ops.exists(v.DB_TEXTURE_PATH) or + (state.ENABLE_MUSIC and not path_ops.exists(v.DB_MUSIC_PATH))): # Database does not exists - LOG.error("The current Kodi version is incompatible " - "to know which Kodi versions are supported.") + LOG.error('The current Kodi version is incompatible') LOG.error('Current Kodi version: %s', utils.try_decode( xbmc.getInfoLabel('System.BuildVersion'))) # "Current Kodi version is unsupported, cancel lib sync" @@ -1579,62 +593,51 @@ class LibrarySync(Thread): return # Do some initializing - # Ensure that DBs exist if called for very first time - self.initialize_plex_db() + # Ensure that Plex DB is set-up + plex_db.initialize() + # Hack to speed up look-ups for actors (giant table!) + utils.create_actor_db_index() # Run start up sync - state.DB_SCAN = True - utils.window('plex_dbScan', value="true") LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion')) - LOG.info('Refreshing video nodes and playlists now') with kodidb.GetKodiDB('video') as kodi_db: # Setup the paths for addon-paths (even when using direct paths) kodi_db.setup_path_table() - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False - playlist_monitor = None - while not self.stopped(): + while not self.isCanceled(): # In the event the server goes offline - while self.suspended(): - if self.stopped(): + while self.isSuspended(): + if self.isCanceled(): # Abort was requested while waiting. We should exit LOG.info("###===--- LibrarySync Stopped ---===###") return xbmc.sleep(1000) - if not self.install_sync_done: - # Very first sync upon installation or reset of Kodi DB - state.DB_SCAN = True - utils.window('plex_dbScan', value='true') + if not install_sync_done: + # Very FIRST sync ever upon installation or reset of Kodi DB + set_library_scan_toggle() # Initialize time offset Kodi - PMS - self.sync_pms_time() + library_sync.sync_pms_time() last_time_sync = utils.unix_timestamp() LOG.info('Initial start-up full sync starting') xbmc.executebuiltin('InhibitIdleShutdown(true)') - # Completely refresh Kodi playlists and video nodes - utils.delete_playlists() - utils.delete_nodes() - if not self.maintain_views(): - LOG.error('Initial maintain_views not successful') - elif self.full_sync(): + # This call will block until scan is completed + self.start_library_sync(show_dialog=True, block=True) + if self.sync_successful: LOG.info('Initial start-up full sync successful') utils.settings('SyncInstallRunDone', value='true') - self.install_sync_done = True + install_sync_done = True utils.settings('dbCreatedWithVersion', v.ADDON_VERSION) self.force_dialog = False - initial_sync_done = True kodi_db_version_checked = True - last_sync = utils.unix_timestamp() - if PLAYLIST_SYNC_ENABLED: + if library_sync.PLAYLIST_SYNC_ENABLED: + from . import playlists playlist_monitor = playlists.kodi_playlist_monitor() self.sync_fanart() self.fanartthread.start() else: LOG.error('Initial start-up full sync unsuccessful') xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False elif not kodi_db_version_checked: # Install sync was already done, don't force-show dialogs @@ -1661,32 +664,25 @@ class LibrarySync(Thread): elif not initial_sync_done: # First sync upon PKC restart. Skipped if very first sync upon # PKC installation has been completed - state.DB_SCAN = True - utils.window('plex_dbScan', value="true") + set_library_scan_toggle() LOG.info('Doing initial sync on Kodi startup') if state.SUSPEND_SYNC: LOG.warning('Forcing startup sync even if Kodi is playing') state.SUSPEND_SYNC = False - # Completely refresh Kodi playlists and video nodes - utils.delete_playlists() - utils.delete_nodes() - if not self.maintain_views(): - LOG.info('Initial maintain_views on startup unsuccessful') - elif self.full_sync(): + self.start_library_sync(block=True) + if self.sync_successful: initial_sync_done = True - last_sync = utils.unix_timestamp() LOG.info('Done initial sync on Kodi startup') - if PLAYLIST_SYNC_ENABLED: + if library_sync.PLAYLIST_SYNC_ENABLED: + from . import playlists playlist_monitor = playlists.kodi_playlist_monitor() artwork.Artwork().cache_major_artwork() self.sync_fanart() self.fanartthread.start() else: LOG.info('Startup sync has not yet been successful') - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False - # Currently no db scan, so we can start a new scan + # Currently no db scan, so we could start a new scan elif state.DB_SCAN is False: # Full scan was requested from somewhere else, e.g. userclient if state.RUN_LIB_SCAN is not None: @@ -1694,18 +690,19 @@ class LibrarySync(Thread): self.force_dialog = True self.triage_lib_scans() self.force_dialog = False + # Reset the flag + state.RUN_LIB_SCAN = None continue - now = utils.unix_timestamp() + # Standard syncs - don't force-show dialogs + now = utils.unix_timestamp() self.force_dialog = False - if (now - last_sync > state.FULL_SYNC_INTERVALL and - not self.suspend_item_sync()): + if (now - self.last_full_sync > state.FULL_SYNC_INTERVALL): LOG.info('Doing scheduled full library scan') - state.DB_SCAN = True - utils.window('plex_dbScan', value="true") + set_library_scan_toggle() success = self.maintain_views() if success: - success = self.full_sync() + success = library_sync.start() if not success and not self.suspend_item_sync(): LOG.error('Could not finish scheduled full sync') self.force_dialog = True @@ -1713,16 +710,14 @@ class LibrarySync(Thread): icon='error') self.force_dialog = False elif success: - last_sync = now + self.last_full_sync = now # Full library sync finished successfully self.show_kodi_note(utils.lang(39407)) else: LOG.info('Full sync interrupted') - utils.window('plex_dbScan', clear=True) - state.DB_SCAN = False elif now - last_time_sync > one_day_in_seconds: LOG.info('Starting daily time sync') - self.sync_pms_time() + library_sync.sync_pms_time() last_time_sync = now elif not state.BACKGROUND_SYNC_DISABLED: # Check back whether we should process something @@ -1734,7 +729,7 @@ class LibrarySync(Thread): # See if there is a PMS message we need to handle try: message = queue.get(block=False) - except Queue.Empty: + except backgroundthread.Queue.Empty: pass # Got a message from PMS; process it else: @@ -1750,6 +745,6 @@ class LibrarySync(Thread): # doUtils could still have a session open due to interrupted sync try: DU().stopSession() - except: + except AttributeError: pass LOG.info("###===--- LibrarySync Stopped ---===###") diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 9b39efba..88ead020 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -588,7 +588,7 @@ class SectionItems(DownloadGen): """ Iterator object to get all items of a Plex library section """ - def __init__(self, section_id, args): + def __init__(self, section_id, args=None): super(SectionItems, self).__init__( '{server}/library/sections/%s/all' % section_id, args) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 53eb9617..b973ce74 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -233,10 +233,12 @@ def dialog(typus, *args, **kwargs): return types[typus](*args, **kwargs) -def ERROR(txt='', hide_tb=False, notify=False): +def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False): import sys short = str(sys.exc_info()[1]) LOG.error('Error encountered: %s - %s', txt, short) + if cancel_sync: + state.STOP_SYNC = True if hide_tb: return short