# -*- coding: utf-8 -*- ################################################################################################## import logging import sqlite3 import threading from datetime import datetime, timedelta, time import xbmc import xbmcgui import xbmcvfs import api import utils import clientinfo import downloadutils import itemtypes import embydb_functions as embydb import kodidb_functions as kodidb import read_embyserver as embyserver import userclient import videonodes from utils import window, settings, language as lang ################################################################################################## log = logging.getLogger("EMBY."+__name__) ################################################################################################## class LibrarySync(threading.Thread): _shared_state = {} stop_thread = False suspend_thread = False # Track websocketclient updates addedItems = [] updateItems = [] userdataItems = [] removeItems = [] forceLibraryUpdate = False refresh_views = False def __init__(self): self.__dict__ = self._shared_state self.monitor = xbmc.Monitor() self.clientInfo = clientinfo.ClientInfo() self.addonName = self.clientInfo.getAddonName() self.doUtils = downloadutils.DownloadUtils().downloadUrl self.user = userclient.UserClient() self.emby = embyserver.Read_EmbyServer() self.vnodes = videonodes.VideoNodes() threading.Thread.__init__(self) def progressDialog(self, title): dialog = None dialog = xbmcgui.DialogProgressBG() dialog.create("Emby for Kodi", title) log.debug("Show progress dialog: %s" % title) return dialog def startSync(self): # Run at start up - optional to use the server plugin if settings('SyncInstallRunDone') == "true": # Validate views self.refreshViews() completed = False # Verify if server plugin is installed. if settings('serverSync') == "true": # Try to use fast start up url = "{server}/emby/Plugins?format=json" result = self.doUtils(url) for plugin in result: if plugin['Name'] == "Emby.Kodi Sync Queue": log.debug("Found server plugin.") completed = self.fastSync() break if not completed: # Fast sync failed or server plugin is not found completed = ManualSync().sync() else: # Install sync is not completed completed = self.fullSync() return completed def fastSync(self): lastSync = settings('LastIncrementalSync') if not lastSync: lastSync = "2010-01-01T00:00:00Z" lastSyncTime = utils.convertDate(lastSync) log.info("Last sync run: %s" % lastSyncTime) # get server RetentionDateTime result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") try: retention_time = result['RetentionDateTime'] except (TypeError, KeyError): retention_time = "2010-01-01T00:00:00Z" retention_time = utils.convertDate(retention_time) log.info("RetentionDateTime: %s" % retention_time) # if last sync before retention time do a full sync if retention_time > lastSyncTime: log.info("Fast sync server retention insufficient, fall back to full sync") return False params = {'LastUpdateDT': lastSync} url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json" result = self.doUtils(url, parameters=params) try: processlist = { 'added': result['ItemsAdded'], 'update': result['ItemsUpdated'], 'userdata': result['UserDataChanged'], 'remove': result['ItemsRemoved'] } except (KeyError, TypeError): log.error("Failed to retrieve latest updates using fast sync.") return False else: log.info("Fast sync changes: %s" % result) for action in processlist: self.triage_items(action, processlist[action]) return True def saveLastSync(self): # Save last sync time overlap = 2 result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") try: # datetime fails when used more than once, TypeError server_time = result['ServerDateTime'] server_time = utils.convertDate(server_time) except Exception as e: # If the server plugin is not installed or an error happened. log.error("An exception occurred: %s" % e) time_now = datetime.utcnow()-timedelta(minutes=overlap) lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') log.info("New sync time: client time -%s min: %s" % (overlap, lastSync)) else: lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ') log.info("New sync time: server time -%s min: %s" % (overlap, lastSync)) finally: settings('LastIncrementalSync', value=lastSync) def shouldStop(self): # Checkpoint during the syncing process if self.monitor.abortRequested(): return True elif window('emby_shouldStop') == "true": return True else: # Keep going return False def dbCommit(self, connection): # Central commit, verifies if Kodi database update is running kodidb_scan = window('emby_kodiScan') == "true" while kodidb_scan: log.info("Kodi scan is running. Waiting...") kodidb_scan = window('emby_kodiScan') == "true" if self.shouldStop(): log.info("Commit unsuccessful. Sync terminated.") break if self.monitor.waitForAbort(1): # Abort was requested while waiting. We should exit log.info("Commit unsuccessful.") break else: connection.commit() log.info("Commit successful.") def fullSync(self, manualrun=False, repair=False): # Only run once when first setting up. Can be run manually. music_enabled = settings('enableMusic') == "true" xbmc.executebuiltin('InhibitIdleShutdown(true)') screensaver = utils.getScreensaver() utils.setScreensaver(value="") window('emby_dbScan', value="true") # Add sources utils.sourcesXML() embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() # Create the tables for the emby database # emby, view, version embycursor.execute( """CREATE TABLE IF NOT EXISTS emby( emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""") embycursor.execute( """CREATE TABLE IF NOT EXISTS view( view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") embyconn.commit() # content sync: movies, tvshows, musicvideos, music kodiconn = utils.kodiSQL('video') kodicursor = kodiconn.cursor() if manualrun: message = "Manual sync" elif repair: message = "Repair sync" else: message = "Initial sync" window('emby_initialScan', value="true") pDialog = self.progressDialog("%s" % message) starttotal = datetime.now() # Set views self.maintainViews(embycursor, kodicursor) embyconn.commit() # Sync video library process = { 'movies': self.movies, 'musicvideos': self.musicvideos, 'tvshows': self.tvshows } for itemtype in process: startTime = datetime.now() completed = process[itemtype](embycursor, kodicursor, pDialog) if not completed: xbmc.executebuiltin('InhibitIdleShutdown(false)') utils.setScreensaver(value=screensaver) window('emby_dbScan', clear=True) if pDialog: pDialog.close() embycursor.close() kodicursor.close() return False else: self.dbCommit(kodiconn) embyconn.commit() elapsedTime = datetime.now() - startTime log.info("SyncDatabase (finished %s in: %s)" % (itemtype, str(elapsedTime).split('.')[0])) else: # Close the Kodi cursor kodicursor.close() # sync music if music_enabled: musicconn = utils.kodiSQL('music') musiccursor = musicconn.cursor() startTime = datetime.now() completed = self.music(embycursor, musiccursor, pDialog) if not completed: xbmc.executebuiltin('InhibitIdleShutdown(false)') utils.setScreensaver(value=screensaver) window('emby_dbScan', clear=True) if pDialog: pDialog.close() embycursor.close() musiccursor.close() return False else: musicconn.commit() embyconn.commit() elapsedTime = datetime.now() - startTime log.info("SyncDatabase (finished music in: %s)" % (str(elapsedTime).split('.')[0])) musiccursor.close() if pDialog: pDialog.close() embycursor.close() settings('SyncInstallRunDone', value="true") settings("dbCreatedWithVersion", self.clientInfo.getVersion()) self.saveLastSync() xbmc.executebuiltin('UpdateLibrary(video)') elapsedtotal = datetime.now() - starttotal xbmc.executebuiltin('InhibitIdleShutdown(false)') utils.setScreensaver(value=screensaver) window('emby_dbScan', clear=True) window('emby_initialScan', clear=True) xbmcgui.Dialog().notification( heading=lang(29999), message="%s %s %s" % (message, lang(33025), str(elapsedtotal).split('.')[0]), icon="special://home/addons/plugin.video.emby/icon.png", sound=False) return True def refreshViews(self): embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() kodiconn = utils.kodiSQL('video') kodicursor = kodiconn.cursor() # Compare views, assign correct tags to items self.maintainViews(embycursor, kodicursor) self.dbCommit(kodiconn) kodicursor.close() embyconn.commit() embycursor.close() def maintainViews(self, embycursor, kodicursor): # Compare the views to emby emby = self.emby emby_db = embydb.Embydb_Functions(embycursor) kodi_db = kodidb.Kodidb_Functions(kodicursor) # Get views result = self.doUtils("{server}/emby/Users/{UserId}/Views?format=json") grouped_views = result['Items'] ordered_views = self.emby.getViews(sortedlist=True) all_views = [] sorted_views = [] for view in ordered_views: all_views.append(view['name']) if view['type'] == "music": continue if view['type'] == "mixed": sorted_views.append(view['name']) sorted_views.append(view['name']) log.info("Sorted views: %s" % sorted_views) # total nodes for window properties self.vnodes.clearProperties() totalnodes = len(sorted_views) + 0 current_views = emby_db.getViews() # Set views for supported media type emby_mediatypes = { 'movies': "Movie", 'tvshows': "Series", 'musicvideos': "MusicVideo", 'homevideos': "Video", 'music': "Audio", 'photos': "Photo" } for mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']: nodes = [] # Prevent duplicate for nodes of the same type playlists = [] # Prevent duplicate for playlists of the same type # Get media folders from server folders = self.emby.getViews(mediatype, root=True) for folder in folders: folderid = folder['id'] foldername = folder['name'] viewtype = folder['type'] if foldername not in all_views: # Media folders are grouped into userview params = { 'ParentId': folderid, 'Recursive': True, 'Limit': 1, 'IncludeItemTypes': emby_mediatypes[mediatype] } # Get one item from server using the folderid url = "{server}/emby/Users/{UserId}/Items?format=json" result = self.doUtils(url, parameters=params) try: verifyitem = result['Items'][0]['Id'] except (TypeError, IndexError): # Something is wrong. Keep the same folder name. # Could be the view is empty or the connection pass else: for grouped_view in grouped_views: # This is only reserved for the detection of grouped views if (grouped_view['Type'] == "UserView" and grouped_view.get('CollectionType') == mediatype): # Take the userview, and validate the item belong to the view if self.emby.verifyView(grouped_view['Id'], verifyitem): # Take the name of the userview log("Found corresponding view: %s %s" % (grouped_view['Name'], grouped_view['Id']), 1) foldername = grouped_view['Name'] break else: # Unable to find a match, add the name to our sorted_view list sorted_views.append(foldername) log.info("Couldn't find corresponding grouped view: %s" % sorted_views) # Failsafe try: sorted_views.index(foldername) except ValueError: sorted_views.append(foldername) # Get current media folders from emby database view = emby_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 Emby database." % folderid) tagid = kodi_db.createTag(foldername) # Create playlist for the video library if (foldername not in playlists and mediatype in ('movies', 'tvshows', 'musicvideos')): utils.playlistXSP(mediatype, foldername, folderid, viewtype) playlists.append(foldername) # Create the video node if foldername not in nodes and mediatype not in ("musicvideos", "music"): self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, viewtype, folderid) if viewtype == "mixed": # Change the value sorted_views[sorted_views.index(foldername)] = "%ss" % foldername nodes.append(foldername) totalnodes += 1 # Add view to emby database emby_db.addView(folderid, foldername, viewtype, tagid) else: log.debug(' '.join(( "Found viewid: %s" % folderid, "viewname: %s" % current_viewname, "viewtype: %s" % current_viewtype, "tagid: %s" % current_tagid))) # View is still valid try: current_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.createTag(foldername) # Update view with new info emby_db.updateView(foldername, tagid, folderid) if mediatype != "music": if emby_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.playlistXSP( mediatype, current_viewname, folderid, current_viewtype, True) # Delete video node if mediatype != "musicvideos": self.vnodes.viewNode( indexnumber=None, tagname=current_viewname, mediatype=mediatype, viewtype=current_viewtype, viewid=folderid, delete=True) # Added new playlist if (foldername not in playlists and mediatype in ('movies', 'tvshows', 'musicvideos')): utils.playlistXSP(mediatype, foldername, folderid, viewtype) playlists.append(foldername) # Add new video node if foldername not in nodes and mediatype != "musicvideos": self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, viewtype, folderid) if viewtype == "mixed": # Change the value sorted_views[sorted_views.index(foldername)] = "%ss" % foldername nodes.append(foldername) totalnodes += 1 # Update items with new tag items = emby_db.getItem_byView(folderid) for item in items: # Remove the "s" from viewtype for tags kodi_db.updateTag( current_tagid, tagid, item[0], current_viewtype[:-1]) else: # Validate the playlist exists or recreate it if mediatype != "music": if (foldername not in playlists and mediatype in ('movies', 'tvshows', 'musicvideos')): utils.playlistXSP(mediatype, foldername, folderid, viewtype) playlists.append(foldername) # Create the video node if not already exists if foldername not in nodes and mediatype != "musicvideos": self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, viewtype, folderid) if viewtype == "mixed": # Change the value sorted_views[sorted_views.index(foldername)] = "%ss" % foldername nodes.append(foldername) totalnodes += 1 else: # Add video nodes listings self.vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites") totalnodes += 1 self.vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites") totalnodes += 1 self.vnodes.singleNode(totalnodes, "channels", "movies", "channels") totalnodes += 1 # Save total window('Emby.nodes.total', str(totalnodes)) # Remove any old referenced views log.info("Removing views: %s" % current_views) for view in current_views: emby_db.removeView(view) def movies(self, embycursor, kodicursor, pdialog): # Get movies from emby emby_db = embydb.Embydb_Functions(embycursor) movies = itemtypes.Movies(embycursor, kodicursor) views = emby_db.getView_byType('movies') views += emby_db.getView_byType('mixed') log.info("Media folders: %s" % views) ##### PROCESS MOVIES ##### for view in views: if self.shouldStop(): return False # Get items per view if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33017), view['name'])) # Initial or repair sync all_embymovies = self.emby.getMovies(view['id'], dialog=pdialog) total = all_embymovies['TotalRecordCount'] embymovies = all_embymovies['Items'] if pdialog: pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) count = 0 for embymovie in embymovies: # Process individual movies if self.shouldStop(): return False title = embymovie['Name'] if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=title) count += 1 movies.add_update(embymovie, view['name'], view['id']) else: log.debug("Movies finished.") ##### PROCESS BOXSETS ##### if pdialog: pdialog.update(heading=lang(29999), message=lang(33018)) boxsets = self.emby.getBoxset(dialog=pdialog) total = boxsets['TotalRecordCount'] embyboxsets = boxsets['Items'] if pdialog: pdialog.update(heading="Processing Boxsets / %s items" % total) count = 0 for boxset in embyboxsets: # Process individual boxset if self.shouldStop(): return False title = boxset['Name'] if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=title) count += 1 movies.add_updateBoxset(boxset) else: log.debug("Boxsets finished.") return True def musicvideos(self, embycursor, kodicursor, pdialog): # Get musicvideos from emby emby_db = embydb.Embydb_Functions(embycursor) mvideos = itemtypes.MusicVideos(embycursor, kodicursor) views = emby_db.getView_byType('musicvideos') log.info("Media folders: %s" % views) for view in views: if self.shouldStop(): return False # Get items per view viewId = view['id'] viewName = view['name'] if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33019), viewName)) # Initial or repair sync all_embymvideos = self.emby.getMusicVideos(viewId, dialog=pdialog) total = all_embymvideos['TotalRecordCount'] embymvideos = all_embymvideos['Items'] if pdialog: pdialog.update(heading="Processing %s / %s items" % (viewName, total)) count = 0 for embymvideo in embymvideos: # Process individual musicvideo if self.shouldStop(): return False title = embymvideo['Name'] if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=title) count += 1 mvideos.add_update(embymvideo, viewName, viewId) else: log.debug("MusicVideos finished.") return True def tvshows(self, embycursor, kodicursor, pdialog): # Get shows from emby emby_db = embydb.Embydb_Functions(embycursor) tvshows = itemtypes.TVShows(embycursor, kodicursor) views = emby_db.getView_byType('tvshows') views += emby_db.getView_byType('mixed') log.info("Media folders: %s" % views) for view in views: if self.shouldStop(): return False # Get items per view if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33020), view['name'])) all_embytvshows = self.emby.getShows(view['id'], dialog=pdialog) total = all_embytvshows['TotalRecordCount'] embytvshows = all_embytvshows['Items'] if pdialog: pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) count = 0 for embytvshow in embytvshows: # Process individual show if self.shouldStop(): return False title = embytvshow['Name'] if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=title) count += 1 tvshows.add_update(embytvshow, view['name'], view['id']) # Process episodes all_episodes = self.emby.getEpisodesbyShow(embytvshow['Id']) for episode in all_episodes['Items']: # Process individual show if self.shouldStop(): return False episodetitle = episode['Name'] if pdialog: pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) tvshows.add_updateEpisode(episode) else: log.debug("TVShows finished.") return True def music(self, embycursor, kodicursor, pdialog): # Get music from emby emby_db = embydb.Embydb_Functions(embycursor) music = itemtypes.Music(embycursor, kodicursor) process = { 'artists': [self.emby.getArtists, music.add_updateArtist], 'albums': [self.emby.getAlbums, music.add_updateAlbum], 'songs': [self.emby.getSongs, music.add_updateSong] } for itemtype in ['artists', 'albums', 'songs']: if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33021), itemtype)) all_embyitems = process[itemtype][0](dialog=pdialog) total = all_embyitems['TotalRecordCount'] embyitems = all_embyitems['Items'] if pdialog: pdialog.update(heading="Processing %s / %s items" % (itemtype, total)) count = 0 for embyitem in embyitems: # Process individual item if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=embyitem['Name']) count += 1 process[itemtype][1](embyitem) else: log.debug("%s finished." % itemtype) return True # Reserved for websocket_client.py and fast start def triage_items(self, process, items): processlist = { 'added': self.addedItems, 'update': self.updateItems, 'userdata': self.userdataItems, 'remove': self.removeItems } if items: if process == "userdata": itemids = [] for item in items: itemids.append(item['ItemId']) items = itemids log.info("Queue %s: %s" % (process, items)) processlist[process].extend(items) def incrementalSync(self): embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() kodiconn = utils.kodiSQL('video') kodicursor = kodiconn.cursor() emby_db = embydb.Embydb_Functions(embycursor) pDialog = None update_embydb = False if self.refresh_views: # Received userconfig update self.refresh_views = False self.maintainViews(embycursor, kodicursor) self.forceLibraryUpdate = True update_embydb = True incSyncIndicator = int(settings('incSyncIndicator')) totalUpdates = len(self.addedItems) + len(self.updateItems) + len(self.userdataItems) + len(self.removeItems) log.info("incSyncIndicator=" + str(incSyncIndicator) + " totalUpdates=" + str(totalUpdates)) if incSyncIndicator != -1 and totalUpdates > incSyncIndicator: # Only present dialog if we are going to process items pDialog = self.progressDialog('Incremental sync') process = { 'added': self.addedItems, 'update': self.updateItems, 'userdata': self.userdataItems, 'remove': self.removeItems } for process_type in ['added', 'update', 'userdata', 'remove']: if process[process_type] and window('emby_kodiScan') != "true": listItems = list(process[process_type]) del process[process_type][:] # Reset class list items_process = itemtypes.Items(embycursor, kodicursor) update = False # Prepare items according to process process_type if process_type == "added": items = self.emby.sortby_mediatype(listItems) elif process_type in ("userdata", "remove"): items = emby_db.sortby_mediaType(listItems, unsorted=False) else: items = emby_db.sortby_mediaType(listItems) if items.get('Unsorted'): sorted_items = self.emby.sortby_mediatype(items['Unsorted']) doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) if doupdate: embyupdate, kodiupdate_video = doupdate if embyupdate: update_embydb = True if kodiupdate_video: self.forceLibraryUpdate = True del items['Unsorted'] doupdate = items_process.itemsbyId(items, process_type, pDialog) if doupdate: embyupdate, kodiupdate_video = doupdate if embyupdate: update_embydb = True if kodiupdate_video: self.forceLibraryUpdate = True if update_embydb: update_embydb = False log.info("Updating emby database.") embyconn.commit() self.saveLastSync() if self.forceLibraryUpdate: # Force update the Kodi library self.forceLibraryUpdate = False self.dbCommit(kodiconn) log.info("Updating video library.") window('emby_kodiScan', value="true") xbmc.executebuiltin('UpdateLibrary(video)') if pDialog: pDialog.close() kodicursor.close() embycursor.close() def compareDBVersion(self, current, minimum): # It returns True is database is up to date. False otherwise. log.info("current: %s minimum: %s" % (current, minimum)) currMajor, currMinor, currPatch = current.split(".") minMajor, minMinor, minPatch = minimum.split(".") if currMajor > minMajor: return True elif currMajor == minMajor and (currMinor > minMinor or (currMinor == minMinor and currPatch >= minPatch)): return True else: # Database out of date. return False def run(self): try: self.run_internal() except Exception as e: window('emby_dbScan', clear=True) xbmcgui.Dialog().ok( heading=lang(29999), line1=( "Library sync thread has exited! " "You should restart Kodi now. " "Please report this on the forum.")) log.exception(e) raise def run_internal(self): dialog = xbmcgui.Dialog() startupComplete = False log.warn("---===### Starting LibrarySync ###===---") while not self.monitor.abortRequested(): # In the event the server goes offline while self.suspend_thread: # Set in service.py if self.monitor.waitForAbort(5): # Abort was requested while waiting. We should exit break if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): # Verify the validity of the database currentVersion = settings('dbCreatedWithVersion') minVersion = window('emby_minDBVersion') uptoDate = self.compareDBVersion(currentVersion, minVersion) if not uptoDate: log.warn("Database version out of date: %s minimum version required: %s" % (currentVersion, minVersion)) resp = dialog.yesno(lang(29999), lang(33022)) if not resp: log.warn("Database version is out of date! USER IGNORED!") dialog.ok(lang(29999), lang(33023)) else: utils.reset() break window('emby_dbCheck', value="true") if not startupComplete: # Verify the video database can be found videoDb = utils.getKodiVideoDBPath() if not xbmcvfs.exists(videoDb): # Database does not exists log.error( "The current Kodi version is incompatible " "with the Emby for Kodi add-on. Please visit " "https://github.com/MediaBrowser/Emby.Kodi/wiki " "to know which Kodi versions are supported.") dialog.ok( heading=lang(29999), line1=lang(33024)) break # Run start up sync log.warn("Database version: %s" % settings('dbCreatedWithVersion')) log.info("SyncDatabase (started)") startTime = datetime.now() librarySync = self.startSync() elapsedTime = datetime.now() - startTime log.info("SyncDatabase (finished in: %s) %s" % (str(elapsedTime).split('.')[0], librarySync)) # Only try the initial sync once per kodi session regardless # This will prevent an infinite loop in case something goes wrong. startupComplete = True # Process updates if window('emby_dbScan') != "true": self.incrementalSync() if window('emby_onWake') == "true" and window('emby_online') == "true": # Kodi is waking up # Set in kodimonitor.py window('emby_onWake', clear=True) if window('emby_syncRunning') != "true": log.info("SyncDatabase onWake (started)") librarySync = self.startSync() log.info("SyncDatabase onWake (finished) %s" % librarySync) if self.stop_thread: # Set in service.py log.debug("Service terminated thread.") break if self.monitor.waitForAbort(1): # Abort was requested while waiting. We should exit break log.warn("###===--- LibrarySync Stopped ---===###") def stopThread(self): self.stop_thread = True log.debug("Ending thread...") def suspendThread(self): self.suspend_thread = True log.debug("Pausing thread...") def resumeThread(self): self.suspend_thread = False log.debug("Resuming thread...") class ManualSync(LibrarySync): def __init__(self): LibrarySync.__init__(self) def sync(self): return self.fullSync(manualrun=True) def movies(self, embycursor, kodicursor, pdialog): # Get movies from emby emby_db = embydb.Embydb_Functions(embycursor) movies = itemtypes.Movies(embycursor, kodicursor) views = emby_db.getView_byType('movies') views += emby_db.getView_byType('mixed') log.info("Media folders: %s" % views) # Pull the list of movies and boxsets in Kodi try: all_kodimovies = dict(emby_db.getChecksum('Movie')) except ValueError: all_kodimovies = {} try: all_kodisets = dict(emby_db.getChecksum('BoxSet')) except ValueError: all_kodisets = {} all_embymoviesIds = set() all_embyboxsetsIds = set() updatelist = [] ##### PROCESS MOVIES ##### for view in views: if self.shouldStop(): return False # Get items per view viewId = view['id'] viewName = view['name'] if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33026), viewName)) all_embymovies = self.emby.getMovies(viewId, basic=True, dialog=pdialog) for embymovie in all_embymovies['Items']: if self.shouldStop(): return False API = api.API(embymovie) itemid = embymovie['Id'] all_embymoviesIds.add(itemid) if all_kodimovies.get(itemid) != API.getChecksum(): # Only update if movie is not in Kodi or checksum is different updatelist.append(itemid) log.info("Movies to update for %s: %s" % (viewName, updatelist)) embymovies = self.emby.getFullItems(updatelist) total = len(updatelist) del updatelist[:] if pdialog: pdialog.update(heading="Processing %s / %s items" % (viewName, total)) count = 0 for embymovie in embymovies: # Process individual movies if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=embymovie['Name']) count += 1 movies.add_update(embymovie, viewName, viewId) ##### PROCESS BOXSETS ##### boxsets = self.emby.getBoxset(dialog=pdialog) embyboxsets = [] if pdialog: pdialog.update(heading=lang(29999), message=lang(33027)) for boxset in boxsets['Items']: if self.shouldStop(): return False # Boxset has no real userdata, so using etag to compare itemid = boxset['Id'] all_embyboxsetsIds.add(itemid) if all_kodisets.get(itemid) != boxset['Etag']: # Only update if boxset is not in Kodi or boxset['Etag'] is different updatelist.append(itemid) embyboxsets.append(boxset) log.info("Boxsets to update: %s" % updatelist) total = len(updatelist) if pdialog: pdialog.update(heading="Processing Boxsets / %s items" % total) count = 0 for boxset in embyboxsets: # Process individual boxset if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=boxset['Name']) count += 1 movies.add_updateBoxset(boxset) ##### PROCESS DELETES ##### for kodimovie in all_kodimovies: if kodimovie not in all_embymoviesIds: movies.remove(kodimovie) else: log.info("Movies compare finished.") for boxset in all_kodisets: if boxset not in all_embyboxsetsIds: movies.remove(boxset) else: log.info("Boxsets compare finished.") return True def musicvideos(self, embycursor, kodicursor, pdialog): # Get musicvideos from emby emby_db = embydb.Embydb_Functions(embycursor) mvideos = itemtypes.MusicVideos(embycursor, kodicursor) views = emby_db.getView_byType('musicvideos') log.info("Media folders: %s" % views) # Pull the list of musicvideos in Kodi try: all_kodimvideos = dict(emby_db.getChecksum('MusicVideo')) except ValueError: all_kodimvideos = {} all_embymvideosIds = set() updatelist = [] for view in views: if self.shouldStop(): return False # Get items per view viewId = view['id'] viewName = view['name'] if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33028), viewName)) all_embymvideos = self.emby.getMusicVideos(viewId, basic=True, dialog=pdialog) for embymvideo in all_embymvideos['Items']: if self.shouldStop(): return False API = api.API(embymvideo) itemid = embymvideo['Id'] all_embymvideosIds.add(itemid) if all_kodimvideos.get(itemid) != API.getChecksum(): # Only update if musicvideo is not in Kodi or checksum is different updatelist.append(itemid) log.info("MusicVideos to update for %s: %s" % (viewName, updatelist)) embymvideos = self.emby.getFullItems(updatelist) total = len(updatelist) del updatelist[:] if pdialog: pdialog.update(heading="Processing %s / %s items" % (viewName, total)) count = 0 for embymvideo in embymvideos: # Process individual musicvideo if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=embymvideo['Name']) count += 1 mvideos.add_update(embymvideo, viewName, viewId) ##### PROCESS DELETES ##### for kodimvideo in all_kodimvideos: if kodimvideo not in all_embymvideosIds: mvideos.remove(kodimvideo) else: log.info("MusicVideos compare finished.") return True def tvshows(self, embycursor, kodicursor, pdialog): # Get shows from emby emby_db = embydb.Embydb_Functions(embycursor) tvshows = itemtypes.TVShows(embycursor, kodicursor) views = emby_db.getView_byType('tvshows') views += emby_db.getView_byType('mixed') log.info("Media folders: %s" % views) # Pull the list of tvshows and episodes in Kodi try: all_koditvshows = dict(emby_db.getChecksum('Series')) except ValueError: all_koditvshows = {} try: all_kodiepisodes = dict(emby_db.getChecksum('Episode')) except ValueError: all_kodiepisodes = {} all_embytvshowsIds = set() all_embyepisodesIds = set() updatelist = [] for view in views: if self.shouldStop(): return False # Get items per view viewId = view['id'] viewName = view['name'] if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33029), viewName)) all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog) for embytvshow in all_embytvshows['Items']: if self.shouldStop(): return False API = api.API(embytvshow) itemid = embytvshow['Id'] all_embytvshowsIds.add(itemid) if all_koditvshows.get(itemid) != API.getChecksum(): # Only update if movie is not in Kodi or checksum is different updatelist.append(itemid) log.info("TVShows to update for %s: %s" % (viewName, updatelist)) embytvshows = self.emby.getFullItems(updatelist) total = len(updatelist) del updatelist[:] if pdialog: pdialog.update(heading="Processing %s / %s items" % (viewName, total)) count = 0 for embytvshow in embytvshows: # Process individual show if self.shouldStop(): return False itemid = embytvshow['Id'] title = embytvshow['Name'] if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=title) count += 1 tvshows.add_update(embytvshow, viewName, viewId) else: # Get all episodes in view if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33030), viewName)) all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog) for embyepisode in all_embyepisodes['Items']: if self.shouldStop(): return False API = api.API(embyepisode) itemid = embyepisode['Id'] all_embyepisodesIds.add(itemid) if all_kodiepisodes.get(itemid) != API.getChecksum(): # Only update if movie is not in Kodi or checksum is different updatelist.append(itemid) log.info("Episodes to update for %s: %s" % (viewName, updatelist)) embyepisodes = self.emby.getFullItems(updatelist) total = len(updatelist) del updatelist[:] count = 0 for episode in embyepisodes: # Process individual episode if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) pdialog.update(percentage, message=title) count += 1 tvshows.add_updateEpisode(episode) ##### PROCESS DELETES ##### for koditvshow in all_koditvshows: if koditvshow not in all_embytvshowsIds: tvshows.remove(koditvshow) else: log.info("TVShows compare finished.") for kodiepisode in all_kodiepisodes: if kodiepisode not in all_embyepisodesIds: tvshows.remove(kodiepisode) else: log.info("Episodes compare finished.") return True def music(self, embycursor, kodicursor, pdialog): # Get music from emby emby_db = embydb.Embydb_Functions(embycursor) music = itemtypes.Music(embycursor, kodicursor) # Pull the list of artists, albums, songs try: all_kodiartists = dict(emby_db.getChecksum('MusicArtist')) except ValueError: all_kodiartists = {} try: all_kodialbums = dict(emby_db.getChecksum('MusicAlbum')) except ValueError: all_kodialbums = {} try: all_kodisongs = dict(emby_db.getChecksum('Audio')) except ValueError: all_kodisongs = {} all_embyartistsIds = set() all_embyalbumsIds = set() all_embysongsIds = set() updatelist = [] process = { 'artists': [self.emby.getArtists, music.add_updateArtist], 'albums': [self.emby.getAlbums, music.add_updateAlbum], 'songs': [self.emby.getSongs, music.add_updateSong] } for data_type in ['artists', 'albums', 'songs']: if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33031), data_type)) if data_type != "artists": all_embyitems = process[data_type][0](basic=True, dialog=pdialog) else: all_embyitems = process[data_type][0](dialog=pdialog) for embyitem in all_embyitems['Items']: if self.shouldStop(): return False API = api.API(embyitem) itemid = embyitem['Id'] if data_type == "artists": all_embyartistsIds.add(itemid) if all_kodiartists.get(itemid) != API.getChecksum(): # Only update if artist is not in Kodi or checksum is different updatelist.append(itemid) elif data_type == "albums": all_embyalbumsIds.add(itemid) if all_kodialbums.get(itemid) != API.getChecksum(): # Only update if album is not in Kodi or checksum is different updatelist.append(itemid) else: all_embysongsIds.add(itemid) if all_kodisongs.get(itemid) != API.getChecksum(): # Only update if songs is not in Kodi or checksum is different updatelist.append(itemid) log.info("%s to update: %s" % (data_type, updatelist)) embyitems = self.emby.getFullItems(updatelist) total = len(updatelist) del updatelist[:] if pdialog: pdialog.update(heading="Processing %s / %s items" % (data_type, total)) count = 0 for embyitem in embyitems: # Process individual item if self.shouldStop(): return False if pdialog: percentage = int((float(count) / float(total))*100) pdialog.update(percentage, message=embyitem['Name']) count += 1 process[data_type][1](embyitem) ##### PROCESS DELETES ##### for kodiartist in all_kodiartists: if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None: music.remove(kodiartist) else: log.info("Artist compare finished.") for kodialbum in all_kodialbums: if kodialbum not in all_embyalbumsIds: music.remove(kodialbum) else: log.info("Albums compare finished.") for kodisong in all_kodisongs: if kodisong not in all_embysongsIds: music.remove(kodisong) else: log.info("Songs compare finished.") return True