diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f0152dd7..818a9146 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -60,29 +60,6 @@ try: except ImportError: import xml.etree.ElementTree as etree -def XbmcItemtypes(): - return ['photo', 'video', 'audio'] - -def PlexItemtypes(): - return ['photo', 'video', 'audio'] - -def PlexLibraryItemtypes(): - return ['movie', 'show'] - # later add: 'artist', 'photo' - -def XbmcPhoto(): - return "photo" -def XbmcVideo(): - return "video" -def XbmcAudio(): - return "audio" -def PlexPhoto(): - return "photo" -def PlexVideo(): - return "video" -def PlexAudio(): - return "music" - @utils.logging class PlexAPI(): @@ -1381,154 +1358,6 @@ class PlexAPI(): }) return serverlist - def GetPlexCollections(self, mediatype): - """ - Input: - mediatype String or list of strings with possible values - 'movie', 'show', 'artist', 'photo' - Output: - List with an entry of the form: - { - 'name': xxx Plex title for the media section - 'type': xxx Plex type: 'movie', 'show', 'artist', 'photo' - 'id': xxx Plex unique key for the section (1, 2, 3...) - 'uuid': xxx Other unique Plex key, e.g. - 74aec9f2-a312-4723-9436-de2ea43843c1 - } - Returns an empty list if nothing is found. - """ - collections = [] - url = "{server}/library/sections" - jsondata = self.doUtils.downloadUrl(url) - try: - result = jsondata['_children'] - except KeyError: - pass - else: - for item in result: - contentType = item['type'] - if contentType in mediatype: - name = item['title'] - contentId = item['key'] - uuid = item['uuid'] - collections.append({ - 'name': name, - 'type': contentType, - 'id': str(contentId), - 'uuid': uuid - }) - return collections - - def GetPlexSectionResults(self, viewId, headerOptions={}): - """ - Returns a list (raw JSON or XML API dump) of all Plex items in the Plex - section with key = viewId. - """ - result = [] - url = "{server}/library/sections/%s/all" % viewId - jsondata = self.doUtils.downloadUrl(url, headerOptions=headerOptions) - try: - result = jsondata['_children'] - except TypeError: - # Maybe we received an XML, check for that with tag attribute - try: - jsondata.tag - result = jsondata - # Nope, not an XML, abort - except AttributeError: - self.logMsg("Error retrieving all items for Plex section %s" - % viewId, -1) - return result - except KeyError: - self.logMsg("Error retrieving all items for Plex section %s" - % viewId, -1) - return result - - def GetAllPlexLeaves(self, viewId, headerOptions={}): - """ - Returns a list (raw JSON or XML API dump) of all Plex subitems for the - key. - (e.g. /library/sections/2/allLeaves pointing to all TV shows) - - Input: - viewId Id of Plex library, e.g. '2' - headerOptions to override the download headers - """ - result = [] - url = "{server}/library/sections/%s/allLeaves" % viewId - jsondata = self.doUtils.downloadUrl(url, headerOptions=headerOptions) - try: - result = jsondata['_children'] - except TypeError: - # Maybe we received an XML, check for that with tag attribute - try: - jsondata.tag - result = jsondata - # Nope, not an XML, abort - except AttributeError: - self.logMsg("Error retrieving all leaves for Plex section %s" - % viewId, -1) - return result - except KeyError: - self.logMsg("Error retrieving all leaves for Plex viewId %s" - % viewId, -1) - return result - - def GetAllPlexChildren(self, key): - """ - Returns a list (raw JSON API dump) of all Plex children for the key. - (e.g. /library/metadata/194853/children pointing to a season) - - Input: - key Key to a Plex item, e.g. 12345 - """ - result = [] - url = "{server}/library/metadata/%s/children" % key - jsondata = self.doUtils.downloadUrl(url) - try: - result = jsondata['_children'] - except KeyError: - self.logMsg("Error retrieving all children for Plex item %s" % key, -1) - pass - return result - - def GetPlexMetadata(self, key): - """ - Returns raw API metadata for key as an etree XML. - - Can be called with either Plex key '/library/metadata/xxxx'metadata - OR with the digits 'xxxx' only. - - Returns an empty string '' if something went wrong - """ - xml = '' - key = str(key) - if '/library/metadata/' in key: - url = "{server}" + key - else: - url = "{server}/library/metadata/" + key - arguments = { - 'checkFiles': 1, # No idea - 'includeExtras': 1, # Trailers and Extras => Extras - 'includeRelated': 1, # Similar movies => Video -> Related - 'includeRelatedCount': 5, - 'includeOnDeck': 1, - 'includeChapters': 1, - 'includePopularLeaves': 1, - 'includeConcerts': 1 - } - url = url + '?' + urlencode(arguments) - headerOptions = {'Accept': 'application/xml'} - xml = self.doUtils.downloadUrl(url, headerOptions=headerOptions) - # Did we receive a valid XML? - try: - xml.tag - # Nope we did not receive a valid XML - except AttributeError: - self.logMsg("Error retrieving metadata for %s" % url, -1) - xml = '' - return xml - @utils.logging class API(): diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py new file mode 100644 index 00000000..f91efcc2 --- /dev/null +++ b/resources/lib/PlexFunctions.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +from urllib import urlencode + +from xbmcaddon import Addon + +from downloadutils import DownloadUtils +from utils import logMsg + + +addonName = Addon().getAddonInfo('name') +title = "%s %s" % (addonName, __name__) + + +def XbmcItemtypes(): + return ['photo', 'video', 'audio'] + + +def PlexItemtypes(): + return ['photo', 'video', 'audio'] + + +def PlexLibraryItemtypes(): + return ['movie', 'show'] + # later add: 'artist', 'photo' + + +def EmbyItemtypes(): + return ['Movie', 'Series', 'Season', 'Episode'] + + +def XbmcPhoto(): + return "photo" +def XbmcVideo(): + return "video" +def XbmcAudio(): + return "audio" +def PlexPhoto(): + return "photo" +def PlexVideo(): + return "video" +def PlexAudio(): + return "music" + + +def GetPlexMetadata(key): + """ + Returns raw API metadata for key as an etree XML. + + Can be called with either Plex key '/library/metadata/xxxx'metadata + OR with the digits 'xxxx' only. + + Returns an empty string '' if something went wrong + """ + xml = '' + key = str(key) + if '/library/metadata/' in key: + url = "{server}" + key + else: + url = "{server}/library/metadata/" + key + arguments = { + 'checkFiles': 1, # No idea + 'includeExtras': 1, # Trailers and Extras => Extras + 'includeRelated': 1, # Similar movies => Video -> Related + 'includeRelatedCount': 5, + 'includeOnDeck': 1, + 'includeChapters': 1, + 'includePopularLeaves': 1, + 'includeConcerts': 1 + } + url = url + '?' + urlencode(arguments) + headerOptions = {'Accept': 'application/xml'} + xml = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + # Did we receive a valid XML? + try: + xml.tag + # Nope we did not receive a valid XML + except AttributeError: + logMsg(title, "Error retrieving metadata for %s" % url, -1) + xml = '' + return xml + + +def GetAllPlexChildren(key): + """ + Returns a list (raw JSON API dump) of all Plex children for the key. + (e.g. /library/metadata/194853/children pointing to a season) + + Input: + key Key to a Plex item, e.g. 12345 + """ + result = [] + url = "{server}/library/metadata/%s/children" % key + jsondata = DownloadUtils().downloadUrl(url) + try: + result = jsondata['_children'] + except KeyError: + logMsg( + title, "Error retrieving all children for Plex item %s" % key, -1) + pass + return result + + +def GetPlexSectionResults(viewId, headerOptions={}): + """ + Returns a list (raw JSON or XML API dump) of all Plex items in the Plex + section with key = viewId. + """ + result = [] + url = "{server}/library/sections/%s/all" % viewId + jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + try: + result = jsondata['_children'] + except TypeError: + # Maybe we received an XML, check for that with tag attribute + try: + jsondata.tag + result = jsondata + # Nope, not an XML, abort + except AttributeError: + logMsg(title, + "Error retrieving all items for Plex section %s" + % viewId, -1) + return result + except KeyError: + logMsg(title, + "Error retrieving all items for Plex section %s" + % viewId, -1) + return result + + +def GetAllPlexLeaves(viewId, headerOptions={}): + """ + Returns a list (raw JSON or XML API dump) of all Plex subitems for the + key. + (e.g. /library/sections/2/allLeaves pointing to all TV shows) + + Input: + viewId Id of Plex library, e.g. '2' + headerOptions to override the download headers + """ + result = [] + url = "{server}/library/sections/%s/allLeaves" % viewId + jsondata = DownloadUtils().downloadUrl(url, headerOptions=headerOptions) + try: + result = jsondata['_children'] + except TypeError: + # Maybe we received an XML, check for that with tag attribute + try: + jsondata.tag + result = jsondata + # Nope, not an XML, abort + except AttributeError: + logMsg(title, + "Error retrieving all leaves for Plex section %s" + % viewId, -1) + return result + except KeyError: + logMsg("Error retrieving all leaves for Plex viewId %s" % viewId, -1) + return result + + +def GetPlexCollections(mediatype): + """ + Input: + mediatype String or list of strings with possible values + 'movie', 'show', 'artist', 'photo' + Output: + List with an entry of the form: + { + 'name': xxx Plex title for the media section + 'type': xxx Plex type: 'movie', 'show', 'artist', 'photo' + 'id': xxx Plex unique key for the section (1, 2, 3...) + 'uuid': xxx Other unique Plex key, e.g. + 74aec9f2-a312-4723-9436-de2ea43843c1 + } + Returns an empty list if nothing is found. + """ + collections = [] + url = "{server}/library/sections" + jsondata = DownloadUtils().downloadUrl(url) + try: + result = jsondata['_children'] + except KeyError: + pass + else: + for item in result: + contentType = item['type'] + if contentType in mediatype: + name = item['title'] + contentId = item['key'] + uuid = item['uuid'] + collections.append({ + 'name': name, + 'type': contentType, + 'id': str(contentId), + 'uuid': uuid + }) + return collections diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 61af9eaa..fc426c39 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -26,6 +26,7 @@ import playutils import api import PlexAPI +import PlexFunctions import embydb_functions ################################################################################################# @@ -61,13 +62,13 @@ def plexCompanion(fullurl, resume=None): else: resume = round(float(resume) / 1000.0, 6) # Start playing - item = PlexAPI.PlexAPI().GetPlexMetadata(itemid) + item = PlexFunctions.GetPlexMetadata(itemid) pbutils.PlaybackUtils(item).play(itemid, dbid, seektime=resume) def doPlayback(itemid, dbid): # Get a first XML to get the librarySectionUUID - item = PlexAPI.PlexAPI().GetPlexMetadata(itemid) + item = PlexFunctions.GetPlexMetadata(itemid) # Use that to call the playlist xmlPlaylist = PlexAPI.API(item).GetPlexPlaylist() if xmlPlaylist: diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index fa786fe8..5d29fbea 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -3,7 +3,7 @@ ################################################################################################## import threading -from datetime import datetime, timedelta +from datetime import datetime import Queue import xbmc @@ -22,6 +22,7 @@ import userclient import videonodes import PlexAPI +import PlexFunctions ################################################################################################## @@ -47,7 +48,6 @@ class ThreadedGetMetadata(threading.Thread): threading.Thread.__init__(self) def run(self): - plx = PlexAPI.PlexAPI() # cache local variables because it's faster queue = self.queue out_queue = self.out_queue @@ -63,7 +63,7 @@ class ThreadedGetMetadata(threading.Thread): continue # Download Metadata try: - plexXML = plx.GetPlexMetadata(updateItem['itemId']) + plexXML = PlexFunctions.GetPlexMetadata(updateItem['itemId']) except: raise # check whether valid XML @@ -218,7 +218,6 @@ class LibrarySync(threading.Thread): self.user = userclient.UserClient() self.emby = embyserver.Read_EmbyServer() self.vnodes = videonodes.VideoNodes() - self.plx = PlexAPI.PlexAPI() self.syncThreadNumber = int(utils.settings('syncThreadNumber')) threading.Thread.__init__(self) @@ -263,37 +262,44 @@ class LibrarySync(threading.Thread): # Get last sync time lastSync = utils.window('LastIncrementalSync') if not lastSync: - lastSync = "2016-01-01T00:00:00Z" + # Original Emby format: + # lastSync = "2016-01-01T00:00:00Z" + # January 1, 2015 at midnight: + lastSync = '1420070400' self.logMsg("Last sync run: %s" % lastSync, 1) - # Convert time to unix main time or whatever it is called - # Get all PMS views + # Get all PMS views and PMS items already saved in Kodi self.maintainViews() embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() emby_db = embydb.Embydb_Functions(embycursor) views = [] - for itemtype in PlexAPI.PlexLibraryItemtypes(): + for itemtype in PlexFunctions.PlexLibraryItemtypes(): views.append(emby_db.getView_byType(itemtype)) - - # Also get checksums of Plex items already saved in Kodi - self.allKodiElementsId = {} - for itemtype in PlexAPI.dk(): + self.logMsg("views is now: %s" % views, 2) + # Also get checksums of every Plex items already saved in Kodi + allKodiElementsId = {} + for itemtype in PlexFunctions.EmbyItemtypes(): try: - self.allKodiElementsId = dict(emby_db.getChecksum('Movie')) + allKodiElementsId.update(dict(emby_db.getChecksum(itemtype))) except ValueError: pass - - views = doUtils.downloadUrl("{server}/library/sections") - try: - views = views['_children'] - except TypeError: - self.logMsg("Could not process fastSync view json, aborting", 0) - return False + self.logMsg("allKodiElementsId is now: %s" % allKodiElementsId, 2) # Run through views and get latest changed elements using time diff for view in views: - pass + if self.threadStopped(): + return False + # Get items per view + viewId = view['id'] + viewName = view['name'] + all_plexmovies = PlexFunctions.GetPlexSectionResults(viewId) + # Populate self.updatelist and self.allPlexElementsId + self.GetUpdatelist(all_plexmovies, + itemType, + 'add_update', + viewName, + viewId) # Figure out whether an item needs updating # Process updates @@ -322,12 +328,8 @@ class LibrarySync(threading.Thread): def saveLastSync(self): # Save last sync time - overlap = 2 - - time_now = datetime.utcnow()-timedelta(minutes=overlap) - lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') - self.logMsg("New sync time: client time -%s min: %s" - % (overlap, lastSync), 1) + lastSync = str(utils.getUnixTimestamp()) + self.logMsg("New sync time: %s" % lastSync, 1) utils.window('LastIncrementalSync', value=lastSync) def initializeDBs(self): @@ -728,7 +730,6 @@ class LibrarySync(threading.Thread): def PlexMovies(self): # Initialize - plx = PlexAPI.PlexAPI() self.allPlexElementsId = {} embyconn = utils.kodiSQL('emby') @@ -761,7 +762,7 @@ class LibrarySync(threading.Thread): # Get items per view viewId = view['id'] viewName = view['name'] - all_plexmovies = plx.GetPlexSectionResults(viewId) + all_plexmovies = PlexFunctions.GetPlexSectionResults(viewId) # Populate self.updatelist and self.allPlexElementsId self.GetUpdatelist(all_plexmovies, itemType, @@ -791,11 +792,10 @@ class LibrarySync(threading.Thread): This is done by downloading one XML for ALL elements with viewId """ starttotal = datetime.now() - plx = PlexAPI.PlexAPI() # Download XML, not JSON, because PMS JSON seems to be damaged headerOptions = {'Accept': 'application/xml'} - plexItems = plx.GetAllPlexLeaves(viewId, - headerOptions=headerOptions) + plexItems = PlexFunctions.GetAllPlexLeaves( + viewId, headerOptions=headerOptions) itemMth = getattr(itemtypes, itemType) with itemMth() as method: method.updateUserdata(plexItems) @@ -854,7 +854,6 @@ class LibrarySync(threading.Thread): def PlexTVShows(self): # Initialize - plx = PlexAPI.PlexAPI() self.allPlexElementsId = {} itemType = 'TVShows' # Open DB connections @@ -897,7 +896,7 @@ class LibrarySync(threading.Thread): # Get items per view viewId = view['id'] viewName = view['name'] - allPlexTvShows = plx.GetPlexSectionResults(viewId) + allPlexTvShows = PlexFunctions.GetPlexSectionResults(viewId) # Populate self.updatelist and self.allPlexElementsId self.GetUpdatelist(allPlexTvShows, itemType, @@ -915,7 +914,7 @@ class LibrarySync(threading.Thread): if self.threadStopped(): return False # Grab all seasons to tvshow from PMS - seasons = plx.GetAllPlexChildren(tvShowId) + seasons = PlexFunctions.GetAllPlexChildren(tvShowId) # Populate self.updatelist and self.allPlexElementsId self.GetUpdatelist(seasons, itemType, @@ -931,7 +930,7 @@ class LibrarySync(threading.Thread): if self.threadStopped(): return False # Grab all episodes to tvshow from PMS - episodes = plx.GetAllPlexLeaves(view['id']) + episodes = PlexFunctions.GetAllPlexLeaves(view['id']) # Populate self.updatelist and self.allPlexElementsId self.GetUpdatelist(episodes, itemType, @@ -948,7 +947,7 @@ class LibrarySync(threading.Thread): # Cycle through tv shows with itemtypes.TVShows() as TVshow: for tvShowId in allPlexTvShowsId: - XMLtvshow = plx.GetPlexMetadata(tvShowId) + XMLtvshow = PlexFunctions.GetPlexMetadata(tvShowId) TVshow.refreshSeasonEntry(XMLtvshow, tvShowId) self.logMsg("Season info refreshed", 1) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index edc05f52..72cc3558 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -10,6 +10,8 @@ import time import unicodedata import xml.etree.ElementTree as etree from functools import wraps, update_wrapper +from datetime import datetime, timedelta +from calendar import timegm import xbmc import xbmcaddon @@ -114,7 +116,7 @@ def logging(cls): # Define new class methods and attach them to class def newFunction(self, msg, lvl=0): - title = "%s %s" % (self.addonName, cls.__name__) + title = "%s %s" % (addonName, cls.__name__) logMsg(title, msg, lvl) cls.logMsg = newFunction @@ -122,6 +124,21 @@ def logging(cls): return cls +def getUnixTimestamp(secondsIntoTheFuture=None): + """ + Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as + an integer. + + Optionally, pass secondsIntoTheFuture: positive int's will result in a + future timestamp, negative the past + """ + if secondsIntoTheFuture: + future = datetime.utcnow() + timedelta(seconds=secondsIntoTheFuture) + else: + future = datetime.utcnow() + return timegm(future.timetuple()) + + def logMsg(title, msg, level=1): # Get the logLevel set in UserClient