diff --git a/addon.xml b/addon.xml index 85d4d694..c7fdb0ca 100644 --- a/addon.xml +++ b/addon.xml @@ -1,13 +1,11 @@ - - diff --git a/changelog.txt b/changelog.txt index 876f8593..ff6f8bff 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +version 1.5.2 (beta only) +A DATABASE RESET IS ABSOLUTELY NECESSARY +- Plex Companion is completely rewired and should now handly anything you throw at it +- New playback startup mechanism for plugin paths +- Krypton: add ratings and IMDB id for movies +- Krypton: add ratings and theTvDB id for TV shows +- Don't support Plex Companion mirror +- Fix for Plex Companion not showing up +- Code rebranding from Emby to Plex, including a plex.db database :-) +- Lots of code refactoring and code optimizations + version 1.5.1 (beta only) - Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface). UPGRADE YOUR PMS!! - Improvements to the way PKC behaves if the PMS goes offline diff --git a/default.py b/default.py index 8dc00b25..c2ccfddd 100644 --- a/default.py +++ b/default.py @@ -3,36 +3,38 @@ ############################################################################### import logging -import os -import sys -import urlparse +from os import path as os_path +from sys import path as sys_path, argv +from urlparse import parse_qsl -import xbmc -import xbmcaddon -import xbmcgui +from xbmc import translatePath, sleep, executebuiltin +from xbmcaddon import Addon +from xbmcgui import ListItem, Dialog +from xbmcplugin import setResolvedUrl - -_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') +_addon = Addon(id='plugin.video.plexkodiconnect') try: _addon_path = _addon.getAddonInfo('path').decode('utf-8') except TypeError: _addon_path = _addon.getAddonInfo('path').decode() try: - _base_resource = xbmc.translatePath(os.path.join( + _base_resource = translatePath(os_path.join( _addon_path, 'resources', 'lib')).decode('utf-8') except TypeError: - _base_resource = xbmc.translatePath(os.path.join( + _base_resource = translatePath(os_path.join( _addon_path, 'resources', 'lib')).decode() -sys.path.append(_base_resource) +sys_path.append(_base_resource) ############################################################################### import entrypoint -import utils +from utils import window, pickl_window, reset, passwordsXML +from pickler import unpickle_me +from PKC_listitem import convert_PKC_to_listitem ############################################################################### @@ -43,34 +45,47 @@ log = logging.getLogger("PLEX.default") ############################################################################### +ARGV = argv +HANDLE = int(argv[1]) + class Main(): # MAIN ENTRY POINT - #@utils.profiling() + # @utils.profiling() def __init__(self): + log.debug("Full sys.argv received: %s" % ARGV) # Parse parameters - log.warn("Full sys.argv received: %s" % sys.argv) - base_url = sys.argv[0] - params = urlparse.parse_qs(sys.argv[2][1:]) + params = dict(parse_qsl(ARGV[2][1:])) try: - mode = params['mode'][0] + mode = params['mode'] itemid = params.get('id', '') - if itemid: - try: - itemid = itemid[0] - except: - pass except: - params = {} mode = "" + itemid = '' + + if mode == 'play': + # Put the request into the "queue" + while window('plex_play_new_item'): + sleep(20) + window('plex_play_new_item', + value='%s%s' % (mode, ARGV[2])) + # Wait for the result + while not pickl_window('plex_result'): + sleep(20) + result = unpickle_me() + if result is None: + log.error('Error encountered, aborting') + setResolvedUrl(HANDLE, False, ListItem()) + elif result.listitem: + listitem = convert_PKC_to_listitem(result.listitem) + setResolvedUrl(HANDLE, True, listitem) + return modes = { - - 'reset': utils.reset, + 'reset': reset, 'resetauth': entrypoint.resetAuth, - 'play': entrypoint.doPlayback, - 'passwords': utils.passwordsXML, + 'passwords': passwordsXML, 'channels': entrypoint.BrowseChannels, 'channelsfolder': entrypoint.BrowseChannels, 'browsecontent': entrypoint.BrowseContent, @@ -79,7 +94,6 @@ class Main(): 'inprogressepisodes': entrypoint.getInProgressEpisodes, 'recentepisodes': entrypoint.getRecentEpisodes, 'refreshplaylist': entrypoint.refreshPlaylist, - 'companion': entrypoint.plexCompanion, 'switchuser': entrypoint.switchPlexUser, 'deviceid': entrypoint.resetDeviceId, 'delete': entrypoint.deleteItem, @@ -89,24 +103,24 @@ class Main(): 'watchlater': entrypoint.watchlater, 'enterPMS': entrypoint.enterPMS, 'togglePlexTV': entrypoint.togglePlexTV, - 'playwatchlater': entrypoint.playWatchLater + 'Plex_Node': entrypoint.Plex_Node } - if "/extrafanart" in sys.argv[0]: - plexpath = sys.argv[2][1:] - plexid = params.get('id', [""])[0] + if "/extrafanart" in ARGV[0]: + plexpath = ARGV[2][1:] + plexid = params.get('id', [""]) entrypoint.getExtraFanArt(plexid, plexpath) entrypoint.getVideoFiles(plexid, plexpath) return if mode == 'fanart': log.info('User requested fanarttv refresh') - utils.window('plex_runLibScan', value='fanart') + window('plex_runLibScan', value='fanart') # Called by e.g. 3rd party plugin video extras - if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or - "/Extras" in sys.argv[2]): - plexId = params.get('id', [None])[0] + if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or + "/Extras" in ARGV[2]): + plexId = params.get('id', None) entrypoint.getVideoFiles(plexId, params) if modes.get(mode): @@ -117,59 +131,60 @@ class Main(): modes[mode](itemid, dbid) elif mode in ("nextup", "inprogressepisodes"): - limit = int(params['limit'][0]) + limit = int(params['limit']) modes[mode](itemid, limit) elif mode in ("channels","getsubfolders"): modes[mode](itemid) elif mode == "browsecontent": - modes[mode](itemid, params.get('type',[""])[0], params.get('folderid',[""])[0]) + modes[mode](itemid, params.get('type',[""]), params.get('folderid',[""])) elif mode == 'browseplex': modes[mode]( itemid, - params.get('type', [""])[0], - params.get('folderid', [""])[0]) + params.get('type', [""]), + params.get('folderid', [""])) elif mode in ('ondeck', 'recentepisodes'): modes[mode]( itemid, - params.get('type', [""])[0], - params.get('tagname', [""])[0], - int(params.get('limit', [""])[0])) + params.get('type', [""]), + params.get('tagname', [""]), + int(params.get('limit', [""]))) elif mode == "channelsfolder": - folderid = params['folderid'][0] + folderid = params['folderid'] modes[mode](itemid, folderid) elif mode == "companion": - modes[mode](itemid, params=sys.argv[2]) - elif mode == 'playwatchlater': - modes[mode](params.get('id')[0], params.get('viewOffset')[0]) + modes[mode](itemid, params=ARGV[2]) + elif mode == 'Plex_Node': + modes[mode](params.get('id'), + params.get('viewOffset'), + params.get('plex_type')) else: modes[mode]() else: # Other functions if mode == "settings": - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') + executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') elif mode in ("manualsync", "repair"): - if utils.window('plex_online') != "true": + if window('plex_online') != "true": # Server is not online, do not run the sync - xbmcgui.Dialog().ok( + Dialog().ok( "PlexKodiConnect", "Unable to run the sync, the add-on is not connected " "to a Plex server.") log.error("Not connected to a PMS.") else: if mode == 'repair': - utils.window('plex_runLibScan', value="repair") + window('plex_runLibScan', value="repair") log.info("Requesting repair lib sync") elif mode == 'manualsync': log.info("Requesting full library scan") - utils.window('plex_runLibScan', value="full") - + window('plex_runLibScan', value="full") elif mode == "texturecache": - utils.window('plex_runLibScan', value='del_textures') + window('plex_runLibScan', value='del_textures') else: entrypoint.doMainListing() diff --git a/resources/lib/PKC_listitem.py b/resources/lib/PKC_listitem.py new file mode 100644 index 00000000..2bb92a21 --- /dev/null +++ b/resources/lib/PKC_listitem.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging + +from xbmcgui import ListItem + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +def convert_PKC_to_listitem(PKC_listitem): + """ + Insert a PKC_listitem and you will receive a valid XBMC listitem + """ + listitem = ListItem() + for func, args in PKC_listitem.data.items(): + if isinstance(args, list): + for arg in args: + getattr(listitem, func)(*arg) + elif isinstance(args, dict): + for arg in args.items(): + getattr(listitem, func)(*arg) + elif args is None: + continue + else: + getattr(listitem, func)(args) + return listitem + + +class PKC_ListItem(object): + """ + Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data + when pickling! + + WARNING: set/get path only via setPath and getPath! (not getProperty) + """ + def __init__(self, label=None, label2=None, path=None): + self.data = { + 'addStreamInfo': [], # (type, values: dict { label: value }) + 'setArt': [], # dict: { label: value } + 'setInfo': {}, # type: infoLabel (dict { label: value }) + 'setLabel': label, # string + 'setLabel2': label2, # string + 'setPath': path, # string + 'setProperty': {}, # (key, value) + 'setSubtitles': [], # string + } + + def addContextMenuItems(self, items, replaceItems): + """ + Adds item(s) to the context menu for media lists. + + items : list - [(label, action,)*] A list of tuples consisting of label + and action pairs. + - label : string or unicode - item's label. + - action : string or unicode - any built-in function to perform. + replaceItes : [opt] bool - True=only your items will show/False=your + items will be amdded to context menu(Default). + + List of functions - http://kodi.wiki/view/List_of_Built_In_Functions + + *Note, You can use the above as keywords for arguments and skip + certain optional arguments. + + Once you use a keyword, all following arguments require the keyword. + """ + raise NotImplementedError + + def addStreamInfo(self, type, values): + """ + Add a stream with details. + type : string - type of stream(video/audio/subtitle). + values : dictionary - pairs of { label: value }. + + - Video Values: + - codec : string (h264) + - aspect : float (1.78) + - width : integer (1280) + - height : integer (720) + - duration : integer (seconds) + - Audio Values: + - codec : string (dts) + - language : string (en) + - channels : integer (2) + - Subtitle Values: + - language : string (en) + """ + self.data['addStreamInfo'].append((type, values)) + + def getLabel(self): + """ + Returns the listitem label + """ + return self.data['setLabel'] + + def getLabel2(self): + """ + Returns the listitem label. + """ + return self.data['setLabel2'] + + def getMusicInfoTag(self): + """ + returns the MusicInfoTag for this item. + """ + raise NotImplementedError + + def getProperty(self, key): + """ + Returns a listitem property as a string, similar to an infolabel. + key : string - property name. + *Note, Key is NOT case sensitive. + + You can use the above as keywords for arguments and skip certain + optional arguments. + + Once you use a keyword, all following arguments require the keyword. + """ + return self.data['setProperty'].get(key) + + def getVideoInfoTag(self): + """ + returns the VideoInfoTag for this item + """ + raise NotImplementedError + + def getdescription(self): + """ + Returns the description of this PlayListItem + """ + raise NotImplementedError + + def getduration(self): + """ + Returns the duration of this PlayListItem + """ + raise NotImplementedError + + def getfilename(self): + """ + Returns the filename of this PlayListItem. + """ + raise NotImplementedError + + def isSelected(self): + """ + Returns the listitem's selected status + """ + raise NotImplementedError + + def select(self): + """ + Sets the listitem's selected status. + selected : bool - True=selected/False=not selected + """ + raise NotImplementedError + + def setArt(self, values): + """ + Sets the listitem's art + values : dictionary - pairs of { label: value }. + + Some default art values (any string possible): + - thumb : string - image filename + - poster : string - image filename + - banner : string - image filename + - fanart : string - image filename + - clearart : string - image filename + - clearlogo : string - image filename + - landscape : string - image filename + - icon : string - image filename + """ + self.data['setArt'].append(values) + + def setContentLookup(self, enable): + """ + Enable or disable content lookup for item. + + If disabled, HEAD requests to e.g determine mime type will not be sent. + + enable : bool + """ + raise NotImplementedError + + def setInfo(self, type, infoLabels): + """ + type : string - type of media(video/music/pictures). + + infoLabels : dictionary - pairs of { label: value }. *Note, To set + pictures exif info, prepend 'exif:' to the label. Exif values must be + passed as strings, separate value pairs with a comma. (eg. + {'exif:resolution': '720,480'} + + See CPictureInfoTag::TranslateString in PictureInfoTag.cpp for valid + strings. You can use the above as keywords for arguments and skip + certain optional arguments. + + Once you use a keyword, all following arguments require the keyword. + + - General Values that apply to all types: + - count : integer (12) - can be used to store an id for later, or + for sorting purposes + - size : long (1024) - size in bytes + - date : string (d.m.Y / 01.01.2009) - file date + + - Video Values: + - genre : string (Comedy) + - year : integer (2009) + - episode : integer (4) + - season : integer (1) + - top250 : integer (192) + - tracknumber : integer (3) + - rating : float (6.4) - range is 0..10 + - userrating : integer (9) - range is 1..10 + - watched : depreciated - use playcount instead + - playcount : integer (2) - number of times this item has been + played + - overlay : integer (2) - range is 0..8. See GUIListItem.h for + values + - cast : list (["Michal C. Hall","Jennifer Carpenter"]) - if + provided a list of tuples cast will be interpreted as castandrole + - castandrole : list of tuples ([("Michael C. + Hall","Dexter"),("Jennifer Carpenter","Debra")]) + - director : string (Dagur Kari) + - mpaa : string (PG-13) + - plot : string (Long Description) + - plotoutline : string (Short Description) + - title : string (Big Fan) + - originaltitle : string (Big Fan) + - sorttitle : string (Big Fan) + - duration : integer (245) - duration in seconds + - studio : string (Warner Bros.) + - tagline : string (An awesome movie) - short description of movie + - writer : string (Robert D. Siegel) + - tvshowtitle : string (Heroes) + - premiered : string (2005-03-04) + - status : string (Continuing) - status of a TVshow + - code : string (tt0110293) - IMDb code + - aired : string (2008-12-07) + - credits : string (Andy Kaufman) - writing credits + - lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04) + - album : string (The Joshua Tree) + - artist : list (['U2']) + - votes : string (12345 votes) + - trailer : string (/home/user/trailer.avi) + - dateadded : string (Y-m-d h:m:s = 2009-04-05 23:16:04) + - mediatype : string - "video", "movie", "tvshow", "season", + "episode" or "musicvideo" + + - Music Values: + - tracknumber : integer (8) + - discnumber : integer (2) + - duration : integer (245) - duration in seconds + - year : integer (1998) + - genre : string (Rock) + - album : string (Pulse) + - artist : string (Muse) + - title : string (American Pie) + - rating : string (3) - single character between 0 and 5 + - lyrics : string (On a dark desert highway...) + - playcount : integer (2) - number of times this item has been + played + - lastplayed : string (Y-m-d h:m:s = 2009-04-05 23:16:04) + + - Picture Values: + - title : string (In the last summer-1) + - picturepath : string (/home/username/pictures/img001.jpg) + - exif : string (See CPictureInfoTag::TranslateString in + PictureInfoTag.cpp for valid strings) + """ + self.data['setInfo'][type] = infoLabels + + def setLabel(self, label): + """ + Sets the listitem's label. + label : string or unicode - text string. + """ + self.data['setLabel'] = label + + def setLabel2(self, label): + """ + Sets the listitem's label2. + label : string or unicode - text string. + """ + self.data['setLabel2'] = label + + def setMimeType(self, mimetype): + """ + Sets the listitem's mimetype if known. + mimetype : string or unicode - mimetype. + + If known prehand, this can (but does not have to) avoid HEAD requests + being sent to HTTP servers to figure out file type. + """ + raise NotImplementedError + + def setPath(self, path): + """ + Sets the listitem's path. + path : string or unicode - path, activated when item is clicked. + + *Note, You can use the above as keywords for arguments. + """ + self.data['setPath'] = path + + def setProperty(self, key, value): + """ + Sets a listitem property, similar to an infolabel. + key : string - property name. + value : string or unicode - value of property. + *Note, Key is NOT case sensitive. + + You can use the above as keywords for arguments and skip certain + optional arguments. Once you use a keyword, all following arguments + require the keyword. + + Some of these are treated internally by XBMC, such as the + 'StartOffset' property, which is the offset in seconds at which to + start playback of an item. Others may be used in the skin to add extra + information, such as 'WatchedCount' for tvshow items + """ + self.data['setProperty'][key] = value + + def setSubtitles(self, subtitles): + """ + Sets subtitles for this listitem. Pass in a list of filepaths + + example: + - listitem.setSubtitles(['special://temp/example.srt', + 'http://example.com/example.srt' ]) + """ + self.data['setSubtitles'].extend(([subtitles],)) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b63bf71a..71ca8bef 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -50,8 +50,9 @@ import downloadutils from utils import window, settings, language as lang, tryDecode, tryEncode, \ DateToKodi, KODILANGUAGE from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \ - REMAP_TYPE_FROM_PLEXTYPE -import embydb_functions as embydb + REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW, \ + PLEX_TYPE_EPISODE +import plexdb_functions as plexdb ############################################################################### @@ -1646,7 +1647,7 @@ class API(): If not found, empty str is returned """ - return self.item.attrib.get('playQueueItemID', '') + return self.item.attrib.get('playQueueItemID') def getDataFromPartOrMedia(self, key): """ @@ -1915,9 +1916,9 @@ class API(): # Return the saved Plex id's, if applicable # Always seek collection's ids since not provided by PMS if collection is False: - if media_type == 'movie': + if media_type == PLEX_TYPE_MOVIE: mediaId = self.getProvider('imdb') - elif media_type == 'show': + elif media_type == PLEX_TYPE_SHOW: mediaId = self.getProvider('tvdb') if mediaId is not None: return mediaId @@ -1927,7 +1928,7 @@ class API(): log.info('Start movie set/collection lookup on themoviedb') apiKey = settings('themoviedbAPIKey') - if media_type == 'show': + if media_type == PLEX_TYPE_SHOW: media_type = 'tv' title = item.get('title', '') # if the title has the year in remove it as tmdb cannot deal with it... @@ -2305,10 +2306,10 @@ class API(): kodiindex = 0 for stream in mediastreams: index = stream.attrib['id'] - # Since Emby returns all possible tracks together, have to pull + # Since plex returns all possible tracks together, have to pull # only external subtitles. key = stream.attrib.get('key') - # IsTextSubtitleStream if true, is available to download from emby. + # IsTextSubtitleStream if true, is available to download from plex. if stream.attrib.get('streamType') == "3" and key: # Direct stream url = ("%s%s" % (self.server, key)) @@ -2318,7 +2319,7 @@ class API(): externalsubs.append(url) kodiindex += 1 mapping = json.dumps(mapping) - window('emby_%s.indexMapping' % playurl, value=mapping) + window('plex_%s.indexMapping' % playurl, value=mapping) log.info('Found external subs: %s' % externalsubs) return externalsubs @@ -2393,7 +2394,7 @@ class API(): # listItem.setProperty('isPlayable', 'true') # listItem.setProperty('isFolder', 'true') # Further stuff - listItem.setIconImage('DefaultPicture.png') + listItem.setArt({'icon': 'DefaultPicture.png'}) return listItem def _createVideoListItem(self, @@ -2456,21 +2457,21 @@ class API(): "s%.2de%.2d" % (season, episode)) if appendSxxExx is True: title = "S%.2dE%.2d - %s" % (season, episode, title) - listItem.setIconImage('DefaultTVShows.png') + listItem.setArt({'icon': 'DefaultTVShows.png'}) if appendShowTitle is True: title = "%s - %s " % (show, title) elif typus == "movie": - listItem.setIconImage('DefaultMovies.png') + listItem.setArt({'icon': 'DefaultMovies.png'}) else: # E.g. clips, trailers, ... - listItem.setIconImage('DefaultVideo.png') + listItem.setArt({'icon': 'DefaultVideo.png'}) plexId = self.getRatingKey() listItem.setProperty('plexid', plexId) - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: try: listItem.setProperty('dbid', - str(emby_db.getItem_byId(plexId)[0])) + str(plex_db.getItem_byId(plexId)[0])) except TypeError: pass # Expensive operation @@ -2563,3 +2564,68 @@ class API(): line1=lang(39031) + url, line2=lang(39032)) return resp + + def set_listitem_artwork(self, listitem): + """ + Set all artwork to the listitem + """ + allartwork = self.getAllArtwork(parentInfo=True) + arttypes = { + 'poster': "Primary", + 'tvshow.poster': "Thumb", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearart': "Primary", + 'tvshow.clearart': "Primary", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Backdrop", + "banner": "Banner" + } + for arttype in arttypes: + art = arttypes[arttype] + if art == "Backdrop": + try: + # Backdrop is a list, grab the first backdrop + self._set_listitem_artprop(listitem, + arttype, + allartwork[art][0]) + except: + pass + else: + self._set_listitem_artprop(listitem, arttype, allartwork[art]) + + def _set_listitem_artprop(self, listitem, arttype, path): + if arttype in ( + 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', + 'medium_landscape', 'medium_poster', 'small_fanartimage', + 'medium_fanartimage', 'fanart_noindicators'): + listitem.setProperty(arttype, path) + else: + listitem.setArt({arttype: path}) + + def set_playback_win_props(self, playurl, listitem): + """ + Set all properties necessary for plugin path playback for listitem + """ + itemtype = self.getType() + userdata = self.getUserData() + + plexitem = "plex_%s" % playurl + window('%s.runtime' % plexitem, value=str(userdata['Runtime'])) + window('%s.type' % plexitem, value=itemtype) + window('%s.itemid' % plexitem, value=self.getRatingKey()) + window('%s.playcount' % plexitem, value=str(userdata['PlayCount'])) + + if itemtype == PLEX_TYPE_EPISODE: + window('%s.refreshid' % plexitem, value=self.getParentRatingKey()) + else: + window('%s.refreshid' % plexitem, value=self.getRatingKey()) + + # Append external subtitles to stream + playmethod = window('%s.playmethod' % plexitem) + if playmethod in ("DirectStream", "DirectPlay"): + subtitles = self.externalSubs(playurl) + listitem.setSubtitles(subtitles) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 25c4e7de..92dd5f1d 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- import logging -import threading -import traceback -import socket +from threading import Thread import Queue +from socket import SHUT_RDWR -import xbmc +from xbmc import sleep from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings -from PlexFunctions import ParseContainerKey, GetPlayQueue, \ - ConvertPlexToKodiTime -import playlist +from PlexFunctions import ParseContainerKey, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE import player +from entrypoint import Plex_Node ############################################################################### @@ -24,24 +22,23 @@ log = logging.getLogger("PLEX."+__name__) @ThreadMethodsAdditionalSuspend('plex_serverStatus') @ThreadMethods -class PlexCompanion(threading.Thread): +class PlexCompanion(Thread): """ - Initialize with a Queue for callbacks """ - def __init__(self): + def __init__(self, callback=None): log.info("----===## Starting PlexCompanion ##===----") + if callback is not None: + self.mgr = callback self.settings = plexsettings.getSettings() # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails(self.settings) log.debug("Registration string is: %s " % self.client.getClientDetails()) - # Initialize playlist/queue stuff - self.playlist = playlist.Playlist('video') # kodi player instance self.player = player.Player() - threading.Thread.__init__(self) + Thread.__init__(self) def _getStartItem(self, string): """ @@ -62,62 +59,48 @@ class PlexCompanion(threading.Thread): def processTasks(self, task): """ - Processes tasks picked up e.g. by Companion listener - - task = { - 'action': 'playlist' - 'data': as received from Plex companion - } + Processes tasks picked up e.g. by Companion listener, e.g. + {'action': 'playlist', + 'data': {'address': 'xyz.plex.direct', + 'commandID': '7', + 'containerKey': '/playQueues/6669?own=1&repeat=0&window=200', + 'key': '/library/metadata/220493', + 'machineIdentifier': 'xyz', + 'offset': '0', + 'port': '32400', + 'protocol': 'https', + 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd', + 'type': 'video'}} """ log.debug('Processing: %s' % task) data = task['data'] - if task['action'] == 'playlist': + if (task['action'] == 'playlist' and + data.get('address') == 'node.plexapp.com'): + # E.g. watch later initiated by Companion + thread = Thread(target=Plex_Node, + args=('{server}%s' % data.get('key'), + data.get('offset'), + data.get('type'), + True),) + thread.setDaemon(True) + thread.start() + elif task['action'] == 'playlist': + # Get the playqueue ID try: - _, queueId, query = ParseContainerKey(data['containerKey']) + _, ID, query = ParseContainerKey(data['containerKey']) except Exception as e: log.error('Exception while processing: %s' % e) import traceback log.error("Traceback:\n%s" % traceback.format_exc()) return - if self.playlist is not None: - if self.playlist.Typus() != data.get('type'): - log.debug('Switching to Kodi playlist of type %s' - % data.get('type')) - self.playlist = None - if self.playlist is None: - if data.get('type') == 'music': - self.playlist = playlist.Playlist('music') - else: - self.playlist = playlist.Playlist('video') - if queueId != self.playlist.QueueId(): - log.info('New playlist received, updating!') - xml = GetPlayQueue(queueId) - if xml in (None, 401): - log.error('Could not download Plex playlist.') - return - # Clear existing playlist on the Kodi side - self.playlist.clear() - # Set new values - self.playlist.QueueId(queueId) - self.playlist.PlayQueueVersion(int( - xml.attrib.get('playQueueVersion'))) - self.playlist.Guid(xml.attrib.get('guid')) - items = [] - for item in xml: - items.append({ - 'playQueueItemID': item.get('playQueueItemID'), - 'plexId': item.get('ratingKey'), - 'kodiId': None}) - self.playlist.playAll( - items, - startitem=self._getStartItem(data.get('key', '')), - offset=ConvertPlexToKodiTime(data.get('offset', 0))) - log.info('Initiated playlist no %s with version %s' - % (self.playlist.QueueId(), - self.playlist.PlayQueueVersion())) - else: - log.error('This has never happened before!') + playqueue = self.mgr.playqueue.get_playqueue_from_type( + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + ID, + repeat=query.get('repeat'), + offset=data.get('offset')) def run(self): httpd = False @@ -130,7 +113,7 @@ class PlexCompanion(threading.Thread): requestMgr = httppersist.RequestMgr() jsonClass = functions.jsonClass(requestMgr, self.settings) subscriptionManager = subscribers.SubscriptionManager( - jsonClass, requestMgr, self.player, self.playlist) + jsonClass, requestMgr, self.player, self.mgr) queue = Queue.Queue(maxsize=100) @@ -151,9 +134,10 @@ class PlexCompanion(threading.Thread): break except: log.error("Unable to start PlexCompanion. Traceback:") + import traceback log.error(traceback.print_exc()) - xbmc.sleep(3000) + sleep(3000) if start_count == 3: log.error("Error: Unable to start web helper.") @@ -168,7 +152,7 @@ class PlexCompanion(threading.Thread): message_count = 0 if httpd: - t = threading.Thread(target=httpd.handle_request) + t = Thread(target=httpd.handle_request) while not threadStopped(): # If we are not authorized, sleep @@ -177,13 +161,13 @@ class PlexCompanion(threading.Thread): while threadSuspended(): if threadStopped(): break - xbmc.sleep(1000) + sleep(1000) try: message_count += 1 if httpd: if not t.isAlive(): # Use threads cause the method will stall - t = threading.Thread(target=httpd.handle_request) + t = Thread(target=httpd.handle_request) t.start() if message_count == 3000: @@ -202,6 +186,7 @@ class PlexCompanion(threading.Thread): message_count = 0 except: log.warn("Error in loop, continuing anyway. Traceback:") + import traceback log.warn(traceback.format_exc()) # See if there's anything we need to process try: @@ -214,12 +199,12 @@ class PlexCompanion(threading.Thread): queue.task_done() # Don't sleep continue - xbmc.sleep(20) + sleep(20) client.stop_all() if httpd: try: - httpd.socket.shutdown(socket.SHUT_RDWR) + httpd.socket.shutdown(SHUT_RDWR) except: pass finally: diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 6f77546b..774f559b 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -20,46 +20,118 @@ addonName = 'PlexKodiConnect' # Multiply Plex time by this factor to receive Kodi time PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0 -# Possible output of Kodi's ListItem.DBTYPE for all video items + +# All the Plex types as communicated in the PMS xml replies +PLEX_TYPE_VIDEO = 'video' +PLEX_TYPE_MOVIE = 'movie' +PLEX_TYPE_CLIP = 'clip' # e.g. trailers + +PLEX_TYPE_EPISODE = 'episode' +PLEX_TYPE_SEASON = 'season' +PLEX_TYPE_SHOW = 'show' + +PLEX_TYPE_AUDIO = 'music' +PLEX_TYPE_SONG = 'track' +PLEX_TYPE_ALBUM = 'album' +PLEX_TYPE_ARTIST = 'artist' + +PLEX_TYPE_PHOTO = 'photo' + + +# All the Kodi types as e.g. used in the JSON API +KODI_TYPE_VIDEO = 'video' +KODI_TYPE_MOVIE = 'movie' +KODI_TYPE_SET = 'set' # for movie sets of several movies +KODI_TYPE_CLIP = 'clip' # e.g. trailers + +KODI_TYPE_EPISODE = 'episode' +KODI_TYPE_SEASON = 'season' +KODI_TYPE_SHOW = 'tvshow' + +KODI_TYPE_AUDIO = 'audio' +KODI_TYPE_SONG = 'song' +KODI_TYPE_ALBUM = 'album' +KODI_TYPE_ARTIST = 'artist' + +KODI_TYPE_PHOTO = 'photo' + + +# Translation tables + KODI_VIDEOTYPES = ( - 'video', - 'movie', - 'set', - 'tvshow', - 'season', - 'episode', - 'musicvideo' + KODI_TYPE_VIDEO, + KODI_TYPE_MOVIE, + KODI_TYPE_SHOW, + KODI_TYPE_SEASON, + KODI_TYPE_EPISODE, + KODI_TYPE_SET ) -# Possible output of Kodi's ListItem.DBTYPE for all audio items KODI_AUDIOTYPES = ( - 'music', - 'song', - 'album', - 'artist' + KODI_TYPE_SONG, + KODI_TYPE_ALBUM, + KODI_TYPE_ARTIST, ) ITEMTYPE_FROM_PLEXTYPE = { - 'movie': 'Movies', - 'season': 'TVShows', - 'episode': 'TVShows', - 'show': 'TVShows', - 'artist': 'Music', - 'album': 'Music', - 'track': 'Music', - 'song': 'Music' + PLEX_TYPE_MOVIE: 'Movies', + PLEX_TYPE_SEASON: 'TVShows', + KODI_TYPE_EPISODE: 'TVShows', + PLEX_TYPE_SHOW: 'TVShows', + PLEX_TYPE_ARTIST: 'Music', + PLEX_TYPE_ALBUM: 'Music', + PLEX_TYPE_SONG: 'Music', +} + +ITEMTYPE_FROM_KODITYPE = { + KODI_TYPE_MOVIE: 'Movies', + KODI_TYPE_SEASON: 'TVShows', + KODI_TYPE_EPISODE: 'TVShows', + KODI_TYPE_SHOW: 'TVShows', + KODI_TYPE_ARTIST: 'Music', + KODI_TYPE_ALBUM: 'Music', + KODI_TYPE_SONG: 'Music', } KODITYPE_FROM_PLEXTYPE = { - 'movie': 'movie', - 'episode': 'episode', - 'track': 'song', - 'artist': 'artist', - 'album': 'album', + PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE, + PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE, + PLEX_TYPE_SEASON: KODI_TYPE_SEASON, + PLEX_TYPE_SHOW: KODI_TYPE_SHOW, + PLEX_TYPE_SONG: KODI_TYPE_SONG, + PLEX_TYPE_ARTIST: KODI_TYPE_ARTIST, + PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM, + PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO, 'XXXXXX': 'musicvideo', 'XXXXXXX': 'genre' } +KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { + PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO, + PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO, + PLEX_TYPE_EPISODE: KODI_TYPE_VIDEO, + PLEX_TYPE_SEASON: KODI_TYPE_VIDEO, + PLEX_TYPE_SHOW: KODI_TYPE_VIDEO, + PLEX_TYPE_CLIP: KODI_TYPE_VIDEO, + PLEX_TYPE_ARTIST: KODI_TYPE_AUDIO, + PLEX_TYPE_ALBUM: KODI_TYPE_AUDIO, + PLEX_TYPE_SONG: KODI_TYPE_AUDIO, + PLEX_TYPE_AUDIO: KODI_TYPE_AUDIO +} + + +REMAP_TYPE_FROM_PLEXTYPE = { + PLEX_TYPE_MOVIE: 'movie', + PLEX_TYPE_CLIP: 'clip', + PLEX_TYPE_SHOW: 'tv', + PLEX_TYPE_SEASON: 'tv', + PLEX_TYPE_EPISODE: 'tv', + PLEX_TYPE_ARTIST: 'music', + PLEX_TYPE_ALBUM: 'music', + PLEX_TYPE_SONG: 'music', + PLEX_TYPE_PHOTO: 'photo' +} + REMAP_TYPE_FROM_PLEXTYPE = { 'movie': 'movie', @@ -159,22 +231,6 @@ def SelectStreams(url, args): url + '?' + urlencode(args), action_type='PUT') -def GetPlayQueue(playQueueID): - """ - Fetches the PMS playqueue with the playQueueID as an XML - - Returns None if something went wrong - """ - url = "{server}/playQueues/%s" % playQueueID - args = {'Accept': 'application/xml'} - xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args) - try: - xml.attrib['playQueueID'] - except (AttributeError, KeyError): - return None - return xml - - def GetPlexMetadata(key): """ Returns raw API metadata for key as an etree XML. @@ -388,23 +444,22 @@ def GetPlexCollections(mediatype): return collections -def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'): +def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie', + trailers=False): """ Returns raw API metadata XML dump for a playlist with e.g. trailers. """ - trailerNumber = settings('trailerNumber') - if not trailerNumber: - trailerNumber = '3' url = "{server}/playQueues" args = { 'type': mediatype, 'uri': ('library://' + librarySectionUUID + '/item/%2Flibrary%2Fmetadata%2F' + itemid), 'includeChapters': '1', - 'extrasPrefixCount': trailerNumber, 'shuffle': '0', 'repeat': '0' } + if trailers is True: + args['extrasPrefixCount'] = settings('trailerNumber') xml = downloadutils.DownloadUtils().downloadUrl( url + '?' + urlencode(args), action_type="POST") try: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index db7aa97e..08105837 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -281,7 +281,7 @@ class Artwork(): def cacheTexture(self, url): # Cache a single image url to the texture cache if url and self.enableTextureCache: - self.queue.put(double_urlencode(url)) + self.queue.put(double_urlencode(tryEncode(url))) def addArtwork(self, artwork, kodiId, mediaType, cursor): # Kodi conversion table diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 5788ca00..1bb9bdd9 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -49,7 +49,7 @@ class ClientInfo(): 'X-Plex-Product': self.getAddonName(), 'X-Plex-Version': self.getVersion(), 'X-Plex-Client-Identifier': self.getDeviceId(), - 'X-Plex-Provides': 'player', + 'X-Plex-Provides': 'client,controller,player', } if window('pms_token'): diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index d5996b9e..382d21ab 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -8,7 +8,7 @@ import xbmc import xbmcaddon import PlexFunctions as PF -import embydb_functions as embydb +import plexdb_functions as plexdb from utils import window, settings, dialog, language as lang, kodiSQL from dialogs import context @@ -75,8 +75,8 @@ class ContextMenu(object): def _get_item_id(cls, kodi_id, item_type): item_id = xbmc.getInfoLabel('ListItem.Property(plexid)') if not item_id and kodi_id and item_type: - with embydb.GetEmbyDB() as emby_db: - item = emby_db.getItem_byKodiId(kodi_id, item_type) + with plexdb.Get_Plex_DB() as plexcursor: + item = plexcursor.getItem_byKodiId(kodi_id, item_type) try: item_id = item[0] except TypeError: @@ -140,8 +140,8 @@ class ContextMenu(object): elif selected == OPTIONS['PMS_Play']: self._PMS_play() - elif selected == OPTIONS['Refresh']: - self.emby.refreshItem(self.item_id) + # elif selected == OPTIONS['Refresh']: + # self.emby.refreshItem(self.item_id) # elif selected == OPTIONS['AddFav']: # self.emby.updateUserRating(self.item_id, favourite=True) @@ -212,6 +212,6 @@ class ContextMenu(object): 'mode': "play" } from urllib import urlencode - handle = ("plugin://plugin.video.plexkodiconnect.movies?%s" + handle = ("plugin://plugin.video.plexkodiconnect/movies?%s" % urlencode(params)) xbmc.executebuiltin('RunPlugin(%s)' % handle) diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py index 770b0a2c..ed535c98 100644 --- a/resources/lib/dialogs/usersconnect.py +++ b/resources/lib/dialogs/usersconnect.py @@ -67,7 +67,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): if self.kodi_version > 15: item.setArt({'Icon': user_image}) else: - item.setIconImage(user_image) + item.setArt({'icon': user_image}) return item diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py deleted file mode 100644 index 3f173e70..00000000 --- a/resources/lib/embydb_functions.py +++ /dev/null @@ -1,402 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################### - -from utils import kodiSQL -import logging - -############################################################################### - -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -class GetEmbyDB(): - """ - Usage: with GetEmbyDB() as emby_db: - do stuff with emby_db - - On exiting "with" (no matter what), commits get automatically committed - and the db gets closed - """ - def __enter__(self): - self.embyconn = kodiSQL('emby') - self.emby_db = Embydb_Functions(self.embyconn.cursor()) - return self.emby_db - - def __exit__(self, type, value, traceback): - self.embyconn.commit() - self.embyconn.close() - - -class Embydb_Functions(): - - def __init__(self, embycursor): - - self.embycursor = embycursor - - def getViews(self): - - views = [] - - query = ' '.join(( - - "SELECT view_id", - "FROM view" - )) - self.embycursor.execute(query) - rows = self.embycursor.fetchall() - for row in rows: - views.append(row[0]) - return views - - def getAllViewInfo(self): - - embycursor = self.embycursor - views = [] - - query = ' '.join(( - - "SELECT view_id, view_name, media_type", - "FROM view" - )) - embycursor.execute(query) - rows = embycursor.fetchall() - for row in rows: - views.append({'id': row[0], - 'name': row[1], - 'itemtype': row[2]}) - return views - - def getView_byId(self, viewid): - - - query = ' '.join(( - - "SELECT view_name, media_type, kodi_tagid", - "FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (viewid,)) - view = self.embycursor.fetchone() - - return view - - def getView_byType(self, mediatype): - - views = [] - - query = ' '.join(( - - "SELECT view_id, view_name, media_type", - "FROM view", - "WHERE media_type = ?" - )) - self.embycursor.execute(query, (mediatype,)) - rows = self.embycursor.fetchall() - for row in rows: - views.append({ - - 'id': row[0], - 'name': row[1], - 'itemtype': row[2] - }) - - return views - - def getView_byName(self, tagname): - - query = ' '.join(( - - "SELECT view_id", - "FROM view", - "WHERE view_name = ?" - )) - self.embycursor.execute(query, (tagname,)) - try: - view = self.embycursor.fetchone()[0] - - except TypeError: - view = None - - return view - - def addView(self, plexid, name, mediatype, tagid): - - query = ( - ''' - INSERT INTO view( - view_id, view_name, media_type, kodi_tagid) - - VALUES (?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (plexid, name, mediatype, tagid)) - - def updateView(self, name, tagid, mediafolderid): - - query = ' '.join(( - - "UPDATE view", - "SET view_name = ?, kodi_tagid = ?", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (name, tagid, mediafolderid)) - - def removeView(self, viewid): - - query = ' '.join(( - - "DELETE FROM view", - "WHERE view_id = ?" - )) - self.embycursor.execute(query, (viewid,)) - - def getItem_byFileId(self, fileId, kodiType): - """ - Returns the Plex itemId by using the Kodi fileId. VIDEO ONLY - - kodiType: 'movie', 'episode', ... - """ - query = ' '.join(( - "SELECT emby_id", - "FROM emby", - "WHERE kodi_fileid = ? AND media_type = ?" - )) - try: - self.embycursor.execute(query, (fileId, kodiType)) - item = self.embycursor.fetchone()[0] - return item - except: - return None - - def getMusicItem_byFileId(self, fileId, kodiType): - """ - Returns the Plex itemId by using the Kodi fileId. MUSIC ONLY - - kodiType: 'song' - """ - query = ' '.join(( - "SELECT emby_id", - "FROM emby", - "WHERE kodi_id = ? AND media_type = ?" - )) - try: - self.embycursor.execute(query, (fileId, kodiType)) - item = self.embycursor.fetchone()[0] - return item - except: - return None - - def getItem_byId(self, plexid): - - query = ' '.join(( - - "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - try: - self.embycursor.execute(query, (plexid,)) - item = self.embycursor.fetchone() - return item - except: return None - - def getItem_byWildId(self, plexid): - - query = ' '.join(( - - "SELECT kodi_id, media_type", - "FROM emby", - "WHERE emby_id LIKE ?" - )) - self.embycursor.execute(query, (plexid+"%",)) - return self.embycursor.fetchall() - - def getItem_byView(self, mediafolderid): - - query = ' '.join(( - - "SELECT kodi_id", - "FROM emby", - "WHERE media_folder = ?" - )) - self.embycursor.execute(query, (mediafolderid,)) - return self.embycursor.fetchall() - - def getPlexId(self, kodiid, mediatype): - """ - Returns the Plex ID usind the Kodiid. Result: - (Plex Id, Parent's Plex Id) - """ - query = ' '.join(( - "SELECT emby_id, parent_id", - "FROM emby", - "WHERE kodi_id = ? AND media_type = ?" - )) - try: - self.embycursor.execute(query, (kodiid, mediatype)) - item = self.embycursor.fetchone() - return item - except: - return None - - def getItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, parent_id", - "FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (kodiid, mediatype,)) - return self.embycursor.fetchone() - - def getItem_byParentId(self, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id, kodi_fileid", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() - - def getItemId_byParentId(self, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() - - def getChecksum(self, mediatype): - - query = ' '.join(( - - "SELECT emby_id, checksum", - "FROM emby", - "WHERE emby_type = ?" - )) - self.embycursor.execute(query, (mediatype,)) - return self.embycursor.fetchall() - - def getMediaType_byId(self, plexid): - - query = ' '.join(( - - "SELECT emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - self.embycursor.execute(query, (plexid,)) - try: - itemtype = self.embycursor.fetchone()[0] - - except TypeError: - itemtype = None - - return itemtype - - def sortby_mediaType(self, itemids, unsorted=True): - - sorted_items = {} - - for itemid in itemids: - mediatype = self.getMediaType_byId(itemid) - if mediatype: - sorted_items.setdefault(mediatype, []).append(itemid) - elif unsorted: - sorted_items.setdefault('Unsorted', []).append(itemid) - - return sorted_items - - def addReference(self, plexid, kodiid, embytype, mediatype, fileid=None, pathid=None, - parentid=None, checksum=None, mediafolderid=None): - query = ( - ''' - INSERT OR REPLACE INTO emby( - emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id, - checksum, media_folder) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype, - parentid, checksum, mediafolderid)) - - def updateReference(self, plexid, checksum): - - query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" - self.embycursor.execute(query, (checksum, plexid)) - - def updateParentId(self, plexid, parent_kodiid): - - query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?" - self.embycursor.execute(query, (parent_kodiid, plexid)) - - def removeItems_byParentId(self, parent_kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (parent_kodiid, mediatype,)) - - def removeItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.embycursor.execute(query, (kodiid, mediatype,)) - - def removeItem(self, plexid): - - query = "DELETE FROM emby WHERE emby_id = ?" - self.embycursor.execute(query, (plexid,)) - - def removeWildItem(self, plexid): - - query = "DELETE FROM emby WHERE emby_id LIKE ?" - self.embycursor.execute(query, (plexid+"%",)) - - def itemsByType(self, plextype): - """ - Returns a list of dictionaries for all Kodi DB items present for - plextype. One dict is of the type - - { - 'plexId': the Plex id - 'kodiId': the Kodi id - 'kodi_type': e.g. 'movie', 'tvshow' - 'plex_type': e.g. 'Movie', 'Series', the input plextype - } - """ - query = ' '.join(( - "SELECT emby_id, kodi_id, media_type", - "FROM emby", - "WHERE emby_type = ?", - )) - self.embycursor.execute(query, (plextype, )) - result = [] - for row in self.embycursor.fetchall(): - result.append({ - 'plexId': row[0], - 'kodiId': row[1], - 'kodi_type': row[2], - 'plex_type': plextype - }) - return result diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 75fcd2d5..eca0ac3c 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -17,12 +17,13 @@ from utils import window, settings, language as lang from utils import tryDecode, tryEncode, CatchExceptions import clientinfo import downloadutils -import embydb_functions as embydb +import plexdb_functions as plexdb import playbackutils as pbutils -import playlist import PlexFunctions import PlexAPI +from PKC_listitem import convert_PKC_to_listitem +from playqueue import Playqueue ############################################################################### @@ -33,40 +34,6 @@ addonName = "PlexKodiConnect" ############################################################################### -def plexCompanion(fullurl, params): - params = PlexFunctions.LiteralEval(params[26:]) - - if params['machineIdentifier'] != window('plex_machineIdentifier'): - log.error("Command was not for us, machineIdentifier controller: %s, " - "our machineIdentifier : %s" - % (params['machineIdentifier'], - window('plex_machineIdentifier'))) - return - - library, key, query = PlexFunctions.ParseContainerKey( - params['containerKey']) - # Construct a container key that works always (get rid of playlist args) - window('containerKey', '/'+library+'/'+key) - - if 'playQueues' in library: - log.debug("Playing a playQueue. Query was: %s" % query) - # Playing a playlist that we need to fetch from PMS - xml = PlexFunctions.GetPlayQueue(key) - if xml is None: - log.error("Error getting PMS playlist for key %s" % key) - return - else: - resume = PlexFunctions.ConvertPlexToKodiTime( - params.get('offset', 0)) - itemids = [] - for item in xml: - itemids.append(item.get('ratingKey')) - return playlist.Playlist().playAll(itemids, resume) - - else: - log.error("Not knowing what to do for now - no playQueue sent") - - def chooseServer(): """ Lets user choose from list of PMS @@ -130,45 +97,21 @@ def togglePlexTV(): sound=False) -def PassPlaylist(xml, resume=None): - """ - resume in KodiTime - seconds. - """ - # Set window properties to make them available later for other threads - windowArgs = [ - # 'containerKey' - 'playQueueID', - 'playQueueVersion'] - for arg in windowArgs: - window(arg, value=xml.attrib.get(arg)) - - # Get resume point - from utils import IntFromStr - resume1 = PlexFunctions.ConvertPlexToKodiTime(IntFromStr( - xml.attrib.get('playQueueSelectedItemOffset', 0))) - resume2 = resume - resume = max(resume1, resume2) - - pbutils.PlaybackUtils(xml).StartPlay( - resume=resume, - resumeId=xml.attrib.get('playQueueSelectedItemID', None)) - - -def playWatchLater(itemid, viewOffset): +def Plex_Node(url, viewOffset, plex_type, playdirectly=False): """ Called only for a SINGLE element for Plex.tv watch later Always to return with a "setResolvedUrl" """ - log.info('playWatchLater called with id: %s, viewOffset: %s' - % (itemid, viewOffset)) + log.info('Plex_Node called with url: %s, viewOffset: %s' + % (url, viewOffset)) # Plex redirect, e.g. watch later. Need to get actual URLs - xml = downloadutils.DownloadUtils().downloadUrl(itemid, - authenticate=False) - if xml in (None, 401): - log.error("Could not resolve url %s" % itemid) - return xbmcplugin.setResolvedUrl( - int(sys.argv[1]), False, xbmcgui.ListItem()) + xml = downloadutils.DownloadUtils().downloadUrl(url) + try: + xml[0].attrib + except: + log.error('Could not download PMS metadata') + return if viewOffset != '0': try: viewOffset = int(PlexFunctions.PLEX_TO_KODI_TIMEFACTOR * @@ -178,41 +121,20 @@ def playWatchLater(itemid, viewOffset): else: window('plex_customplaylist.seektime', value=str(viewOffset)) log.info('Set resume point to %s' % str(viewOffset)) - return pbutils.PlaybackUtils(xml).play(None, 'plexnode') - - -def doPlayback(itemid, dbid): - """ - Called only for a SINGLE element, not playQueues - - Always to return with a "setResolvedUrl" - """ - if window('plex_authenticated') != "true": - log.error('Not yet authenticated for a PMS, abort starting playback') - # Not yet connected to a PMS server - xbmcgui.Dialog().notification( - addonName, - lang(39210), - xbmcgui.NOTIFICATION_ERROR, - 7000, - True) - return xbmcplugin.setResolvedUrl( - int(sys.argv[1]), False, xbmcgui.ListItem()) - - xml = PlexFunctions.GetPlexMetadata(itemid) - if xml in (None, 401): - return xbmcplugin.setResolvedUrl( - int(sys.argv[1]), False, xbmcgui.ListItem()) - if xml[0].attrib.get('type') == 'photo': - # Photo - API = PlexAPI.API(xml[0]) - listitem = API.CreateListItemFromPlexItem() - API.AddStreamInfo(listitem) - pbutils.PlaybackUtils(xml[0]).setArtwork(listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type] + playqueue = Playqueue().get_playqueue_from_type(typus) + result = pbutils.PlaybackUtils(xml, playqueue).play( + None, + kodi_id='plexnode', + plex_lib_UUID=xml.attrib.get('librarySectionUUID')) + if result.listitem: + listitem = convert_PKC_to_listitem(result.listitem) else: - # Video - return pbutils.PlaybackUtils(xml).play(itemid, dbid) + return + if playdirectly: + xbmc.Player().play(listitem.getfilename(), listitem) + else: + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) ##### DO RESET AUTH ##### @@ -319,12 +241,8 @@ def deleteItem(): log.error("Unknown type, unable to proceed.") return - from utils import kodiSQL - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(dbid, itemtype) - embycursor.close() + with plexdb.Get_Plex_DB() as plexcursor: + item = plexcursor.getItem_byKodiId(dbid, itemtype) try: plexid = item[0] @@ -467,99 +385,6 @@ def BrowseContent(viewname, browse_type="", folderid=""): xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) -##### CREATE LISTITEM FROM EMBY METADATA ##### -# def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()): -def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils()): - API = PlexAPI.API(item) - itemid = item['Id'] - - title = item.get('Name') - li = xbmcgui.ListItem(title) - - premieredate = item.get('PremiereDate',"") - if not premieredate: premieredate = item.get('DateCreated',"") - if premieredate: - premieredatelst = premieredate.split('T')[0].split("-") - premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0]) - - li.setProperty("plexid",itemid) - - allart = art.getAllArtwork(item) - - if item["Type"] == "Photo": - #listitem setup for pictures... - img_path = allart.get('Primary') - li.setProperty("path",img_path) - picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid) - if picture: - picture = picture[0] - if picture.get("Width") > picture.get("Height"): - li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title}) - li.setThumbnailImage(img_path) - li.setProperty("plot",API.getOverview()) - li.setIconImage('DefaultPicture.png') - else: - #normal video items - li.setProperty('IsPlayable', 'true') - path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id")) - li.setProperty("path",path) - genre = API.getGenres() - overlay = 0 - userdata = API.getUserData() - runtime = item.get("RunTimeTicks",0)/ 10000000.0 - seektime = userdata['Resume'] - if seektime: - li.setProperty("resumetime", str(seektime)) - li.setProperty("totaltime", str(runtime)) - - played = userdata['Played'] - if played: overlay = 7 - else: overlay = 6 - playcount = userdata['PlayCount'] - if playcount is None: - playcount = 0 - - rating = item.get('CommunityRating') - if not rating: rating = userdata['UserRating'] - - # Populate the extradata list and artwork - extradata = { - 'id': itemid, - 'rating': rating, - 'year': item.get('ProductionYear'), - 'genre': genre, - 'playcount': str(playcount), - 'title': title, - 'plot': API.getOverview(), - 'Overlay': str(overlay), - 'duration': runtime - } - if premieredate: - extradata["premieredate"] = premieredate - extradata["date"] = premieredate - li.setInfo('video', infoLabels=extradata) - if allart.get('Primary'): - li.setThumbnailImage(allart.get('Primary')) - else: li.setThumbnailImage('DefaultTVShows.png') - li.setIconImage('DefaultTVShows.png') - if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation - li.setArt( {"fanart": allart.get('Primary') } ) - else: - pbutils.PlaybackUtils(item).setArtwork(li) - - mediastreams = API.getMediaStreams() - videostreamFound = False - if mediastreams: - for key, value in mediastreams.iteritems(): - if key == "video" and value: videostreamFound = True - if value: li.addStreamInfo(key, value[0]) - if not videostreamFound: - #just set empty streamdetails to prevent errors in the logs - li.addStreamInfo("video", {'duration': runtime}) - - return li - ##### BROWSE EMBY CHANNELS ##### def BrowseChannels(itemid, folderid=None): @@ -664,7 +489,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False): li.setProperty('totaltime', str(item['resume']['total'])) li.setArt(item['art']) li.setThumbnailImage(item['art'].get('thumb','')) - li.setIconImage('DefaultTVShows.png') + li.setArt({'icon': 'DefaultTVShows.png'}) li.setProperty('dbid', str(item['episodeid'])) li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) for key, value in item['streamdetails'].iteritems(): @@ -1095,14 +920,14 @@ def BrowsePlexContent(viewid, mediatype="", folderid=""): li.setProperty('IsPlayable', 'false') path = "%s?id=%s&mode=browseplex&type=%s&folderid=%s" \ % (sys.argv[0], viewid, mediatype, API.getKey()) - pbutils.PlaybackUtils(item).setArtwork(li) + API.set_listitem_artwork(li) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True) else: li = API.CreateListItemFromPlexItem() - pbutils.PlaybackUtils(item).setArtwork(li) + API.set_listitem_artwork(li) xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), url=li.getProperty("path"), @@ -1159,7 +984,7 @@ def getOnDeck(viewid, mediatype, tagname, limit): appendShowTitle=appendShowTitle, appendSxxExx=appendSxxExx) API.AddStreamInfo(listitem) - pbutils.PlaybackUtils(item).setArtwork(listitem) + API.set_listitem_artwork(listitem) if directpaths: url = API.getFilePath() else: @@ -1168,7 +993,7 @@ def getOnDeck(viewid, mediatype, tagname, limit): 'id': API.getRatingKey(), 'dbid': listitem.getProperty('dbid') } - url = "plugin://plugin.video.plexkodiconnect.tvshows/?%s" \ + url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ % urllib.urlencode(params) xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), @@ -1306,15 +1131,16 @@ def watchlater(): xbmcplugin.setContent(int(sys.argv[1]), 'movies') url = "plugin://plugin.video.plexkodiconnect/" params = { - 'mode': "playwatchlater", + 'mode': "Plex_Node", } for item in xml: API = PlexAPI.API(item) listitem = API.CreateListItemFromPlexItem() API.AddStreamInfo(listitem) - pbutils.PlaybackUtils(item).setArtwork(listitem) + API.set_listitem_artwork(listitem) params['id'] = item.attrib.get('key') params['viewOffset'] = item.attrib.get('viewOffset', '0') + params['plex_type'] = item.attrib.get('type') xbmcplugin.addDirectoryItem( handle=int(sys.argv[1]), url="%s?%s" % (url, urllib.urlencode(params)), diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index a895ec8c..4724be97 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -9,7 +9,7 @@ import xbmcgui from utils import settings, window, language as lang import clientinfo import downloadutils -import userclient +from userclient import UserClient import PlexAPI from PlexFunctions import GetMachineIdentifier, get_PMS_settings @@ -30,11 +30,10 @@ class InitialSetup(): self.clientInfo = clientinfo.ClientInfo() self.addonId = self.clientInfo.getAddonId() self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.userClient = userclient.UserClient() self.plx = PlexAPI.PlexAPI() self.dialog = xbmcgui.Dialog() - self.server = self.userClient.getServer() + self.server = UserClient().getServer() self.serverid = settings('plex_machineIdentifier') # Get Plex credentials from settings file, if they exist plexdict = self.plx.GetPlexLoginFromSettings() diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 0ddc8351..5fbbd6ca 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -7,16 +7,14 @@ from urllib import urlencode from ntpath import dirname from datetime import datetime -import xbmcgui - import artwork from utils import tryEncode, tryDecode, settings, window, kodiSQL, \ CatchExceptions, KODIVERSION -import embydb_functions as embydb +import plexdb_functions as plexdb import kodidb_functions as kodidb import PlexAPI -from PlexFunctions import GetPlexMetadata +import PlexFunctions as PF ############################################################################### @@ -45,11 +43,11 @@ class Items(object): """ Open DB connections and cursors """ - self.embyconn = kodiSQL('emby') - self.embycursor = self.embyconn.cursor() + self.plexconn = kodiSQL('plex') + self.plexcursor = self.plexconn.cursor() self.kodiconn = kodiSQL('video') self.kodicursor = self.kodiconn.cursor() - self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) return self @@ -57,9 +55,9 @@ class Items(object): """ Make sure DB changes are committed and connection to DB is closed. """ - self.embyconn.commit() + self.plexconn.commit() self.kodiconn.commit() - self.embyconn.close() + self.plexconn.close() self.kodiconn.close() return self @@ -75,180 +73,16 @@ class Items(object): mediaType, self.kodicursor) # Also get artwork for collections/movie sets - if mediaType == 'movie': + if mediaType == PF.KODI_TYPE_MOVIE: for setname in API.getCollections(): log.debug('Getting artwork for movie set %s' % setname) setid = self.kodi_db.createBoxset(setname) self.artwork.addArtwork(API.getSetArtwork(), setid, - "set", + PF.KODI_TYPE_SET, self.kodicursor) self.kodi_db.assignBoxset(setid, kodiId) - def itemsbyId(self, items, process, pdialog=None): - # Process items by itemid. Process can be added, update, userdata, remove - embycursor = self.embycursor - kodicursor = self.kodicursor - music_enabled = self.music_enabled - - itemtypes = { - - 'Movie': Movies, - 'BoxSet': Movies, - 'Series': TVShows, - 'Season': TVShows, - 'Episode': TVShows, - 'MusicAlbum': Music, - 'MusicArtist': Music, - 'AlbumArtist': Music, - 'Audio': Music - } - - update_videolibrary = False - total = 0 - for item in items: - total += len(items[item]) - - if total == 0: - return False - - log.info("Processing %s: %s" % (process, items)) - if pdialog: - pdialog.update(heading="Processing %s: %s items" % (process, total)) - - count = 0 - for itemtype in items: - - # Safety check - if not itemtypes.get(itemtype): - # We don't process this type of item - continue - - itemlist = items[itemtype] - if not itemlist: - # The list to process is empty - continue - - musicconn = None - - if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): - if music_enabled: - musicconn = kodiSQL('music') - musiccursor = musicconn.cursor() - items_process = itemtypes[itemtype](embycursor, musiccursor) - else: - # Music is not enabled, do not proceed with itemtype - continue - else: - update_videolibrary = True - items_process = itemtypes[itemtype](embycursor, kodicursor) - - if itemtype == "Movie": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "BoxSet": - actions = { - 'added': items_process.added_boxset, - 'update': items_process.add_updateBoxset, - 'remove': items_process.remove - } - elif itemtype == "MusicVideo": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Series": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Season": - actions = { - 'added': items_process.added_season, - 'update': items_process.add_updateSeason, - 'remove': items_process.remove - } - elif itemtype == "Episode": - actions = { - 'added': items_process.added_episode, - 'update': items_process.add_updateEpisode, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "MusicAlbum": - actions = { - 'added': items_process.added_album, - 'update': items_process.add_updateAlbum, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype in ("MusicArtist", "AlbumArtist"): - actions = { - 'added': items_process.added, - 'update': items_process.add_updateArtist, - 'remove': items_process.remove - } - elif itemtype == "Audio": - actions = { - 'added': items_process.added_song, - 'update': items_process.add_updateSong, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - else: - log.info("Unsupported itemtype: %s." % itemtype) - actions = {} - - if actions.get(process): - - if process == "remove": - for item in itemlist: - actions[process](item) - - elif process == "added": - actions[process](itemlist, pdialog) - - else: - processItems = emby.getFullItems(itemlist) - for item in processItems: - - title = item['Name'] - - if itemtype == "Episode": - title = "%s - %s" % (item['SeriesName'], title) - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - actions[process](item) - - - if musicconn is not None: - # close connection for special types - log.info("Updating music database.") - musicconn.commit() - musiccursor.close() - - return (True, update_videolibrary) - - def contentPop(self, name, time=5000): - xbmcgui.Dialog().notification( - heading="Emby for Kodi", - message="Added: %s" % name, - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - time=time, - sound=False) - def updateUserdata(self, xml, viewtag=None, viewid=None): """ Updates the Kodi watched state of the item from PMS. Also retrieves @@ -260,7 +94,7 @@ class Items(object): API = PlexAPI.API(mediaitem) # Get key and db entry on the Kodi db side try: - fileid = self.emby_db.getItem_byId(API.getRatingKey())[1] + fileid = self.plex_db.getItem_byId(API.getRatingKey())[1] except: continue # Grab the user's viewcount, resume points etc. from PMS' answer @@ -305,7 +139,7 @@ class Movies(Items): def add_update(self, item, viewtag=None, viewid=None): # Process single movie kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) @@ -318,11 +152,11 @@ class Movies(Items): if not itemid: log.error("Cannot parse XML data for movie") return - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] + movieid = plex_dbitem[0] + fileid = plex_dbitem[1] + pathid = plex_dbitem[2] except TypeError: # movieid @@ -407,7 +241,7 @@ class Movies(Items): path = playurl.replace(filename, "") if doIndirect: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.plexkodiconnect/movies/" params = { 'filename': API.getKey(), 'id': itemid, @@ -433,7 +267,23 @@ class Movies(Items): % (itemid, title)) # Update the movie entry - if KODIVERSION > 16: + if KODIVERSION >= 17: + # update new ratings Kodi 17 + ratingid = self.kodi_db.get_ratingid(movieid) + self.kodi_db.update_ratings(movieid, + PF.KODI_TYPE_MOVIE, + "default", + rating, + votecount, + ratingid) + # update new uniqueid Kodi 17 + uniqueid = self.kodi_db.get_uniqueid(movieid) + self.kodi_db.update_uniqueid(movieid, + PF.KODI_TYPE_MOVIE, + imdb, + "imdb", + uniqueid) + query = ' '.join(( "UPDATE movie", "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?," @@ -464,7 +314,23 @@ class Movies(Items): ##### OR ADD THE MOVIE ##### else: log.info("ADD movie itemid: %s - Title: %s" % (itemid, title)) - if KODIVERSION > 16: + if KODIVERSION >= 17: + # add new ratings Kodi 17 + ratingid = self.kodi_db.create_entry_rating() + self.kodi_db.add_ratings(ratingid, + movieid, + PF.KODI_TYPE_MOVIE, + "default", + rating, + votecount) + # add new uniqueid Kodi 17 + uniqueid = self.kodi_db.create_entry_uniqueid() + self.kodi_db.add_uniqueid(uniqueid, + movieid, + PF.KODI_TYPE_MOVIE, + imdb, + "imdb") + query = ( ''' INSERT INTO movie( idMovie, idFile, c00, c01, c02, c03, @@ -495,11 +361,18 @@ class Movies(Items): sorttitle, runtime, mpaa, genre, director, title, studio, trailer, country, playurl, pathid)) - # Create or update the reference in emby table Add reference is + # Create or update the reference in plex table Add reference is # idempotent; the call here updates also fileid and pathid when item is # moved or renamed - emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, - None, checksum, viewid) + plex_db.addReference(itemid, + PF.PLEX_TYPE_MOVIE, + movieid, + PF.KODI_TYPE_MOVIE, + kodi_fileid=fileid, + kodi_pathid=pathid, + parent_id=None, + checksum=checksum, + view_id=viewid) # Update the path query = ' '.join(( @@ -543,23 +416,23 @@ class Movies(Items): self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) def remove(self, itemid): - # Remove movieid, fileid, emby reference - emby_db = self.emby_db + # Remove movieid, fileid, plex reference + plex_db = self.plex_db kodicursor = self.kodicursor artwork = self.artwork - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] + kodiid = plex_dbitem[0] + fileid = plex_dbitem[1] + mediatype = plex_dbitem[4] log.info("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid)) except TypeError: return - # Remove the emby reference - emby_db.removeItem(itemid) + # Remove the plex reference + plex_db.removeItem(itemid) # Remove artwork artwork.deleteArtwork(kodiid, mediatype, kodicursor) @@ -570,13 +443,13 @@ class Movies(Items): elif mediatype == "set": # Delete kodi boxset - boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") + boxset_movies = plex_db.getItem_byParentId(kodiid, "movie") for movie in boxset_movies: plexid = movie[0] movieid = movie[1] self.kodi_db.removefromBoxset(movieid) - # Update emby reference - emby_db.updateParentId(plexid, None) + # Update plex reference + plex_db.updateParentId(plexid, None) kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,)) @@ -590,7 +463,7 @@ class TVShows(Items): def add_update(self, item, viewtag=None, viewid=None): # Process single tvshow kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) @@ -604,10 +477,10 @@ class TVShows(Items): # If the item doesn't exist, we'll add it to the database update_item = True force_episodes = False - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - showid = emby_dbitem[0] - pathid = emby_dbitem[2] + showid = plex_dbitem[0] + pathid = plex_dbitem[2] except TypeError: update_item = False kodicursor.execute("select coalesce(max(idShow),0) from tvshow") @@ -627,11 +500,6 @@ class TVShows(Items): # Force re-add episodes after the show is re-created. force_episodes = True - if viewtag is None or viewid is None: - # Get view tag from emby - viewtag, viewid, mediatype = embyserver.getView_plexid(itemid) - log.debug("View tag found: %s" % viewtag) - # fileId information checksum = API.getChecksum() @@ -675,7 +543,7 @@ class TVShows(Items): toplevelpath = "%s/" % dirname(dirname(path)) if doIndirect: # Set plugin path - toplevelpath = "plugin://plugin.video.plexkodiconnect.tvshows/" + toplevelpath = "plugin://plugin.video.plexkodiconnect/tvshows/" path = "%s%s/" % (toplevelpath, itemid) # Add top path @@ -687,7 +555,22 @@ class TVShows(Items): if update_item: log.info("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title)) - + if KODIVERSION >= 17: + # update new ratings Kodi 17 + ratingid = self.kodi_db.get_ratingid(showid) + self.kodi_db.update_ratings(showid, + PF.KODI_TYPE_SHOW, + "default", + rating, + None, # votecount + ratingid) + # update new uniqueid Kodi 17 + uniqueid = self.kodi_db.get_uniqueid(showid) + self.kodi_db.update_uniqueid(showid, + PF.KODI_TYPE_SHOW, + tvdb, + "tvdb", + uniqueid) # Update the tvshow entry query = ' '.join(( @@ -701,18 +584,33 @@ class TVShows(Items): # Add reference is idempotent; the call here updates also fileid # and pathid when item is moved or renamed - emby_db.addReference(itemid, + plex_db.addReference(itemid, + PF.PLEX_TYPE_SHOW, showid, - "Series", - "tvshow", - pathid=pathid, + PF.KODI_TYPE_SHOW, + kodi_pathid=pathid, checksum=checksum, - mediafolderid=viewid) + view_id=viewid) ##### OR ADD THE TVSHOW ##### else: log.info("ADD tvshow itemid: %s - Title: %s" % (itemid, title)) - + if KODIVERSION >= 17: + # add new ratings Kodi 17 + ratingid = self.kodi_db.create_entry_rating() + self.kodi_db.add_ratings(ratingid, + showid, + PF.KODI_TYPE_SHOW, + "default", + rating, + None) # votecount + # add new uniqueid Kodi 17 + uniqueid = self.kodi_db.create_entry_uniqueid() + self.kodi_db.add_uniqueid(uniqueid, + showid, + PF.KODI_TYPE_SHOW, + tvdb, + "tvdb") query = ' '.join(( "UPDATE path", @@ -737,9 +635,14 @@ class TVShows(Items): query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)" kodicursor.execute(query, (showid, pathid)) - # Create the reference in emby table - emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) + # Create the reference in plex table + plex_db.addReference(itemid, + PF.PLEX_TYPE_SHOW, + showid, + PF.KODI_TYPE_SHOW, + kodi_pathid=pathid, + checksum=checksum, + view_id=viewid) # Update the path query = ' '.join(( @@ -765,41 +668,41 @@ class TVShows(Items): tags.extend(collections) self.kodi_db.addTags(showid, tags, "tvshow") - if force_episodes: - # We needed to recreate the show entry. Re-add episodes now. - log.info("Repairing episodes for showid: %s %s" % (showid, title)) - all_episodes = embyserver.getEpisodesbyShow(itemid) - self.added_episode(all_episodes['Items'], None) + # if force_episodes: + # # We needed to recreate the show entry. Re-add episodes now. + # log.info("Repairing episodes for showid: %s %s" % (showid, title)) + # all_episodes = embyserver.getEpisodesbyShow(itemid) + # self.added_episode(all_episodes['Items'], None) @CatchExceptions(warnuser=True) def add_updateSeason(self, item, viewtag=None, viewid=None): API = PlexAPI.API(item) - itemid = API.getRatingKey() - if not itemid: - log.error('Error getting itemid for season, skipping') + plex_id = API.getRatingKey() + if not plex_id: + log.error('Error getting plex_id for season, skipping') return kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork seasonnum = API.getIndex() # Get parent tv show Plex id plexshowid = item.attrib.get('parentRatingKey') # Get Kodi showid - emby_dbitem = emby_db.getItem_byId(plexshowid) + plex_dbitem = plex_db.getItem_byId(plexshowid) try: - showid = emby_dbitem[0] + showid = plex_dbitem[0] except: log.error('Could not find parent tv show for season %s. ' - 'Skipping season for now.' % (itemid)) + 'Skipping season for now.' % (plex_id)) return seasonid = self.kodi_db.addSeason(showid, seasonnum) checksum = API.getChecksum() # Check whether Season already exists update_item = True - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(plex_id) try: - embyDbItemId = emby_dbitem[0] + plexdbItemId = plex_dbitem[0] except TypeError: update_item = False @@ -808,15 +711,16 @@ class TVShows(Items): artwork.addArtwork(allartworks, seasonid, "season", kodicursor) if update_item: - # Update a reference: checksum in emby table - emby_db.updateReference(itemid, checksum) + # Update a reference: checksum in plex table + plex_db.updateReference(plex_id, checksum) else: - # Create the reference in emby table - emby_db.addReference(itemid, + # Create the reference in plex table + plex_db.addReference(plex_id, + PF.PLEX_TYPE_SEASON, seasonid, - "Season", - "season", - parentid=viewid, + PF.KODI_TYPE_SEASON, + parent_id=showid, + view_id=viewid, checksum=checksum) @CatchExceptions(warnuser=True) @@ -826,7 +730,7 @@ class TVShows(Items): """ # Process single episode kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) @@ -838,11 +742,11 @@ class TVShows(Items): if not itemid: log.error('Error getting itemid for episode, skipping') return - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - episodeid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] + episodeid = plex_dbitem[0] + fileid = plex_dbitem[1] + pathid = plex_dbitem[2] except TypeError: update_item = False # episodeid @@ -910,19 +814,10 @@ class TVShows(Items): # title = "| %02d | %s" % (item['IndexNumberEnd'], title) # Get season id - show = emby_db.getItem_byId(seriesId) + show = plex_db.getItem_byId(seriesId) try: showid = show[0] except TypeError: - # self.logMsg("Show is missing from database, trying to add", 2) - # show = self.emby.getItem(seriesId) - # self.logMsg("Show now: %s. Trying to add new show" % show, 2) - # self.add_update(show) - # show = emby_db.getItem_byId(seriesId) - # try: - # showid = show[0] - # except TypeError: - # log.error("Skipping: %s. Unable to add series: %s." % (itemid, seriesId)) log.error("Parent tvshow now found, skip item") return False seasonid = self.kodi_db.addSeason(showid, season) @@ -956,7 +851,7 @@ class TVShows(Items): filename = playurl.rsplit('/', 1)[1] else: filename = 'file_not_found.mkv' - path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId + path = "plugin://plugin.video.plexkodiconnect/tvshows/%s/" % seriesId params = { 'filename': tryEncode(filename), 'id': itemid, @@ -966,7 +861,7 @@ class TVShows(Items): filename = "%s?%s" % (path, tryDecode(urlencode(params))) playurl = filename parentPathId = self.kodi_db.addPath( - 'plugin://plugin.video.plexkodiconnect.tvshows/') + 'plugin://plugin.video.plexkodiconnect/tvshows/') # episodes table: # c18 - playurl @@ -983,7 +878,7 @@ class TVShows(Items): log.info("UPDATE episode itemid: %s" % (itemid)) # Update the movie entry - if KODIVERSION in (16, 17): + if KODIVERSION >= 16: # Kodi Jarvis, Krypton query = ' '.join(( "UPDATE episode", @@ -1010,13 +905,13 @@ class TVShows(Items): airsBeforeSeason, airsBeforeEpisode, playurl, pathid, fileid, episodeid)) # Update parentid reference - emby_db.updateParentId(itemid, seasonid) + plex_db.updateParentId(itemid, seasonid) ##### OR ADD THE EPISODE ##### else: log.info("ADD episode itemid: %s - Title: %s" % (itemid, title)) # Create the episode entry - if KODIVERSION in (16, 17): + if KODIVERSION >= 16: # Kodi Jarvis, Krypton query = ( ''' @@ -1044,11 +939,18 @@ class TVShows(Items): premieredate, runtime, director, season, episode, title, showid, airsBeforeSeason, airsBeforeEpisode, playurl, pathid)) - # Create or update the reference in emby table Add reference is + # Create or update the reference in plex table Add reference is # idempotent; the call here updates also fileid and pathid when item is # moved or renamed - emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, - pathid, seasonid, checksum) + plex_db.addReference(itemid, + PF.PLEX_TYPE_EPISODE, + episodeid, + PF.KODI_TYPE_EPISODE, + kodi_fileid=fileid, + kodi_pathid=pathid, + parent_id=seasonid, + checksum=checksum, + view_id=viewid) # Update the path query = ' '.join(( @@ -1093,7 +995,7 @@ class TVShows(Items): self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) if not self.directpath and resume: # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/") + temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/") tempfileid = self.kodi_db.addFile(filename, temppathid) query = ' '.join(( @@ -1110,17 +1012,17 @@ class TVShows(Items): dateplayed) def remove(self, itemid): - # Remove showid, fileid, pathid, emby reference - emby_db = self.emby_db + # Remove showid, fileid, pathid, plex reference + plex_db = self.plex_db kodicursor = self.kodicursor - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - parentid = emby_dbitem[3] - mediatype = emby_dbitem[4] + kodiid = plex_dbitem[0] + fileid = plex_dbitem[1] + pathid = plex_dbitem[2] + parentid = plex_dbitem[3] + mediatype = plex_dbitem[4] log.info("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid)) except TypeError: @@ -1128,30 +1030,31 @@ class TVShows(Items): ##### PROCESS ITEM ##### - # Remove the emby reference - emby_db.removeItem(itemid) + # Remove the plex reference + plex_db.removeItem(itemid) ##### IF EPISODE ##### - if mediatype == "episode": + if mediatype == PF.KODI_TYPE_EPISODE: # Delete kodi episode and file, verify season and tvshow self.removeEpisode(kodiid, fileid) # Season verification - season = emby_db.getItem_byKodiId(parentid, "season") + season = plex_db.getItem_byKodiId(parentid, PF.KODI_TYPE_SEASON) try: showid = season[1] except TypeError: return - season_episodes = emby_db.getItem_byParentId(parentid, "episode") + season_episodes = plex_db.getItem_byParentId(parentid, + PF.KODI_TYPE_EPISODE) if not season_episodes: self.removeSeason(parentid) - emby_db.removeItem(season[0]) + plex_db.removeItem(season[0]) # Show verification - show = emby_db.getItem_byKodiId(showid, "tvshow") + show = plex_db.getItem_byKodiId(showid, PF.KODI_TYPE_SHOW) query = ' '.join(( "SELECT totalCount", @@ -1162,55 +1065,62 @@ class TVShows(Items): result = kodicursor.fetchone() if result and result[0] is None: # There's no episodes left, delete show and any possible remaining seasons - seasons = emby_db.getItem_byParentId(showid, "season") + seasons = plex_db.getItem_byParentId(showid, + PF.KODI_TYPE_SEASON) for season in seasons: self.removeSeason(season[1]) else: - # Delete emby season entries - emby_db.removeItems_byParentId(showid, "season") + # Delete plex season entries + plex_db.removeItems_byParentId(showid, + PF.KODI_TYPE_SEASON) self.removeShow(showid) - emby_db.removeItem(show[0]) + plex_db.removeItem(show[0]) ##### IF TVSHOW ##### elif mediatype == "tvshow": # Remove episodes, seasons, tvshow - seasons = emby_db.getItem_byParentId(kodiid, "season") + seasons = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_SEASON) for season in seasons: seasonid = season[1] - season_episodes = emby_db.getItem_byParentId(seasonid, "episode") + season_episodes = plex_db.getItem_byParentId(seasonid, + PF.KODI_TYPE_EPISODE) for episode in season_episodes: self.removeEpisode(episode[1], episode[2]) else: - # Remove emby episodes - emby_db.removeItems_byParentId(seasonid, "episode") + # Remove plex episodes + plex_db.removeItems_byParentId(seasonid, + PF.KODI_TYPE_EPISODE) else: - # Remove emby seasons - emby_db.removeItems_byParentId(kodiid, "season") + # Remove plex seasons + plex_db.removeItems_byParentId(kodiid, + PF.KODI_TYPE_SEASON) # Remove tvshow self.removeShow(kodiid) ##### IF SEASON ##### - elif mediatype == "season": + elif mediatype == PF.KODI_TYPE_SEASON: # Remove episodes, season, verify tvshow - season_episodes = emby_db.getItem_byParentId(kodiid, "episode") + season_episodes = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_EPISODE) for episode in season_episodes: self.removeEpisode(episode[1], episode[2]) else: - # Remove emby episodes - emby_db.removeItems_byParentId(kodiid, "episode") + # Remove plex episodes + plex_db.removeItems_byParentId(kodiid, PF.KODI_TYPE_EPISODE) # Remove season self.removeSeason(kodiid) # Show verification - seasons = emby_db.getItem_byParentId(parentid, "season") + seasons = plex_db.getItem_byParentId(parentid, PF.KODI_TYPE_SEASON) if not seasons: # There's no seasons, delete the show self.removeShow(parentid) - emby_db.removeItem_byKodiId(parentid, "tvshow") + plex_db.removeItem_byKodiId(parentid, PF.KODI_TYPE_SHOW) log.debug("Deleted %s: %s from kodi database" % (mediatype, itemid)) @@ -1249,12 +1159,12 @@ class Music(Items): OVERWRITE this method, because we need to open another DB. Open DB connections and cursors """ - self.embyconn = kodiSQL('emby') - self.embycursor = self.embyconn.cursor() + self.plexconn = kodiSQL('plex') + self.plexcursor = self.plexconn.cursor() # Here it is, not 'video' but 'music' self.kodiconn = kodiSQL('music') self.kodicursor = self.kodiconn.cursor() - self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) return self @@ -1262,15 +1172,15 @@ class Music(Items): def add_updateArtist(self, item, viewtag=None, viewid=None, artisttype="MusicArtist"): kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) update_item = True itemid = API.getRatingKey() - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - artistid = emby_dbitem[0] + artistid = plex_dbitem[0] except TypeError: update_item = False @@ -1300,23 +1210,26 @@ class Music(Items): # UPDATE THE ARTIST ##### if update_item: log.info("UPDATE artist itemid: %s - Name: %s" % (itemid, name)) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) + # Update the checksum in plex table + plex_db.updateReference(itemid, checksum) # OR ADD THE ARTIST ##### else: log.info("ADD artist itemid: %s - Name: %s" % (itemid, name)) - # safety checks: It looks like Emby supports the same artist + # safety checks: It looks like plex supports the same artist # multiple times. # Kodi doesn't allow that. In case that happens we just merge the # artist entries. artistid = self.kodi_db.addArtist(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference( - itemid, artistid, artisttype, "artist", checksum=checksum) + # Create the reference in plex table + plex_db.addReference(itemid, + PF.PLEX_TYPE_ARTIST, + artistid, + PF.KODI_TYPE_ARTIST, + checksum=checksum) # Process the artist - if KODIVERSION in (16, 17): + if KODIVERSION >= 16: query = ' '.join(( "UPDATE artist", @@ -1343,7 +1256,7 @@ class Music(Items): @CatchExceptions(warnuser=True) def add_updateAlbum(self, item, viewtag=None, viewid=None): kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) @@ -1352,9 +1265,9 @@ class Music(Items): if not itemid: log.error('Error processing Album, skipping') return - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - albumid = emby_dbitem[0] + albumid = plex_dbitem[0] except TypeError: # Albumid not found update_item = False @@ -1393,23 +1306,26 @@ class Music(Items): # UPDATE THE ALBUM ##### if update_item: log.info("UPDATE album itemid: %s - Name: %s" % (itemid, name)) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) + # Update the checksum in plex table + plex_db.updateReference(itemid, checksum) # OR ADD THE ALBUM ##### else: log.info("ADD album itemid: %s - Name: %s" % (itemid, name)) - # safety checks: It looks like Emby supports the same artist + # safety checks: It looks like plex supports the same artist # multiple times. # Kodi doesn't allow that. In case that happens we just merge the # artist entries. albumid = self.kodi_db.addAlbum(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference( - itemid, albumid, "MusicAlbum", "album", checksum=checksum) + # Create the reference in plex table + plex_db.addReference(itemid, + PF.PLEX_TYPE_ALBUM, + albumid, + PF.KODI_TYPE_ALBUM, + checksum=checksum) # Process the album info - if KODIVERSION == 17: + if KODIVERSION >= 17: # Kodi Krypton query = ' '.join(( @@ -1462,41 +1378,43 @@ class Music(Items): rating, lastScraped, dateadded, studio, albumid)) - # Associate the parentid for emby reference + # Associate the parentid for plex reference parentId = item.attrib.get('parentRatingKey') if parentId is not None: - emby_dbartist = emby_db.getItem_byId(parentId) + plex_dbartist = plex_db.getItem_byId(parentId) try: - artistid = emby_dbartist[0] + artistid = plex_dbartist[0] except TypeError: - log.info('Artist %s does not exist in emby database' + log.info('Artist %s does not exist in plex database' % parentId) - artist = GetPlexMetadata(parentId) + artist = PF.GetPlexMetadata(parentId) # Item may not be an artist, verification necessary. if artist is not None and artist != 401: if artist[0].attrib.get('type') == "artist": # Update with the parentId, for remove reference - emby_db.addReference( - parentId, parentId, "MusicArtist", "artist") - emby_db.updateParentId(itemid, parentId) + plex_db.addReference(parentId, + PF.PLEX_TYPE_ARTIST, + parentId, + PF.KODI_TYPE_ARTIST) + plex_db.updateParentId(itemid, parentId) else: - # Update emby reference with the artistid - emby_db.updateParentId(itemid, artistid) + # Update plex reference with the artistid + plex_db.updateParentId(itemid, artistid) # Assign main artists to album # Plex unfortunately only supports 1 artist :-( artistId = parentId - emby_dbartist = emby_db.getItem_byId(artistId) + plex_dbartist = plex_db.getItem_byId(artistId) try: - artistid = emby_dbartist[0] + artistid = plex_dbartist[0] except TypeError: - # Artist does not exist in emby database, create the reference + # Artist does not exist in plex database, create the reference log.info('Artist %s does not exist in Plex database' % artistId) - artist = GetPlexMetadata(artistId) + artist = PF.GetPlexMetadata(artistId) if artist is not None and artist != 401: self.add_updateArtist(artist[0], artisttype="AlbumArtist") - emby_dbartist = emby_db.getItem_byId(artistId) - artistid = emby_dbartist[0] + plex_dbartist = plex_db.getItem_byId(artistId) + artistid = plex_dbartist[0] else: # Best take this name over anything else. query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" @@ -1522,8 +1440,8 @@ class Music(Items): ''' ) kodicursor.execute(query, (artistid, name, year)) - # Update emby reference with parentid - emby_db.updateParentId(artistId, albumid) + # Update plex reference with parentid + plex_db.updateParentId(artistId, albumid) # Add genres self.kodi_db.addMusicGenres(albumid, genres, "album") # Update artwork @@ -1533,7 +1451,7 @@ class Music(Items): def add_updateSong(self, item, viewtag=None, viewid=None): # Process single song kodicursor = self.kodicursor - emby_db = self.emby_db + plex_db = self.plex_db artwork = self.artwork API = PlexAPI.API(item) @@ -1542,11 +1460,11 @@ class Music(Items): if not itemid: log.error('Error processing Song; skipping') return - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - songid = emby_dbitem[0] - pathid = emby_dbitem[2] - albumid = emby_dbitem[3] + songid = plex_dbitem[0] + pathid = plex_dbitem[2] + albumid = plex_dbitem[3] except TypeError: # Songid not found update_item = False @@ -1630,8 +1548,8 @@ class Music(Items): duration, year, filename, playcount, dateplayed, rating, comment, songid)) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) + # Update the checksum in plex table + plex_db.updateReference(itemid, checksum) # OR ADD THE SONG ##### else: @@ -1642,9 +1560,9 @@ class Music(Items): try: # Get the album - emby_dbalbum = emby_db.getItem_byId( + plex_dbalbum = plex_db.getItem_byId( item.attrib.get('parentRatingKey')) - albumid = emby_dbalbum[0] + albumid = plex_dbalbum[0] except KeyError: # Verify if there's an album associated. album_name = item.get('parentTitle') @@ -1652,7 +1570,10 @@ class Music(Items): log.info("Creating virtual music album for song: %s." % itemid) albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) - emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album") + plex_db.addReference("%salbum%s" % (itemid, albumid), + PF.PLEX_TYPE_ALBUM, + albumid, + PF.KODI_TYPE_ALBUM) else: # No album Id associated to the song. log.error("Song itemid: %s has no albumId associated." @@ -1662,22 +1583,22 @@ class Music(Items): except TypeError: # No album found. Let's create it log.info("Album database entry missing.") - emby_albumId = item.attrib.get('parentRatingKey') - album = GetPlexMetadata(emby_albumId) + plex_albumId = item.attrib.get('parentRatingKey') + album = PF.GetPlexMetadata(plex_albumId) if album is None or album == 401: log.error('Could not download album, abort') return self.add_updateAlbum(album[0]) - emby_dbalbum = emby_db.getItem_byId(emby_albumId) + plex_dbalbum = plex_db.getItem_byId(plex_albumId) try: - albumid = emby_dbalbum[0] + albumid = plex_dbalbum[0] log.debug("Found albumid: %s" % albumid) except TypeError: # No album found, create a single's album log.info("Failed to add album. Creating singles.") kodicursor.execute("select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 - if KODIVERSION == 16: + if KODIVERSION >= 16: # Kodi Jarvis query = ( ''' @@ -1724,12 +1645,14 @@ class Music(Items): duration, year, filename, musicBrainzId, playcount, dateplayed, rating, 0, 0)) - # Create the reference in emby table - emby_db.addReference( - itemid, songid, "Audio", "song", - pathid=pathid, - parentid=albumid, - checksum=checksum) + # Create the reference in plex table + plex_db.addReference(itemid, + PF.PLEX_TYPE_SONG, + songid, + PF.KODI_TYPE_SONG, + kodi_pathid=pathid, + parent_id=albumid, + checksum=checksum) # Link song to album query = ( @@ -1752,17 +1675,17 @@ class Music(Items): artist_name = artist['Name'] artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) + artist_edb = plex_db.getItem_byId(artist_eid) try: artistid = artist_edb[0] except TypeError: - # Artist is missing from emby database, add it. - artistXml = GetPlexMetadata(artist_eid) + # Artist is missing from plex database, add it. + artistXml = PF.GetPlexMetadata(artist_eid) if artistXml is None or artistXml == 401: log.error('Error getting artist, abort') return self.add_updateArtist(artistXml[0]) - artist_edb = emby_db.getItem_byId(artist_eid) + artist_edb = plex_db.getItem_byId(artist_eid) artistid = artist_edb[0] finally: if KODIVERSION >= 17: @@ -1798,17 +1721,17 @@ class Music(Items): artist_name = artist['Name'] album_artists.append(artist_name) artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) + artist_edb = plex_db.getItem_byId(artist_eid) try: artistid = artist_edb[0] except TypeError: - # Artist is missing from emby database, add it. - artistXml = GetPlexMetadata(artist_eid) + # Artist is missing from plex database, add it. + artistXml = PF.GetPlexMetadata(artist_eid) if artistXml is None or artistXml == 401: log.error('Error getting artist, abort') return self.add_updateArtist(artistXml) - artist_edb = emby_db.getItem_byId(artist_eid) + artist_edb = plex_db.getItem_byId(artist_eid) artistid = artist_edb[0] finally: query = ( @@ -1840,7 +1763,7 @@ class Music(Items): result = kodicursor.fetchone() if result and result[0] != album_artists: # Field is empty - if KODIVERSION in (16, 17): + if KODIVERSION >= 16: # Kodi Jarvis, Krypton query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" kodicursor.execute(query, (album_artists, albumid)) @@ -1868,76 +1791,84 @@ class Music(Items): artwork.addArtwork(allart, albumid, "album", kodicursor) def remove(self, itemid): - # Remove kodiid, fileid, pathid, emby reference - emby_db = self.emby_db + # Remove kodiid, fileid, pathid, plex reference + plex_db = self.plex_db - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] + kodiid = plex_dbitem[0] + mediatype = plex_dbitem[4] log.info("Removing %s kodiid: %s" % (mediatype, kodiid)) except TypeError: return ##### PROCESS ITEM ##### - # Remove the emby reference - emby_db.removeItem(itemid) + # Remove the plex reference + plex_db.removeItem(itemid) ##### IF SONG ##### - if mediatype == "song": + if mediatype == PF.KODI_TYPE_SONG: # Delete song self.removeSong(kodiid) # This should only address single song scenario, where server doesn't actually # create an album for the song. - emby_db.removeWildItem(itemid) + plex_db.removeWildItem(itemid) - for item in emby_db.getItem_byWildId(itemid): + for item in plex_db.getItem_byWildId(itemid): item_kid = item[0] item_mediatype = item[1] - if item_mediatype == "album": - childs = emby_db.getItem_byParentId(item_kid, "song") + if item_mediatype == PF.KODI_TYPE_ALBUM: + childs = plex_db.getItem_byParentId(item_kid, + PF.KODI_TYPE_SONG) if not childs: # Delete album self.removeAlbum(item_kid) ##### IF ALBUM ##### - elif mediatype == "album": + elif mediatype == PF.KODI_TYPE_ALBUM: # Delete songs, album - album_songs = emby_db.getItem_byParentId(kodiid, "song") + album_songs = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_SONG) for song in album_songs: self.removeSong(song[1]) else: - # Remove emby songs - emby_db.removeItems_byParentId(kodiid, "song") + # Remove plex songs + plex_db.removeItems_byParentId(kodiid, + PF.KODI_TYPE_SONG) # Remove the album self.removeAlbum(kodiid) ##### IF ARTIST ##### - elif mediatype == "artist": + elif mediatype == PF.KODI_TYPE_ARTIST: # Delete songs, album, artist - albums = emby_db.getItem_byParentId(kodiid, "album") + albums = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_ALBUM) for album in albums: albumid = album[1] - album_songs = emby_db.getItem_byParentId(albumid, "song") + album_songs = plex_db.getItem_byParentId(albumid, + PF.KODI_TYPE_SONG) for song in album_songs: self.removeSong(song[1]) else: - # Remove emby song - emby_db.removeItems_byParentId(albumid, "song") - # Remove emby artist - emby_db.removeItems_byParentId(albumid, "artist") + # Remove plex song + plex_db.removeItems_byParentId(albumid, + PF.KODI_TYPE_SONG) + # Remove plex artist + plex_db.removeItems_byParentId(albumid, + PF.KODI_TYPE_ARTIST) # Remove kodi album self.removeAlbum(albumid) else: - # Remove emby albums - emby_db.removeItems_byParentId(kodiid, "album") + # Remove plex albums + plex_db.removeItems_byParentId(kodiid, + PF.KODI_TYPE_ALBUM) # Remove artist self.removeArtist(kodiid) @@ -1945,16 +1876,18 @@ class Music(Items): log.info("Deleted %s: %s from kodi database" % (mediatype, itemid)) def removeSong(self, kodiid): - self.artwork.deleteArtwork(kodiid, "song", self.kodicursor) + self.artwork.deleteArtwork(kodiid, PF.KODI_TYPE_SONG, self.kodicursor) self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) def removeAlbum(self, kodiid): - self.artwork.deleteArtwork(kodiid, "album", self.kodicursor) + self.artwork.deleteArtwork(kodiid, PF.KODI_TYPE_ALBUM, self.kodicursor) self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) def removeArtist(self, kodiid): - self.artwork.deleteArtwork(kodiid, "artist", self.kodicursor) + self.artwork.deleteArtwork(kodiid, + PF.KODI_TYPE_ARTIST, + self.kodicursor) self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f2046fd8..e1c7ff6b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -7,6 +7,7 @@ from ntpath import dirname import artwork from utils import kodiSQL, KODIVERSION +from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE ############################################################################### @@ -31,8 +32,8 @@ class GetKodiDB(): def __enter__(self): self.kodiconn = kodiSQL(self.itemType) - self.emby_db = Kodidb_Functions(self.kodiconn.cursor()) - return self.emby_db + kodi_db = Kodidb_Functions(self.kodiconn.cursor()) + return kodi_db def __exit__(self, type, value, traceback): self.kodiconn.commit() @@ -61,7 +62,7 @@ class Kodidb_Functions(): self.cursor.execute( query, ('movies', 'metadata.local', - 'plugin://plugin.video.plexkodiconnect.movies%%')) + 'plugin://plugin.video.plexkodiconnect/movies%%')) def getParentPathId(self, path): """ @@ -869,7 +870,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (idFile,)) try: itemId = self.cursor.fetchone()[0] - typus = 'movie' + typus = KODI_TYPE_MOVIE except TypeError: # Try tv shows next query = ' '.join(( @@ -880,7 +881,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (idFile,)) try: itemId = self.cursor.fetchone()[0] - typus = 'episode' + typus = KODI_TYPE_EPISODE except TypeError: log.warn('Unexpectantly did not find a match!') return @@ -907,13 +908,13 @@ class Kodidb_Functions(): return ids def getVideoRuntime(self, kodiid, mediatype): - if mediatype == 'movie': + if mediatype == KODI_TYPE_MOVIE: query = ' '.join(( "SELECT c11", "FROM movie", "WHERE idMovie = ?", )) - elif mediatype == 'episode': + elif mediatype == KODI_TYPE_EPISODE: query = ' '.join(( "SELECT c09", "FROM episode", @@ -1397,3 +1398,89 @@ class Kodidb_Functions(): query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" self.cursor.execute(query, (genreid, kodiid)) + +# Krypton only stuff ############################## + + def create_entry_uniqueid(self): + self.cursor.execute( + "select coalesce(max(uniqueid_id),0) from uniqueid") + return self.cursor.fetchone()[0] + 1 + + def add_uniqueid(self, *args): + """ + Feed with: + uniqueid_id, media_id, media_type, value, type + + type: e.g. 'imdb' + """ + query = ''' + INSERT INTO uniqueid( + uniqueid_id, media_id, media_type, value, type) + VALUES (?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (args)) + + def create_entry_rating(self): + self.cursor.execute("select coalesce(max(rating_id),0) from rating") + return self.cursor.fetchone()[0] + 1 + + def get_ratingid(self, media_id): + query = "SELECT rating_id FROM rating WHERE media_id = ?" + self.cursor.execute(query, (media_id,)) + try: + ratingid = self.cursor.fetchone()[0] + except TypeError: + ratingid = None + return ratingid + + def update_ratings(self, *args): + """ + Feed with media_id, media_type, rating_type, rating, votes, rating_id + """ + query = ''' + UPDATE rating + SET media_id = ?, + media_type = ?, + rating_type = ?, + rating = ?, + votes = ? + WHERE rating_id = ? + ''' + self.cursor.execute(query, (args)) + + def add_ratings(self, *args): + """ + feed with: + rating_id, media_id, media_type, rating_type, rating, votes + + rating_type = 'default' + """ + query = ''' + INSERT INTO rating( + rating_id, media_id, media_type, rating_type, rating, votes) + VALUES (?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (args)) + + +def get_kodiid_from_filename(file): + """ + Returns the tuple (kodiid, type) if we have a video in the database with + said filename, or (None, None) + """ + kodiid = None + typus = None + try: + filename = file.rsplit('/', 1)[1] + path = file.rsplit('/', 1)[0] + '/' + except IndexError: + filename = file.rsplit('\\', 1)[1] + path = file.rsplit('\\', 1)[0] + '\\' + log.debug('Trying to figure out playing item from filename: %s ' + 'and path: %s' % (filename, path)) + with GetKodiDB('video') as kodi_db: + try: + kodiid, typus = kodi_db.getIdFromFilename(filename, path) + except TypeError: + log.info('No kodi video element found with filename %s' % filename) + return (kodiid, typus) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 8898e25f..019c1b5f 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -3,17 +3,16 @@ ############################################################################### import logging -import json +from json import loads -import xbmc -import xbmcgui +from xbmc import Monitor, Player, sleep import downloadutils -import embydb_functions as embydb -import kodidb_functions as kodidb -import playbackutils as pbutils +import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, tryDecode, tryEncode from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE +from kodidb_functions import get_kodiid_from_filename +from PlexAPI import API ############################################################################### @@ -22,13 +21,14 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -class KodiMonitor(xbmc.Monitor): - - def __init__(self): +class KodiMonitor(Monitor): + def __init__(self, callback): + self.mgr = callback self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.xbmcplayer = xbmc.Player() - xbmc.Monitor.__init__(self) + self.xbmcplayer = Player() + self.playqueue = self.mgr.playqueue + Monitor.__init__(self) log.info("Kodi monitor started.") def onScanStarted(self, library): @@ -70,7 +70,7 @@ class KodiMonitor(xbmc.Monitor): def onNotification(self, sender, method, data): if data: - data = json.loads(data, 'utf-8') + data = loads(data, 'utf-8') log.debug("Method: %s Data: %s" % (method, data)) if method == "Player.OnPlay": @@ -92,18 +92,18 @@ class KodiMonitor(xbmc.Monitor): log.info("Item is invalid for playstate update.") else: # Send notification to the server. - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) + with plexdb.Get_Plex_DB() as plexcur: + plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type) try: - itemid = emby_dbitem[0] + itemid = plex_dbitem[0] except TypeError: - log.error("Could not find itemid in emby database for a " + log.error("Could not find itemid in plex database for a " "video library update") else: # Stop from manually marking as watched unwatched, with actual playback. - if window('emby_skipWatched%s' % itemid) == "true": + if window('plex_skipWatched%s' % itemid) == "true": # property is set in player.py - window('emby_skipWatched%s' % itemid, clear=True) + window('plex_skipWatched%s' % itemid, clear=True) else: # notify the server if playcount != 0: @@ -112,40 +112,7 @@ class KodiMonitor(xbmc.Monitor): scrobble(itemid, 'unwatched') elif method == "VideoLibrary.OnRemove": - # Removed function, because with plugin paths + clean library, it will wipe - # entire library if user has permissions. Instead, use the emby context menu available - # in Isengard and higher version pass - '''try: - kodiid = data['id'] - type = data['type'] - except (KeyError, TypeError): - log.info("Item is invalid for emby deletion.") - else: - # Send the delete action to the server. - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - log.info("Could not find itemid in emby database.") - else: - if settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1="Delete file on Emby Server?") - if not resp: - log.info("User skipped deletion.") - embycursor.close() - return - - url = "{server}/emby/Items/%s?format=json" % itemid - log.info("Deleting request: %s" % itemid) - doUtils.downloadUrl(url, action_type="DELETE") - finally: - embycursor.close()''' elif method == "System.OnSleep": # Connection is going to sleep @@ -154,18 +121,15 @@ class KodiMonitor(xbmc.Monitor): elif method == "System.OnWake": # Allow network to wake up - xbmc.sleep(10000) + sleep(10000) window('plex_onWake', value="true") window('plex_online', value="false") elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": - xbmc.sleep(5000) + sleep(5000) window('plex_runLibScan', value="full") - elif method == "Playlist.OnClear": - pass - def PlayBackStart(self, data): """ Called whenever a playback is started @@ -177,7 +141,7 @@ class KodiMonitor(xbmc.Monitor): currentFile = None count = 0 while currentFile is None: - xbmc.sleep(100) + sleep(100) try: currentFile = self.xbmcplayer.getPlayingFile() except: @@ -201,7 +165,7 @@ class KodiMonitor(xbmc.Monitor): # Try to get a Kodi ID # If PKC was used - native paths, not direct paths - plexid = window('emby_%s.itemid' % tryEncode(currentFile)) + plexid = window('plex_%s.itemid' % tryEncode(currentFile)) # Get rid of the '' if the window property was not set plexid = None if not plexid else plexid kodiid = None @@ -215,27 +179,16 @@ class KodiMonitor(xbmc.Monitor): # When using Widgets, Kodi doesn't tell us shit so we need this hack if (kodiid is None and plexid is None and typus != 'song' and not currentFile.startswith('http')): - try: - filename = currentFile.rsplit('/', 1)[1] - path = currentFile.rsplit('/', 1)[0] + '/' - except IndexError: - filename = currentFile.rsplit('\\', 1)[1] - path = currentFile.rsplit('\\', 1)[0] + '\\' - log.debug('Trying to figure out playing item from filename: %s ' - 'and path: %s' % (filename, path)) - with kodidb.GetKodiDB('video') as kodi_db: - try: - kodiid, typus = kodi_db.getIdFromFilename(filename, path) - except TypeError: - log.info('Abort playback report, could not id kodi item') - return + (kodiid, typus) = get_kodiid_from_filename(currentFile) + if kodiid is None: + return if plexid is None: # Get Plex' item id - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(kodiid, typus) + with plexdb.Get_Plex_DB() as plexcursor: + plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus) try: - plexid = emby_dbitem[0] + plexid = plex_dbitem[0] except TypeError: log.info("No Plex id returned for kodiid %s. Aborting playback" " report" % kodiid) @@ -256,24 +209,25 @@ class KodiMonitor(xbmc.Monitor): # Save currentFile for cleanup later and to be able to access refs window('plex_lastPlayedFiled', value=currentFile) window('plex_currently_playing_itemid', value=plexid) - window("emby_%s.itemid" % tryEncode(currentFile), value=plexid) + window("plex_%s.itemid" % tryEncode(currentFile), value=plexid) log.info('Finish playback startup') def StartDirectPath(self, plexid, type, currentFile): """ Set some additional stuff if playback was initiated by Kodi, not PKC """ - result = self.doUtils('{server}/library/metadata/%s' % plexid) + xml = self.doUtils('{server}/library/metadata/%s' % plexid) try: - result[0].attrib + xml[0].attrib except: log.error('Did not receive a valid XML for plexid %s.' % plexid) return False # Setup stuff, because playback was started by Kodi, not PKC - pbutils.PlaybackUtils(result[0]).setProperties( - currentFile, xbmcgui.ListItem()) + api = API(xml[0]) + listitem = api.CreateListItemFromPlexItem() + api.set_playback_win_props(currentFile, listitem) if type == "song" and settings('streamMusic') == "true": - window('emby_%s.playmethod' % currentFile, value="DirectStream") + window('plex_%s.playmethod' % currentFile, value="DirectStream") else: - window('emby_%s.playmethod' % currentFile, value="DirectPlay") + window('plex_%s.playmethod' % currentFile, value="DirectPlay") log.debug('Window properties set for direct paths!') diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 7b37ce4c..82997705 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -11,7 +11,7 @@ import xbmc import xbmcgui import xbmcvfs -from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\ +from utils import window, settings, getUnixTimestamp, sourcesXML,\ ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\ @@ -19,7 +19,7 @@ from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\ import clientinfo import downloadutils import itemtypes -import embydb_functions as embydb +import plexdb_functions as plexdb import kodidb_functions as kodidb import userclient import videonodes @@ -302,9 +302,9 @@ class ProcessFanartThread(Thread): # Leave the Plex art untouched allartworks = None else: - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: try: - kodiId = emby_db.getItem_byId(item['itemId'])[0] + kodiId = plex_db.getItem_byId(item['itemId'])[0] except TypeError: log.error('Could not get Kodi id for plex id %s' % item['itemId']) @@ -356,19 +356,10 @@ class ProcessFanartThread(Thread): @ThreadMethods class LibrarySync(Thread): """ - librarysync.LibrarySync(queue) - - where (communication with websockets) - queue: Queue object for background sync """ - # Borg, even though it's planned to only have 1 instance up and running! - _shared_state = {} + def __init__(self, callback=None): + self.mgr = callback - def __init__(self, queue): - self.__dict__ = self._shared_state - - # Communication with websockets - self.queue = queue self.itemsToProcess = [] self.sessionKeys = [] self.fanartqueue = Queue.Queue() @@ -455,7 +446,9 @@ class LibrarySync(Thread): return False plexId = None - for mediatype in ('movie', 'show', 'artist'): + for mediatype in (PF.PLEX_TYPE_MOVIE, + PF.PLEX_TYPE_SHOW, + PF.PLEX_TYPE_ARTIST): if plexId is not None: break for view in sections: @@ -539,24 +532,34 @@ class LibrarySync(Thread): def initializeDBs(self): """ - Run once during startup to verify that emby db exists. + Run once during startup to verify that plex db exists. """ - embyconn = 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 - embyconn.close() + with plexdb.Get_Plex_DB() as plex_db: + # Create the tables for the plex database + plex_db.plexcursor.execute(''' + CREATE TABLE IF NOT EXISTS plex( + plex_id TEXT UNIQUE, + view_id TEXT, + plex_type TEXT, + kodi_type TEXT, + kodi_id INTEGER, + kodi_fileid INTEGER, + kodi_pathid INTEGER, + parent_id INTEGER, + checksum INTEGER) + ''') + plex_db.plexcursor.execute(''' + CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, + view_name TEXT, + kodi_type TEXT, + kodi_tagid INTEGER) + ''') + plex_db.plexcursor.execute(''' + CREATE TABLE IF NOT EXISTS version(idVersion TEXT) + ''') + # Create an index for actors to speed up sync + create_actor_db_index() # Create an index for actors to speed up sync create_actor_db_index() @@ -643,12 +646,13 @@ class LibrarySync(Thread): log.error('Path hack failed with error message: %s' % str(e)) return True - def processView(self, folderItem, kodi_db, emby_db, totalnodes): + def processView(self, folderItem, kodi_db, plex_db, totalnodes): vnodes = self.vnodes folder = folderItem.attrib mediatype = folder['type'] # Only process supported formats - if mediatype not in ('movie', 'show', 'artist', 'photo'): + if mediatype not in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW, + PF.PLEX_TYPE_ARTIST, PF.PLEX_TYPE_PHOTO): return totalnodes # Prevent duplicate for nodes of the same type @@ -661,8 +665,8 @@ class LibrarySync(Thread): foldername = folder['title'] viewtype = folder['type'] - # Get current media folders from emby database - view = emby_db.getView_byId(folderid) + # Get current media folders from plex database + view = plex_db.getView_byId(folderid) try: current_viewname = view[0] current_viewtype = view[1] @@ -672,12 +676,12 @@ class LibrarySync(Thread): tagid = kodi_db.createTag(foldername) # Create playlist for the video library if (foldername not in playlists and - mediatype in ('movie', 'show', 'musicvideos')): + mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)): playlistXSP(mediatype, foldername, folderid, viewtype) playlists.append(foldername) # Create the video node if (foldername not in nodes and - mediatype not in ("musicvideos", "artist")): + mediatype != PF.PLEX_TYPE_ARTIST): vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype, @@ -685,8 +689,8 @@ class LibrarySync(Thread): folderid) nodes.append(foldername) totalnodes += 1 - # Add view to emby database - emby_db.addView(folderid, foldername, viewtype, tagid) + # Add view to plex database + plex_db.addView(folderid, foldername, viewtype, tagid) else: log.info(' '.join(( "Found viewid: %s" % folderid, @@ -708,10 +712,10 @@ class LibrarySync(Thread): tagid = kodi_db.createTag(foldername) # Update view with new info - emby_db.updateView(foldername, tagid, folderid) + plex_db.updateView(foldername, tagid, folderid) if mediatype != "artist": - if emby_db.getView_byName(current_viewname) is None: + 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. @@ -731,7 +735,7 @@ class LibrarySync(Thread): delete=True) # Added new playlist if (foldername not in playlists and - mediatype in ('movie', 'show', 'musicvideos')): + mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)): playlistXSP(mediatype, foldername, folderid, @@ -748,16 +752,16 @@ class LibrarySync(Thread): totalnodes += 1 # Update items with new tag - items = emby_db.getItem_byView(folderid) + items = plex_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 != "artist": + if mediatype != PF.PLEX_TYPE_ARTIST: if (foldername not in playlists and - mediatype in ('movie', 'show', 'musicvideos')): + mediatype in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW)): playlistXSP(mediatype, foldername, folderid, @@ -792,22 +796,22 @@ class LibrarySync(Thread): # For whatever freaking reason, .copy() or dict() does NOT work?!?!?! self.nodes = { - 'movie': [], - 'show': [], - 'artist': [], - 'photo': [] + PF.PLEX_TYPE_MOVIE: [], + PF.PLEX_TYPE_SHOW: [], + PF.PLEX_TYPE_ARTIST: [], + PF.PLEX_TYPE_PHOTO: [] } self.playlists = { - 'movie': [], - 'show': [], - 'artist': [], - 'photo': [] + PF.PLEX_TYPE_MOVIE: [], + PF.PLEX_TYPE_SHOW: [], + PF.PLEX_TYPE_ARTIST: [], + PF.PLEX_TYPE_PHOTO: [] } self.sorted_views = [] for view in sections: itemType = view.attrib['type'] - if itemType in ('movie', 'show', 'photo'): # NOT artist for now + if itemType in (PF.PLEX_TYPE_MOVIE, PF.PLEX_TYPE_SHOW, PF.PLEX_TYPE_PHOTO): # NOT artist for now self.sorted_views.append(view.attrib['title']) log.debug('Sorted views: %s' % self.sorted_views) @@ -815,15 +819,15 @@ class LibrarySync(Thread): vnodes.clearProperties() totalnodes = len(self.sorted_views) - with embydb.GetEmbyDB() as emby_db: + 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 = emby_db.getViews() + self.old_views = plex_db.getViews() with kodidb.GetKodiDB('video') as kodi_db: for folderItem in sections: totalnodes = self.processView(folderItem, kodi_db, - emby_db, + plex_db, totalnodes) # Add video nodes listings # Plex: there seem to be no favorites/favorites tag @@ -842,19 +846,17 @@ class LibrarySync(Thread): # "movies", # "channels") # totalnodes += 1 - with kodidb.GetKodiDB('music') as kodi_db: - pass # Save total window('Plex.nodes.total', str(totalnodes)) # Reopen DB connection to ensure that changes were commited before - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: log.info("Removing views: %s" % self.old_views) for view in self.old_views: - emby_db.removeView(view) + plex_db.removeView(view) # update views for all: - self.views = emby_db.getAllViewInfo() + self.views = plex_db.getAllViewInfo() log.info("Finished processing views. Views saved: %s" % self.views) return True @@ -1038,9 +1040,10 @@ class LibrarySync(Thread): if (settings('FanartTV') == 'true' and itemType in ('Movies', 'TVShows')): # Save to queue for later processing - typus = {'Movies': 'movie', 'TVShows': 'tvshow'}[itemType] + typus = {'Movies': PF.KODI_TYPE_MOVIE, + 'TVShows': PF.KODI_TYPE_SHOW}[itemType] for item in self.updatelist: - if item['mediaType'] in ('movie', 'show'): + if item['mediaType'] in (PF.KODI_TYPE_MOVIE, PF.KODI_TYPE_SHOW): self.fanartqueue.put({ 'itemId': item['itemId'], 'class': itemType, @@ -1056,16 +1059,17 @@ class LibrarySync(Thread): itemType = 'Movies' - views = [x for x in self.views if x['itemtype'] == 'movie'] + views = [x for x in self.views if x['itemtype'] == PF.KODI_TYPE_MOVIE] log.info("Processing Plex %s. Libraries: %s" % (itemType, views)) self.allKodiElementsId = {} if self.compare: - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: # Get movies from Plex server # Pull the list of movies and boxsets in Kodi try: - self.allKodiElementsId = dict(emby_db.getChecksum('Movie')) + self.allKodiElementsId = dict( + plex_db.getChecksum(PF.PLEX_TYPE_MOVIE)) except ValueError: self.allKodiElementsId = {} @@ -1148,11 +1152,13 @@ class LibrarySync(Thread): self.allKodiElementsId = {} if self.compare: - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex: # Pull the list of TV shows already in Kodi - for kind in ('Series', 'Season', 'Episode'): + for kind in (PF.PLEX_TYPE_SHOW, + PF.PLEX_TYPE_SEASON, + PF.PLEX_TYPE_EPISODE): try: - elements = dict(emby_db.getChecksum(kind)) + elements = dict(plex.getChecksum(kind)) self.allKodiElementsId.update(elements) # Yet empty/not yet synched except ValueError: @@ -1270,22 +1276,24 @@ class LibrarySync(Thread): def PlexMusic(self): itemType = 'Music' - views = [x for x in self.views if x['itemtype'] == 'artist'] + views = [x for x in self.views if x['itemtype'] == PF.PLEX_TYPE_ARTIST] log.info("Media folders for %s: %s" % (itemType, views)) methods = { - 'MusicArtist': 'add_updateArtist', - 'MusicAlbum': 'add_updateAlbum', - 'Audio': 'add_updateSong' + PF.PLEX_TYPE_ARTIST: 'add_updateArtist', + PF.PLEX_TYPE_ALBUM: 'add_updateAlbum', + PF.PLEX_TYPE_SONG: 'add_updateSong' } urlArgs = { - 'MusicArtist': {'type': 8}, - 'MusicAlbum': {'type': 9}, - 'Audio': {'type': 10} + PF.PLEX_TYPE_ARTIST: {'type': 8}, + PF.PLEX_TYPE_ALBUM: {'type': 9}, + PF.PLEX_TYPE_SONG: {'type': 10} } # Process artist, then album and tracks last to minimize overhead - for kind in ('MusicArtist', 'MusicAlbum', 'Audio'): + for kind in (PF.PLEX_TYPE_ARTIST, + PF.PLEX_TYPE_ALBUM, + PF.PLEX_TYPE_SONG): if self.threadStopped(): return False log.debug("Start processing music %s" % kind) @@ -1318,10 +1326,10 @@ class LibrarySync(Thread): # Get a list of items already existing in Kodi db if self.compare: - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: # Pull the list of items already in Kodi try: - elements = dict(emby_db.getChecksum(kind)) + elements = dict(plex_db.getChecksum(kind)) self.allKodiElementsId.update(elements) # Yet empty/nothing yet synched except ValueError: @@ -1569,14 +1577,14 @@ class LibrarySync(Thread): where """ items = [] - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: for item in data: # Drop buffering messages immediately state = item.get('state') if state == 'buffering': continue ratingKey = item.get('ratingKey') - kodiInfo = emby_db.getItem_byId(ratingKey) + kodiInfo = plex_db.getItem_byId(ratingKey) if kodiInfo is None: # Item not (yet) in Kodi library continue @@ -1663,7 +1671,7 @@ class LibrarySync(Thread): # Now tell Kodi where we are for item in items: itemFkt = getattr(itemtypes, - PF.ITEMTYPE_FROM_PLEXTYPE[item['kodi_type']]) + PF.ITEMTYPE_FROM_KODITYPE[item['kodi_type']]) with itemFkt() as Fkt: Fkt.updatePlaystate(item) @@ -1675,12 +1683,12 @@ class LibrarySync(Thread): """ items = [] typus = { - 'Movie': 'Movies', - 'Series': 'TVShows' + PF.PLEX_TYPE_MOVIE: 'Movies', + PF.PLEX_TYPE_SHOW: 'TVShows' } - with embydb.GetEmbyDB() as emby_db: + with plexdb.Get_Plex_DB() as plex_db: for plextype in typus: - items.extend(emby_db.itemsByType(plextype)) + items.extend(plex_db.itemsByType(plextype)) # Shuffle the list to not always start out identically shuffle(items) for item in items: @@ -1720,7 +1728,8 @@ class LibrarySync(Thread): xbmcplayer = xbmc.Player() - queue = self.queue + # Link to Websocket queue + queue = self.mgr.ws.queue startupComplete = False self.views = [] diff --git a/resources/lib/monitor_kodi_play.py b/resources/lib/monitor_kodi_play.py new file mode 100644 index 00000000..b7968eeb --- /dev/null +++ b/resources/lib/monitor_kodi_play.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Thread +from Queue import Queue + +from xbmc import sleep + +from utils import window, ThreadMethods + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethods +class Monitor_Kodi_Play(Thread): + """ + Monitors for new plays initiated on the Kodi side with addon paths. + Immediately throws them into a queue to be processed by playback_starter + """ + # Borg - multiple instances, shared state + def __init__(self, callback=None): + self.mgr = callback + self.playback_queue = Queue() + Thread.__init__(self) + + def run(self): + threadStopped = self.threadStopped + queue = self.playback_queue + log.info("----===## Starting Kodi_Play_Client ##===----") + while not threadStopped(): + if window('plex_play_new_item'): + queue.put(window('plex_play_new_item')) + window('plex_play_new_item', clear=True) + else: + sleep(20) + # Put one last item into the queue to let playback_starter end + queue.put(None) + log.info("----===## Kodi_Play_Client stopped ##===----") diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py new file mode 100644 index 00000000..9bd73bec --- /dev/null +++ b/resources/lib/pickler.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +import cPickle as Pickle + +from utils import pickl_window +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +def pickle_me(obj, window_var='plex_result'): + """ + Pickles the obj to the window variable. Use to transfer Python + objects between different PKC python instances (e.g. if default.py is + called and you'd want to use the service.py instance) + + obj can be pretty much any Python object. However, classes and + functions won't work. See the Pickle documentation + """ + log.debug('Start pickling: %s' % obj) + pickl_window(window_var, value=Pickle.dumps(obj)) + log.debug('Successfully pickled') + + +def unpickle_me(window_var='plex_result'): + """ + Unpickles a Python object from the window variable window_var. + Will then clear the window variable! + """ + result = pickl_window(window_var) + pickl_window(window_var, clear=True) + log.debug('Start unpickling') + obj = Pickle.loads(result) + log.debug('Successfully unpickled: %s' % obj) + return obj + + +class Playback_Successful(object): + """ + Used to communicate with another PKC Python instance + """ + listitem = None diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py new file mode 100644 index 00000000..e22feba7 --- /dev/null +++ b/resources/lib/playback_starter.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Thread +from urlparse import parse_qsl + +from PKC_listitem import PKC_ListItem +from pickler import pickle_me, Playback_Successful +from playbackutils import PlaybackUtils +from utils import window +from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO, \ + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE +from PlexAPI import API +from playqueue import lock + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +class Playback_Starter(Thread): + """ + Processes new plays + """ + def __init__(self, callback=None): + self.mgr = callback + self.playqueue = self.mgr.playqueue + Thread.__init__(self) + + def process_play(self, plex_id, kodi_id=None): + """ + Processes Kodi playback init for ONE item + """ + log.info("Process_play called with plex_id %s, kodi_id %s" + % (plex_id, kodi_id)) + if window('plex_authenticated') != "true": + log.error('Not yet authenticated for PMS, abort starting playback') + # Todo: Warn user with dialog + return + xml = GetPlexMetadata(plex_id) + try: + xml[0].attrib + except (TypeError, AttributeError): + log.error('Could not get a PMS xml for plex id %s' % plex_id) + return + api = API(xml[0]) + if api.getType() == PLEX_TYPE_PHOTO: + # Photo + result = Playback_Successful() + listitem = PKC_ListItem() + listitem = api.CreateListItemFromPlexItem(listitem) + api.AddStreamInfo(listitem) + api.set_listitem_artwork(listitem) + result.listitem = listitem + else: + # Video and Music + playqueue = self.playqueue.get_playqueue_from_type( + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + with lock: + result = PlaybackUtils(xml, playqueue).play( + plex_id, + kodi_id, + xml.attrib.get('librarySectionUUID')) + log.info('Done process_play, playqueues: %s' + % self.playqueue.playqueues) + return result + + def triage(self, item): + mode, params = item.split('?', 1) + params = dict(parse_qsl(params)) + log.debug('Received mode: %s, params: %s' % (mode, params)) + try: + if mode == 'play': + result = self.process_play(params.get('id'), + params.get('dbid')) + elif mode == 'companion': + result = self.process_companion() + except: + log.error('Error encountered for mode %s, params %s' + % (mode, params)) + import traceback + log.error(traceback.format_exc()) + # Let default.py know! + pickle_me(None) + else: + pickle_me(result) + + def run(self): + queue = self.mgr.monitor_kodi_play.playback_queue + log.info("----===## Starting Playback_Starter ##===----") + while True: + item = queue.get() + if item is None: + # Need to shutdown - initiated by monitor_kodi_play + break + else: + self.triage(item) + queue.task_done() + log.info("----===## Playback_Starter stopped ##===----") diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 8205ff3e..4a17cde1 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -3,20 +3,25 @@ ############################################################################### import logging -import sys from urllib import urlencode +from threading import Thread -import xbmc +from xbmc import getCondVisibility, Player import xbmcgui -import xbmcplugin import playutils as putils -import playlist from utils import window, settings, tryEncode, tryDecode import downloadutils -import PlexAPI -import PlexFunctions as PF +from PlexAPI import API +from PlexFunctions import GetPlexPlaylist, KODITYPE_FROM_PLEXTYPE, \ + PLEX_TYPE_CLIP, PLEX_TYPE_MOVIE +from PKC_listitem import PKC_ListItem as ListItem +from playlist_func import add_item_to_kodi_playlist, \ + get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \ + add_listitem_to_playlist, remove_from_Kodi_playlist +from pickler import Playback_Successful +from plexdb_functions import Get_Plex_DB ############################################################################### @@ -29,74 +34,72 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item): + def __init__(self, xml, playqueue): + self.xml = xml + self.playqueue = playqueue - self.item = item - self.API = PlexAPI.API(item) - - self.userid = window('currUserId') - self.server = window('pms_server') - - if self.API.getType() == 'track': - self.pl = playlist.Playlist(typus='music') - else: - self.pl = playlist.Playlist(typus='video') - - def play(self, itemid, dbid=None): - - item = self.item - # Hack to get only existing entry in PMS response for THIS instance of - # playbackutils :-) - self.API = PlexAPI.API(item[0]) - API = self.API - listitem = xbmcgui.ListItem() - playutils = putils.PlayUtils(item[0]) - - log.info("Play called.") + def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): + """ + plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting + to the PMS + """ + log.info("Playbackutils called") + item = self.xml[0] + api = API(item) + playqueue = self.playqueue + xml = None + result = Playback_Successful() + listitem = ListItem() + playutils = putils.PlayUtils(item) playurl = playutils.getPlayUrl() if not playurl: - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) + log.error('No playurl found, aborting') + return - if dbid in (None, 'plextrailer', 'plexnode'): - # Item is not in Kodi database, is a trailer or plex redirect + if kodi_id in (None, 'plextrailer', 'plexnode'): + # Item is not in Kodi database, is a trailer/clip or plex redirect # e.g. plex.tv watch later - API.CreateListItemFromPlexItem(listitem) - self.setArtwork(listitem) - if dbid == 'plexnode': + api.CreateListItemFromPlexItem(listitem) + api.set_listitem_artwork(listitem) + if kodi_id == 'plexnode': # Need to get yet another xml to get final url - window('emby_%s.playmethod' % playurl, clear=True) + window('plex_%s.playmethod' % playurl, clear=True) xml = downloadutils.DownloadUtils().downloadUrl( - '{server}%s' % item[0][0][0].attrib.get('key')) - if xml in (None, 401): + '{server}%s' % item[0][0].attrib.get('key')) + try: + xml[0].attrib + except (TypeError, AttributeError): log.error('Could not download %s' - % item[0][0][0].attrib.get('key')) - return xbmcplugin.setResolvedUrl( - int(sys.argv[1]), False, listitem) + % item[0][0].attrib.get('key')) + return playurl = tryEncode(xml[0].attrib.get('key')) - window('emby_%s.playmethod' % playurl, value='DirectStream') + window('plex_%s.playmethod' % playurl, value='DirectStream') - playmethod = window('emby_%s.playmethod' % playurl) + playmethod = window('plex_%s.playmethod' % playurl) if playmethod == "Transcode": - window('emby_%s.playmethod' % playurl, clear=True) + window('plex_%s.playmethod' % playurl, clear=True) playurl = tryEncode(playutils.audioSubsPref( listitem, tryDecode(playurl))) - window('emby_%s.playmethod' % playurl, "Transcode") + window('plex_%s.playmethod' % playurl, "Transcode") listitem.setPath(playurl) - self.setProperties(playurl, listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + api.set_playback_win_props(playurl, listitem) + result.listitem = listitem + return result - ############### ORGANIZE CURRENT PLAYLIST ################ + kodi_type = KODITYPE_FROM_PLEXTYPE[api.getType()] + kodi_id = int(kodi_id) + + # ORGANIZE CURRENT PLAYLIST ################ contextmenu_play = window('plex_contextplay') == 'true' window('plex_contextplay', clear=True) - homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - kodiPl = self.pl.playlist - sizePlaylist = kodiPl.size() + homeScreen = getCondVisibility('Window.IsActive(home)') + sizePlaylist = len(playqueue.items) if contextmenu_play: # Need to start with the items we're inserting here startPos = sizePlaylist else: # Can return -1 - startPos = max(kodiPl.getposition(), 0) + startPos = max(playqueue.kodi_pl.getposition(), 0) self.currentPosition = startPos propertiesPlayback = window('plex_playbackProps') == "true" @@ -108,72 +111,106 @@ class PlaybackUtils(): log.info("Playlist plugin position: %s" % self.currentPosition) log.info("Playlist size: %s" % sizePlaylist) - ############### RESUME POINT ################ - seektime, runtime = API.getRuntime() + # RESUME POINT ################ + seektime, runtime = api.getRuntime() + if window('plex_customplaylist.seektime'): + # Already got seektime, e.g. from playqueue & Plex companion + seektime = int(window('plex_customplaylist.seektime')) # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. if not propertiesPlayback: - window('plex_playbackProps', value="true") log.info("Setting up properties in playlist.") + # Where will the player need to start? + # Do we need to get trailers? + trailers = False + if (api.getType() == PLEX_TYPE_MOVIE and + not seektime and + sizePlaylist < 2 and + settings('enableCinema') == "true"): + if settings('askCinema') == "true": + trailers = xbmcgui.Dialog().yesno( + addonName, + "Play trailers?") + else: + trailers = True + # Post to the PMS. REUSE THE PLAYQUEUE! + xml = GetPlexPlaylist( + plex_id, + plex_lib_UUID, + mediatype=api.getType(), + trailers=trailers) + get_playlist_details_from_xml(playqueue, xml=xml) - if (not homeScreen and not seektime and + if (not homeScreen and not seektime and sizePlaylist < 2 and window('plex_customplaylist') != "true" and not contextmenu_play): + # Need to add a dummy file because the first item will fail log.debug("Adding dummy file to playlist.") dummyPlaylist = True - kodiPl.add(playurl, listitem, index=startPos) + add_listitem_to_Kodi_playlist( + playqueue, + startPos, + xbmcgui.ListItem(), + playurl, + xml[0]) # Remove the original item from playlist - self.pl.removefromPlaylist(startPos+1) - # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insertintoPlaylist( + remove_from_Kodi_playlist( + playqueue, + startPos+1) + # Readd the original item to playlist - via jsonrpc so we have + # full metadata + add_item_to_kodi_playlist( + playqueue, self.currentPosition+1, - dbid, - PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + kodi_id=kodi_id, + kodi_type=kodi_type, + file=playurl) self.currentPosition += 1 - ############### -- CHECK FOR INTROS ################ - if (settings('enableCinema') == "true" and not seektime): - # if we have any play them when the movie/show is not being resumed - xml = PF.GetPlexPlaylist( - itemid, - item.attrib.get('librarySectionUUID'), - mediatype=API.getType()) - introsPlaylist = self.AddTrailers(xml) - - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## + # -- ADD TRAILERS ################ + if trailers: + for i, item in enumerate(xml): + if i == len(xml) - 1: + # Don't add the main movie itself + break + self.add_trailer(item) + introsPlaylist = True + # -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## if homeScreen and not seektime and not sizePlaylist: # Extend our current playlist with the actual item to play # only if there's no playlist first log.info("Adding main item to playlist.") - self.pl.addtoPlaylist( - dbid, - PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + add_item_to_kodi_playlist( + playqueue, + self.currentPosition, + kodi_id, + kodi_type) elif contextmenu_play: if window('useDirectPaths') == 'true': # Cannot add via JSON with full metadata because then we # Would be using the direct path log.debug("Adding contextmenu item for direct paths") - if window('emby_%s.playmethod' % playurl) == "Transcode": - window('emby_%s.playmethod' % playurl, + if window('plex_%s.playmethod' % playurl) == "Transcode": + window('plex_%s.playmethod' % playurl, clear=True) playurl = tryEncode(playutils.audioSubsPref( listitem, tryDecode(playurl))) - window('emby_%s.playmethod' % playurl, + window('plex_%s.playmethod' % playurl, value="Transcode") - self.setProperties(playurl, listitem) - self.setArtwork(listitem) - API.CreateListItemFromPlexItem(listitem) + api.CreateListItemFromPlexItem(listitem) + api.set_playback_win_props(playurl, listitem) + api.set_listitem_artwork(listitem) kodiPl.add(playurl, listitem, index=self.currentPosition+1) else: # Full metadata self.pl.insertintoPlaylist( self.currentPosition+1, - dbid, - PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + kodi_id, + kodi_type) self.currentPosition += 1 if seektime: window('plex_customplaylist.seektime', value=str(seektime)) @@ -181,177 +218,145 @@ class PlaybackUtils(): # Ensure that additional parts are played after the main item self.currentPosition += 1 - ############### -- CHECK FOR ADDITIONAL PARTS ################ - if len(item[0][0]) > 1: - # Only add to the playlist after intros have played - for counter, part in enumerate(item[0][0]): - # Never add first part - if counter == 0: - continue - # Set listitem and properties for each additional parts - API.setPartNumber(counter) - additionalListItem = xbmcgui.ListItem() - additionalPlayurl = playutils.getPlayUrl( - partNumber=counter) - log.debug("Adding additional part: %s, url: %s" - % (counter, additionalPlayurl)) - - self.setProperties(additionalPlayurl, additionalListItem) - self.setArtwork(additionalListItem) - # NEW to Plex - API.CreateListItemFromPlexItem(additionalListItem) - - kodiPl.add(additionalPlayurl, additionalListItem, - index=self.currentPosition) - self.pl.verifyPlaylist() - self.currentPosition += 1 - API.setPartNumber(0) + # -- CHECK FOR ADDITIONAL PARTS ################ + if len(item[0]) > 1: + self.add_part(item, api, kodi_id, kodi_type) if dummyPlaylist: # Added a dummy file to the playlist, # because the first item is going to fail automatically. log.info("Processed as a playlist. First item is skipped.") - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) + # Delete the item that's gonna fail! + del playqueue.items[startPos] + # Don't attach listitem + return result # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: log.debug("Resetting properties playback flag.") window('plex_playbackProps', clear=True) - #self.pl.verifyPlaylist() - ########## SETUP MAIN ITEM ########## + # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref - if (window('emby_%s.playmethod' % playurl) == "Transcode" and + if (window('plex_%s.playmethod' % playurl) == "Transcode" and not contextmenu_play): - window('emby_%s.playmethod' % playurl, clear=True) + window('plex_%s.playmethod' % playurl, clear=True) playurl = tryEncode(playutils.audioSubsPref( listitem, tryDecode(playurl))) - window('emby_%s.playmethod' % playurl, value="Transcode") + window('plex_%s.playmethod' % playurl, value="Transcode") listitem.setPath(playurl) - self.setProperties(playurl, listitem) + api.set_playback_win_props(playurl, listitem) + api.set_listitem_artwork(listitem) - ############### PLAYBACK ################ + # PLAYBACK ################ if (homeScreen and seektime and window('plex_customplaylist') != "true" and not contextmenu_play): - log.info("Play as a widget item.") - API.CreateListItemFromPlexItem(listitem) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + log.info("Play as a widget item") + api.CreateListItemFromPlexItem(listitem) + result.listitem = listitem + return result elif ((introsPlaylist and window('plex_customplaylist') == "true") or (homeScreen and not sizePlaylist) or contextmenu_play): # Playlist was created just now, play it. # Contextmenu plays always need this - log.info("Play playlist.") - xbmcplugin.endOfDirectory(int(sys.argv[1]), True, False, False) - xbmc.Player().play(kodiPl, startpos=startPos) - + log.info("Play playlist from starting position %s" % startPos) + # Need a separate thread because Player won't return in time + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, None, False, startPos)) + thread.setDaemon(True) + thread.start() + # Don't attach listitem + return result else: - log.info("Play as a regular item.") - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + log.info("Play as a regular item") + result.listitem = listitem + return result - def AddTrailers(self, xml): + def play_all(self): """ - Adds trailers to a movie, if applicable. Returns True if trailers were - added + Play all items contained in the xml passed in. Called by Plex Companion """ - # Failure when downloading trailer playQueue - if xml in (None, 401): - return False - # Failure when getting trailers, e.g. when no plex pass - if xml.attrib.get('size') == '1': - return False - - if settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno(addonName, "Play trailers?") - if not resp: - # User selected to not play trailers - log.info("Skip trailers.") - return False + log.info("Playbackutils play_all called") + window('plex_playbackProps', value="true") + self.currentPosition = 0 + for item in self.xml: + api = API(item) + if api.getType() == PLEX_TYPE_CLIP: + self.add_trailer(item) + continue + with Get_Plex_DB() as plex_db: + db_item = plex_db.getItem_byId(api.getRatingKey()) + try: + add_item_to_kodi_playlist(self.playqueue, + self.currentPosition, + kodi_id=db_item[0], + kodi_type=db_item[4]) + self.currentPosition += 1 + if len(item[0]) > 1: + self.add_part(item, + api, + db_item[0], + db_item[4]) + except TypeError: + # Item not in Kodi DB + self.add_trailer(item) + continue + def add_trailer(self, item): # Playurl needs to point back so we can get metadata! - path = "plugin://plugin.video.plexkodiconnect.movies/" + path = "plugin://plugin.video.plexkodiconnect/movies/" params = { 'mode': "play", 'dbid': 'plextrailer' } - for counter, intro in enumerate(xml): - # Don't process the last item - it's the original movie - if counter == len(xml)-1: - break - # The server randomly returns intros, process them. - # introListItem = xbmcgui.ListItem() - # introPlayurl = putils.PlayUtils(intro).getPlayUrl() - introAPI = PlexAPI.API(intro) - params['id'] = introAPI.getRatingKey() - params['filename'] = introAPI.getKey() - introPlayurl = path + '?' + urlencode(params) - log.info("Adding Intro: %s" % introPlayurl) + introAPI = API(item) + listitem = introAPI.CreateListItemFromPlexItem() + params['id'] = introAPI.getRatingKey() + params['filename'] = introAPI.getKey() + introPlayurl = path + '?' + urlencode(params) + introAPI.set_listitem_artwork(listitem) + # Overwrite the Plex url + listitem.setPath(introPlayurl) + log.info("Adding Plex trailer: %s" % introPlayurl) + add_listitem_to_Kodi_playlist( + self.playqueue, + self.currentPosition, + listitem, + introPlayurl, + xml_video_element=item) + self.currentPosition += 1 - self.pl.insertintoPlaylist(self.currentPosition, url=introPlayurl) + def add_part(self, item, api, kodi_id, kodi_type): + """ + Adds an additional part to the playlist + """ + # Only add to the playlist after intros have played + for counter, part in enumerate(item[0]): + # Never add first part + if counter == 0: + continue + # Set listitem and properties for each additional parts + api.setPartNumber(counter) + additionalListItem = xbmcgui.ListItem() + playutils = putils.PlayUtils(item) + additionalPlayurl = playutils.getPlayUrl( + partNumber=counter) + log.debug("Adding additional part: %s, url: %s" + % (counter, additionalPlayurl)) + api.CreateListItemFromPlexItem(additionalListItem) + api.set_playback_win_props(additionalPlayurl, + additionalListItem) + api.set_listitem_artwork(additionalListItem) + add_listitem_to_playlist( + self.playqueue, + self.currentPosition, + additionalListItem, + kodi_id=kodi_id, + kodi_type=kodi_type, + plex_id=api.getRatingKey(), + file=additionalPlayurl) self.currentPosition += 1 - - return True - - def setProperties(self, playurl, listitem): - # Set all properties necessary for plugin path playback - itemid = self.API.getRatingKey() - itemtype = self.API.getType() - userdata = self.API.getUserData() - - embyitem = "emby_%s" % playurl - window('%s.runtime' % embyitem, value=str(userdata['Runtime'])) - window('%s.type' % embyitem, value=itemtype) - window('%s.itemid' % embyitem, value=itemid) - window('%s.playcount' % embyitem, value=str(userdata['PlayCount'])) - - if itemtype == "episode": - window('%s.refreshid' % embyitem, - value=self.API.getParentRatingKey()) - else: - window('%s.refreshid' % embyitem, value=itemid) - - # Append external subtitles to stream - playmethod = window('%s.playmethod' % embyitem) - if playmethod in ("DirectStream", "DirectPlay"): - subtitles = self.API.externalSubs(playurl) - listitem.setSubtitles(subtitles) - - self.setArtwork(listitem) - - def setArtwork(self, listItem): - allartwork = self.API.getAllArtwork(parentInfo=True) - arttypes = { - 'poster': "Primary", - 'tvshow.poster': "Thumb", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearart': "Primary", - 'tvshow.clearart': "Primary", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Backdrop", - "banner": "Banner" - } - for arttype in arttypes: - art = arttypes[arttype] - if art == "Backdrop": - try: - # Backdrop is a list, grab the first backdrop - self.setArtProp(listItem, arttype, allartwork[art][0]) - except: - pass - else: - self.setArtProp(listItem, arttype, allartwork[art]) - - def setArtProp(self, listItem, arttype, path): - if arttype in ( - 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators'): - listItem.setProperty(arttype, path) - else: - listItem.setArt({arttype: path}) + api.setPartNumber(0) diff --git a/resources/lib/player.py b/resources/lib/player.py index 06b6b0f8..14e4a3af 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -11,8 +11,9 @@ from utils import window, settings, language as lang, DateToKodi, \ getUnixTimestamp import clientinfo import downloadutils -import embydb_functions as embydb +import plexdb_functions as plexdb import kodidb_functions as kodidb +from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE ############################################################################### @@ -76,11 +77,11 @@ class Player(xbmc.Player): self.currentFile = currentFile window('plex_lastPlayedFiled', value=currentFile) # We may need to wait for info to be set in kodi monitor - itemId = window("emby_%s.itemid" % currentFile) + itemId = window("plex_%s.itemid" % currentFile) count = 0 while not itemId: xbmc.sleep(200) - itemId = window("emby_%s.itemid" % currentFile) + itemId = window("plex_%s.itemid" % currentFile) if count == 5: log.warn("Could not find itemId, cancelling playback report!") return @@ -88,16 +89,16 @@ class Player(xbmc.Player): log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) - embyitem = "emby_%s" % currentFile - runtime = window("%s.runtime" % embyitem) - refresh_id = window("%s.refreshid" % embyitem) - playMethod = window("%s.playmethod" % embyitem) - itemType = window("%s.type" % embyitem) + plexitem = "plex_%s" % currentFile + runtime = window("%s.runtime" % plexitem) + refresh_id = window("%s.refreshid" % plexitem) + playMethod = window("%s.playmethod" % plexitem) + itemType = window("%s.type" % plexitem) try: - playcount = int(window("%s.playcount" % embyitem)) + playcount = int(window("%s.playcount" % plexitem)) except ValueError: playcount = 0 - window('emby_skipWatched%s' % itemId, value="true") + window('plex_skipWatched%s' % itemId, value="true") log.debug("Playing itemtype is: %s" % itemType) @@ -134,7 +135,7 @@ class Player(xbmc.Player): volume = result.get('volume') muted = result.get('muted') - # Postdata structure to send to Emby server + # Postdata structure to send to plex server url = "{server}/:/timeline?" postdata = { @@ -154,7 +155,7 @@ class Player(xbmc.Player): postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) else: - # Get the current kodi audio and subtitles and convert to Emby equivalent + # Get the current kodi audio and subtitles and convert to plex equivalent tracks_query = { "jsonrpc": "2.0", "id": 1, @@ -190,9 +191,9 @@ class Player(xbmc.Player): # Postdata for the subtitles if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: - # Number of audiotracks to help get Emby Index + # Number of audiotracks to help get plex Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % embyitem) + mapping = window("%s.indexMapping" % plexitem) if mapping: # Set in playbackutils.py @@ -229,10 +230,10 @@ class Player(xbmc.Player): log.error('Could not get kodi runtime, setting to zero') runtime = 0 - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byId(itemId) + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(itemId) try: - fileid = emby_dbitem[1] + fileid = plex_dbitem[1] except TypeError: log.info("Could not find fileid in plex db.") fileid = None @@ -338,7 +339,7 @@ class Player(xbmc.Player): playMethod = data['playmethod'] # Prevent manually mark as watched in Kodi monitor - window('emby_skipWatched%s' % itemid, value="true") + window('plex_skipWatched%s' % itemid, value="true") if currentPosition and runtime: try: @@ -353,7 +354,7 @@ class Player(xbmc.Player): if percentComplete >= markPlayed: # Tell Kodi that we've finished watching (Plex knows) if (data['fileid'] is not None and - data['itemType'] in ('movie', 'episode')): + data['itemType'] in (KODI_TYPE_MOVIE, KODI_TYPE_EPISODE)): with kodidb.GetKodiDB('video') as kodi_db: kodi_db.addPlaystate( data['fileid'], @@ -391,13 +392,13 @@ class Player(xbmc.Player): # Clean the WINDOW properties for filename in self.played_info: cleanup = ( - 'emby_%s.itemid' % filename, - 'emby_%s.runtime' % filename, - 'emby_%s.refreshid' % filename, - 'emby_%s.playmethod' % filename, - 'emby_%s.type' % filename, - 'emby_%s.runtime' % filename, - 'emby_%s.playcount' % filename, + 'plex_%s.itemid' % filename, + 'plex_%s.runtime' % filename, + 'plex_%s.refreshid' % filename, + 'plex_%s.playmethod' % filename, + 'plex_%s.type' % filename, + 'plex_%s.runtime' % filename, + 'plex_%s.playcount' % filename, 'plex_%s.playlistPosition' % filename ) for item in cleanup: diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py deleted file mode 100644 index f6886a9c..00000000 --- a/resources/lib/playlist.py +++ /dev/null @@ -1,337 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################### - -import logging -import json -from urllib import urlencode -from threading import Lock -from functools import wraps - -import xbmc - -import embydb_functions as embydb -from utils import window, tryEncode -import playbackutils -import PlexFunctions -import PlexAPI - -############################################################################### - -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -class lockMethod: - """ - Decorator for class methods to lock hem completely. Same lock is used for - every single decorator and instance used! - - Here only used for Playlist() - """ - lock = Lock() - - @classmethod - def decorate(cls, func): - @wraps(func) - def wrapper(*args, **kwargs): - with cls.lock: - result = func(*args, **kwargs) - return result - return wrapper - - -class Playlist(): - """ - Initiate with Playlist(typus='video' or 'music') - """ - # Borg - multiple instances, shared state - _shared_state = {} - - typus = None - queueId = None - playQueueVersion = None - guid = None - playlistId = None - player = xbmc.Player() - # "interal" PKC playlist - items = [] - - @lockMethod.decorate - def __init__(self, typus=None): - # Borg - self.__dict__ = self._shared_state - - self.userid = window('currUserId') - self.server = window('pms_server') - # Construct the Kodi playlist instance - if self.typus == typus: - return - if typus == 'video': - self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - self.typus = 'video' - log.info('Initiated video playlist') - elif typus == 'music': - self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - self.typus = 'music' - log.info('Initiated music playlist') - else: - self.playlist = None - self.typus = None - log.info('Empty playlist initiated') - if self.playlist is not None: - self.playlistId = self.playlist.getPlayListId() - - @lockMethod.decorate - def getQueueIdFromPosition(self, playlistPosition): - return self.items[playlistPosition]['playQueueItemID'] - - @lockMethod.decorate - def Typus(self, value=None): - if value: - self.typus = value - else: - return self.typus - - @lockMethod.decorate - def PlayQueueVersion(self, value=None): - if value: - self.playQueueVersion = value - else: - return self.playQueueVersion - - @lockMethod.decorate - def QueueId(self, value=None): - if value: - self.queueId = value - else: - return self.queueId - - @lockMethod.decorate - def Guid(self, value=None): - if value: - self.guid = value - else: - return self.guid - - @lockMethod.decorate - def clear(self): - """ - Empties current Kodi playlist and associated variables - """ - log.info('Clearing playlist') - self.playlist.clear() - self.items = [] - self.queueId = None - self.playQueueVersion = None - self.guid = None - - def _initiatePlaylist(self): - log.info('Initiating playlist') - playlist = None - with embydb.GetEmbyDB() as emby_db: - for item in self.items: - itemid = item['plexId'] - embydb_item = emby_db.getItem_byId(itemid) - try: - mediatype = embydb_item[4] - except TypeError: - log.info('Couldnt find item %s in Kodi db' % itemid) - item = PlexFunctions.GetPlexMetadata(itemid) - if item in (None, 401): - log.info('Couldnt find item %s on PMS, trying next' - % itemid) - continue - if PlexAPI.API(item[0]).getType() == 'track': - playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - log.info('Music playlist initiated') - self.typus = 'music' - else: - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - log.info('Video playlist initiated') - self.typus = 'video' - else: - if mediatype == 'song': - playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - log.info('Music playlist initiated') - self.typus = 'music' - else: - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - log.info('Video playlist initiated') - self.typus = 'video' - break - self.playlist = playlist - if self.playlist is not None: - self.playlistId = self.playlist.getPlayListId() - - def _processItems(self, startitem, startPlayer=False): - startpos = None - with embydb.GetEmbyDB() as emby_db: - for pos, item in enumerate(self.items): - kodiId = None - plexId = item['plexId'] - embydb_item = emby_db.getItem_byId(plexId) - try: - kodiId = embydb_item[0] - mediatype = embydb_item[4] - except TypeError: - log.info('Couldnt find item %s in Kodi db' % plexId) - xml = PlexFunctions.GetPlexMetadata(plexId) - if xml in (None, 401): - log.error('Could not download plexId %s' % plexId) - else: - log.debug('Downloaded xml metadata, adding now') - self._addtoPlaylist_xbmc(xml[0]) - else: - # Add to playlist - log.debug("Adding %s PlexId %s, KodiId %s to playlist." - % (mediatype, plexId, kodiId)) - self._addtoPlaylist(kodiId, mediatype) - # Add the kodiId - if kodiId is not None: - item['kodiId'] = str(kodiId) - if (startpos is None and startitem[1] == item[startitem[0]]): - startpos = pos - - if startPlayer is True and len(self.playlist) > 0: - if startpos is not None: - self.player.play(self.playlist, startpos=startpos) - else: - log.info('Never received a starting item for playlist, ' - 'starting with the first entry') - self.player.play(self.playlist) - - @lockMethod.decorate - def playAll(self, items, startitem, offset): - """ - items: list of dicts of the form - { - 'playQueueItemID': Plex playQueueItemID, e.g. '29175' - 'plexId': Plex ratingKey, e.g. '125' - 'kodiId': Kodi's db id of the same item - } - - startitem: tuple (typus, id), where typus is either - 'playQueueItemID' or 'plexId' and id is the corresponding - id as a string - offset: First item's time offset to play in Kodi time (an int) - """ - log.info("---*** PLAY ALL ***---") - log.debug('Startitem: %s, offset: %s, items: %s' - % (startitem, offset, items)) - self.items = items - if self.playlist is None: - self._initiatePlaylist() - if self.playlist is None: - log.error('Could not create playlist, abort') - return - - window('plex_customplaylist', value="true") - if offset != 0: - # Seek to the starting position - window('plex_customplaylist.seektime', str(offset)) - self._processItems(startitem, startPlayer=True) - # Log playlist - self._verifyPlaylist() - log.debug('Internal playlist: %s' % self.items) - - @lockMethod.decorate - def modifyPlaylist(self, itemids): - log.info("---*** MODIFY PLAYLIST ***---") - log.debug("Items: %s" % itemids) - - self._initiatePlaylist(itemids) - self._processItems(itemids, startPlayer=True) - - self._verifyPlaylist() - - @lockMethod.decorate - def addtoPlaylist(self, dbid=None, mediatype=None, url=None): - """ - mediatype: Kodi type: 'movie', 'episode', 'musicvideo', 'artist', - 'album', 'song', 'genre' - """ - self._addtoPlaylist(dbid, mediatype, url) - - def _addtoPlaylist(self, dbid=None, mediatype=None, url=None): - pl = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Add", - 'params': { - 'playlistid': self.playlistId - } - } - if dbid is not None: - pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - pl['params']['item'] = {'file': url} - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) - - def _addtoPlaylist_xbmc(self, item): - API = PlexAPI.API(item) - params = { - 'mode': "play", - 'dbid': 'plextrailer', - 'id': API.getRatingKey(), - 'filename': API.getKey() - } - playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \ - % urlencode(params) - - listitem = API.CreateListItemFromPlexItem() - playbackutils.PlaybackUtils(item).setArtwork(listitem) - - self.playlist.add(playurl, listitem) - - @lockMethod.decorate - def insertintoPlaylist(self, - position, - dbid=None, - mediatype=None, - url=None): - pl = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Insert", - 'params': { - 'playlistid': self.playlistId, - 'position': position - } - } - if dbid is not None: - pl['params']['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - pl['params']['item'] = {'file': url} - - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) - - @lockMethod.decorate - def verifyPlaylist(self): - self._verifyPlaylist() - - def _verifyPlaylist(self): - pl = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.GetItems", - 'params': { - 'playlistid': self.playlistId, - 'properties': ['title', 'file'] - } - } - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) - - @lockMethod.decorate - def removefromPlaylist(self, position): - pl = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Remove", - 'params': { - 'playlistid': self.playlistId, - 'position': position - } - } - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py new file mode 100644 index 00000000..ee2794f5 --- /dev/null +++ b/resources/lib/playlist_func.py @@ -0,0 +1,519 @@ +import logging +from urllib import quote + +import plexdb_functions as plexdb +from downloadutils import DownloadUtils as DU +from utils import JSONRPC, tryEncode +from PlexAPI import API + +############################################################################### + +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + +# kodi_item dict: +# {u'type': u'movie', u'id': 3, 'file': path-to-file} + + +class Playlist_Object_Baseclase(object): + playlistid = None # Kodi playlist ID, [int] + type = None # Kodi type: 'audio', 'video', 'picture' + kodi_pl = None # Kodi xbmc.PlayList object + items = [] # list of PLAYLIST_ITEMS + old_kodi_pl = [] # to store old Kodi JSON result with all pl items + ID = None # Plex id, e.g. playQueueID + version = None # Plex version, [int] + selectedItemID = None + selectedItemOffset = None + shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? + repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? + + def __repr__(self): + answ = "<%s: " % (self.__class__.__name__) + # For some reason, can't use dir directly + answ += "ID: %s, " % self.ID + answ += "items: %s, " % self.items + for key in self.__dict__: + if key not in ("ID", 'items'): + answ += '%s: %s, ' % (key, getattr(self, key)) + return answ[:-2] + ">" + + def clear(self): + """ + Resets the playlist object to an empty playlist + """ + # Clear Kodi playlist object + self.kodi_pl.clear() + self.items = [] + self.old_kodi_pl = [] + self.ID = None + self.version = None + self.selectedItemID = None + self.selectedItemOffset = None + self.shuffled = 0 + self.repeat = 0 + log.debug('Playlist cleared: %s' % self) + + def log_Kodi_playlist(self): + log.debug('Current Kodi playlist: %s' % get_kodi_playlist_items(self)) + + +class Playlist_Object(Playlist_Object_Baseclase): + kind = 'playList' + + +class Playqueue_Object(Playlist_Object_Baseclase): + kind = 'playQueue' + + +class Playlist_Item(object): + ID = None # Plex playlist/playqueue id, e.g. playQueueItemID + plex_id = None # Plex unique item id, "ratingKey" + plex_UUID = None # Plex librarySectionUUID + kodi_id = None # Kodi unique kodi id (unique only within type!) + kodi_type = None # Kodi type: 'movie' + file = None # Path to the item's file + uri = None # Weird Plex uri path involving plex_UUID + guid = None # Weird Plex guid + + def __repr__(self): + answ = "<%s: " % (self.__class__.__name__) + for key in self.__dict__: + answ += '%s: %s, ' % (key, getattr(self, key)) + return answ[:-2] + ">" + + +def playlist_item_from_kodi(kodi_item): + """ + Turns the JSON answer from Kodi into a playlist element + + Supply with data['item'] as returned from Kodi JSON-RPC interface. + kodi_item dict contains keys 'id', 'type', 'file' (if applicable) + """ + item = Playlist_Item() + item.kodi_id = kodi_item.get('id') + if item.kodi_id: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'], + kodi_item['type']) + try: + item.plex_id = plex_dbitem[0] + item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-) + except TypeError: + pass + item.file = kodi_item.get('file') + item.kodi_type = kodi_item.get('type') + if item.plex_id is None: + item.uri = 'library://whatever/item/%s' % quote(item.file, safe='') + else: + # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER + item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % + (item.plex_UUID, item.plex_id)) + return item + + +def playlist_item_from_plex(plex_id): + """ + Returns a playlist element providing the plex_id ("ratingKey") + + Returns a Playlist_Item + """ + item = Playlist_Item() + item.plex_id = plex_id + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(plex_id) + try: + item.kodi_id = plex_dbitem[0] + item.kodi_type = plex_dbitem[4] + except: + raise KeyError('Could not find plex_id %s in database' % plex_id) + return item + + +def playlist_item_from_xml(playlist, xml_video_element): + """ + Returns a playlist element for the playqueue using the Plex xml + """ + item = Playlist_Item() + api = API(xml_video_element) + item.plex_id = api.getRatingKey() + item.ID = xml_video_element.attrib['%sItemID' % playlist.kind] + item.guid = xml_video_element.attrib.get('guid') + if item.plex_id: + with plexdb.Get_Plex_DB() as plex_db: + db_element = plex_db.getItem_byId(item.plex_id) + try: + item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] + except TypeError: + pass + log.debug('Created new playlist item from xml: %s' % item) + return item + + +def _log_xml(xml): + try: + xml.attrib + except AttributeError: + log.error('Did not receive an XML. Answer was: %s' % xml) + else: + from xml.etree.ElementTree import dump + log.error('XML received from the PMS:') + dump(xml) + + +def _get_playListVersion_from_xml(playlist, xml): + """ + Takes a PMS xml as input to overwrite the playlist version (e.g. Plex + playQueueVersion). Returns True if successful, False otherwise + """ + try: + playlist.version = int(xml.attrib['%sVersion' % playlist.kind]) + except (TypeError, AttributeError, KeyError): + log.error('Could not get new playlist Version for playlist %s' + % playlist) + _log_xml(xml) + return False + return True + + +def get_playlist_details_from_xml(playlist, xml): + """ + Takes a PMS xml as input and overwrites all the playlist's details, e.g. + playlist.ID with the XML's playQueueID + """ + try: + playlist.ID = xml.attrib['%sID' % playlist.kind] + playlist.version = xml.attrib['%sVersion' % playlist.kind] + playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] + playlist.selectedItemID = xml.attrib.get( + '%sSelectedItemID' % playlist.kind) + playlist.selectedItemOffset = xml.attrib.get( + '%sSelectedItemOffset' % playlist.kind) + except: + log.error('Could not parse xml answer from PMS for playlist %s' + % playlist) + import traceback + log.error(traceback.format_exc()) + _log_xml(xml) + raise KeyError + log.debug('Updated playlist from xml: %s' % playlist) + + +def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): + """ + Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we + need to fetch a new playqueue + + If an xml is passed in, the playlist will be overwritten with its info + """ + if xml is None: + xml = get_PMS_playlist(playlist, playlist_id) + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not process Plex playlist') + return + # Clear our existing playlist and the associated Kodi playlist + playlist.clear() + # Set new values + get_playlist_details_from_xml(playlist, xml) + for plex_item in xml: + playlist.items.append(add_to_Kodi_playlist(playlist, plex_item)) + + +def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): + """ + Initializes the Plex side without changing the Kodi playlists + + WILL ALSO UPDATE OUR PLAYLISTS + """ + log.debug('Initializing the playlist %s on the Plex side' % playlist) + if plex_id: + item = playlist_item_from_plex(plex_id) + else: + item = playlist_item_from_kodi(kodi_item) + params = { + 'next': 0, + 'type': playlist.type, + 'uri': item.uri + } + xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, + action_type="POST", + parameters=params) + get_playlist_details_from_xml(playlist, xml) + playlist.items.append(item) + log.debug('Initialized the playlist on the Plex side: %s' % playlist) + + +def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, + kodi_type=None, plex_id=None, file=None): + """ + Adds a listitem to both the Kodi and Plex playlist at position pos [int]. + + If file is not None, file will overrule kodi_id! + """ + log.debug('add_listitem_to_playlist. Playlist before add: %s' % playlist) + kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file} + if playlist.ID is None: + init_Plex_playlist(playlist, plex_id, kodi_item) + else: + add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) + if kodi_id is None and playlist.items[pos].kodi_id: + kodi_id = playlist.items[pos].kodi_id + kodi_type = playlist.items[pos].kodi_type + if file is None: + file = playlist.items[pos].file + # Otherwise we double the item! + del playlist.items[pos] + kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file} + add_listitem_to_Kodi_playlist(playlist, + pos, + listitem, + file, + kodi_item=kodi_item) + + +def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, + plex_id=None, file=None): + """ + Adds an item to BOTH the Kodi and Plex playlist at position pos [int] + """ + log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist) + kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file} + if playlist.ID is None: + init_Plex_playlist(playlist, plex_id, kodi_item) + else: + add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) + kodi_id = playlist.items[pos].kodi_id + kodi_type = playlist.items[pos].kodi_type + file = playlist.items[pos].file + add_item_to_kodi_playlist(playlist, pos, kodi_id, kodi_type, file) + + +def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): + """ + Adds a new item to the playlist at position pos [int] only on the Plex + side of things (e.g. because the user changed the Kodi side) + + WILL ALSO UPDATE OUR PLAYLISTS + """ + log.debug('Adding new item plex_id: %s, kodi_item: %s on the Plex side at ' + 'position %s for %s' % (plex_id, kodi_item, pos, playlist)) + if plex_id: + item = playlist_item_from_plex(plex_id) + else: + item = playlist_item_from_kodi(kodi_item) + url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri) + # Will always put the new item at the end of the Plex playlist + xml = DU().downloadUrl(url, action_type="PUT") + try: + item.ID = xml[-1].attrib['%sItemID' % playlist.kind] + except IndexError: + log.info('Could not get playlist children. Adding a dummy') + except (TypeError, AttributeError, KeyError): + log.error('Could not add item %s to playlist %s' + % (kodi_item, playlist)) + _log_xml(xml) + return + # Get the guid for this item + for plex_item in xml: + if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: + item.guid = plex_item.attrib['guid'] + playlist.items.append(item) + if pos == len(playlist.items) - 1: + # Item was added at the end + _get_playListVersion_from_xml(playlist, xml) + else: + # Move the new item to the correct position + move_playlist_item(playlist, + len(playlist.items) - 1, + pos) + log.debug('Successfully added item on the Plex side: %s' % playlist) + + +def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, + file=None): + """ + Adds an item to the KODI playlist only + + WILL ALSO UPDATE OUR PLAYLISTS + """ + log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi ' + 'only at position %s for %s' + % (kodi_id, kodi_type, file, pos, playlist)) + params = { + 'playlistid': playlist.playlistid, + 'position': pos + } + if kodi_id is not None: + params['item'] = {'%sid' % kodi_type: int(kodi_id)} + else: + params['item'] = {'file': file} + log.debug(JSONRPC('Playlist.Insert').execute(params)) + playlist.items.insert(pos, playlist_item_from_kodi( + {'id': kodi_id, 'type': kodi_type, 'file': file})) + + +def move_playlist_item(playlist, before_pos, after_pos): + """ + Moves playlist item from before_pos [int] to after_pos [int] for Plex only. + + WILL ALSO CHANGE OUR PLAYLISTS + """ + log.debug('Moving item from %s to %s on the Plex side for %s' + % (before_pos, after_pos, playlist)) + if after_pos == 0: + url = "{server}/%ss/%s/items/%s/move?after=0" % \ + (playlist.kind, + playlist.ID, + playlist.items[before_pos].ID) + else: + url = "{server}/%ss/%s/items/%s/move?after=%s" % \ + (playlist.kind, + playlist.ID, + playlist.items[before_pos].ID, + playlist.items[after_pos - 1].ID) + xml = DU().downloadUrl(url, action_type="PUT") + # We need to increment the playlistVersion + _get_playListVersion_from_xml(playlist, xml) + # Move our item's position in our internal playlist + playlist.items.insert(after_pos, playlist.items.pop(before_pos)) + log.debug('Done moving for %s' % playlist) + + +def get_PMS_playlist(playlist, playlist_id=None): + """ + Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we + need to fetch a new playlist + + Returns None if something went wrong + """ + playlist_id = playlist_id if playlist_id else playlist.ID + xml = DU().downloadUrl( + "{server}/%ss/%s" % (playlist.kind, playlist_id), + headerOptions={'Accept': 'application/xml'}) + try: + xml.attrib['%sID' % playlist.kind] + except (AttributeError, KeyError): + xml = None + return xml + + +def refresh_playlist_from_PMS(playlist): + """ + Only updates the selected item from the PMS side (e.g. + playQueueSelectedItemID). Will NOT check whether items still make sense. + """ + xml = get_PMS_playlist(playlist) + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not download Plex playlist.') + return + get_playlist_details_from_xml(playlist, xml) + + +def delete_playlist_item_from_PMS(playlist, pos): + """ + Delete the item at position pos [int] on the Plex side and our playlists + """ + log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist)) + xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" % + (playlist.kind, + playlist.ID, + playlist.items[pos].ID, + playlist.repeat), + action_type="DELETE") + _get_playListVersion_from_xml(playlist, xml) + del playlist.items[pos] + + +def get_kodi_playlist_items(playlist): + """ + Returns a list of the current Kodi playlist items using JSON + + E.g.: + [{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file': + u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}] + """ + answ = JSONRPC('Playlist.GetItems').execute({ + 'playlistid': playlist.playlistid, + 'properties': ["title", "file"] + }) + try: + answ = answ['result']['items'] + except KeyError: + answ = [] + return answ + + +def get_kodi_playqueues(): + """ + Example return: [{u'playlistid': 0, u'type': u'audio'}, + {u'playlistid': 1, u'type': u'video'}, + {u'playlistid': 2, u'type': u'picture'}] + """ + queues = JSONRPC('Playlist.GetPlaylists').execute() + try: + queues = queues['result'] + except KeyError: + raise KeyError('Could not get Kodi playqueues. JSON Result was: %s' + % queues) + return queues + + +# Functions operating on the Kodi playlist objects ########## + +def add_to_Kodi_playlist(playlist, xml_video_element): + """ + Adds a new item to the Kodi playlist via JSON (at the end of the playlist). + Pass in the PMS xml's video element (one level underneath MediaContainer). + + Returns a Playlist_Item + """ + item = playlist_item_from_xml(playlist, xml_video_element) + params = { + 'playlistid': playlist.playlistid + } + if item.kodi_id: + params['item'] = {'%sid' % item.kodi_type: item.kodi_id} + else: + params['item'] = {'file': tryEncode(item.file)} + log.debug(JSONRPC('Playlist.Add').execute(params)) + return item + + +def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, + xml_video_element=None, kodi_item=None): + """ + Adds an xbmc listitem to the Kodi playlist.xml_video_element + + WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS + """ + log.debug('Insert listitem at position %s for Kodi only for %s' + % (pos, playlist)) + # Add the item into Kodi playlist + playlist.kodi_pl.add(file, listitem, index=pos) + # We need to add this to our internal queue as well + if xml_video_element is not None: + item = playlist_item_from_xml(playlist, xml_video_element) + item.file = file + else: + item = playlist_item_from_kodi(kodi_item) + playlist.items.insert(pos, item) + log.debug('Done inserting for %s' % playlist) + + +def remove_from_Kodi_playlist(playlist, pos): + """ + Removes the item at position pos from the Kodi playlist using JSON. + + WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS + """ + log.debug('Removing position %s from Kodi only from %s' % (pos, playlist)) + log.debug(JSONRPC('Playlist.Remove').execute({ + 'playlistid': playlist.playlistid, + 'position': pos + })) + del playlist.items[pos] diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py new file mode 100644 index 00000000..cf87218e --- /dev/null +++ b/resources/lib/playqueue.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import RLock, Thread + +from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO + +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend +import playlist_func as PL +from PlexFunctions import ConvertPlexToKodiTime +from playbackutils import PlaybackUtils + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +# Lock used for playqueue manipulations +lock = RLock() +############################################################################### + + +@ThreadMethodsAdditionalSuspend('plex_serverStatus') +@ThreadMethods +class Playqueue(Thread): + """ + Monitors Kodi's playqueues for changes on the Kodi side + """ + # Borg - multiple instances, shared state + __shared_state = {} + playqueues = None + + def __init__(self, callback=None): + self.__dict__ = self.__shared_state + if self.playqueues is not None: + return + self.mgr = callback + + # Initialize Kodi playqueues + with lock: + self.playqueues = [] + for queue in PL.get_kodi_playqueues(): + playqueue = PL.Playqueue_Object() + playqueue.playlistid = queue['playlistid'] + playqueue.type = queue['type'] + # Initialize each Kodi playlist + if playqueue.type == 'audio': + playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC) + elif playqueue.type == 'video': + playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) + else: + # Currently, only video or audio playqueues available + playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) + self.playqueues.append(playqueue) + # sort the list by their playlistid, just in case + self.playqueues = sorted( + self.playqueues, key=lambda i: i.playlistid) + log.debug('Initialized the Kodi play queues: %s' % self.playqueues) + Thread.__init__(self) + + def get_playqueue_from_type(self, typus): + """ + Returns the playqueue according to the typus ('video', 'audio', + 'picture') passed in + """ + with lock: + for playqueue in self.playqueues: + if playqueue.type == typus: + break + else: + raise ValueError('Wrong playlist type passed in: %s' % typus) + return playqueue + + def update_playqueue_from_PMS(self, + playqueue, + playqueue_id=None, + repeat=None, + offset=None): + """ + Completely updates the Kodi playqueue with the new Plex playqueue. Pass + in playqueue_id if we need to fetch a new playqueue + + repeat = 0, 1, 2 + offset = time offset in Plextime (milliseconds) + """ + log.info('New playqueue %s received from Plex companion with offset ' + '%s, repeat %s' % (playqueue_id, offset, repeat)) + with lock: + xml = PL.get_PMS_playlist(playqueue, playqueue_id) + if xml is None: + log.error('Could not get playqueue ID %s' % playqueue_id) + return + playqueue.clear() + PL.get_playlist_details_from_xml(playqueue, xml) + PlaybackUtils(xml, playqueue).play_all() + playqueue.repeat = 0 if not repeat else int(repeat) + window('plex_customplaylist', value="true") + if offset not in (None, "0"): + window('plex_customplaylist.seektime', + str(ConvertPlexToKodiTime(offset))) + for startpos, item in enumerate(playqueue.items): + if item.ID == playqueue.selectedItemID: + break + else: + startpos = 0 + # Start playback. Player does not return in time + log.debug('Playqueues after Plex Companion update are now: %s' + % self.playqueues) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, + None, + False, + startpos)) + thread.setDaemon(True) + thread.start() + + def _compare_playqueues(self, playqueue, new): + """ + Used to poll the Kodi playqueue and update the Plex playqueue if needed + """ + old = list(playqueue.items) + index = list(range(0, len(old))) + log.debug('Comparing new Kodi playqueue %s with our play queue %s' + % (new, old)) + for i, new_item in enumerate(new): + for j, old_item in enumerate(old): + if self.threadStopped(): + # Chances are that we got an empty Kodi playlist due to + # Kodi exit + return + if new_item.get('id') is None: + identical = old_item.file == new_item['file'] + else: + identical = (old_item.kodi_id == new_item['id'] and + old_item.kodi_type == new_item['type']) + if j == 0 and identical: + del old[j], index[j] + break + elif identical: + log.debug('Detected playqueue item %s moved to position %s' + % (i+j, i)) + PL.move_playlist_item(playqueue, i + j, i) + del old[j], index[j] + break + else: + log.debug('Detected new Kodi element at position %s: %s ' + % (i, new_item)) + if playqueue.ID is None: + PL.init_Plex_playlist(playqueue, + kodi_item=new_item) + else: + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + index.insert(i, i) + for j in range(i+1, len(index)): + index[j] += 1 + for i in reversed(index): + log.debug('Detected deletion of playqueue element at pos %s' % i) + PL.delete_playlist_item_from_PMS(playqueue, i) + log.debug('Done comparing playqueues') + + def run(self): + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended + log.info("----===## Starting PlayQueue client ##===----") + # Initialize the playqueues, if Kodi already got items in them + for playqueue in self.playqueues: + for i, item in enumerate(PL.get_kodi_playlist_items(playqueue)): + if i == 0: + PL.init_Plex_playlist(playqueue, kodi_item=item) + else: + PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item) + while not threadStopped(): + while threadSuspended(): + if threadStopped(): + break + sleep(1000) + with lock: + for playqueue in self.playqueues: + kodi_playqueue = PL.get_kodi_playlist_items(playqueue) + if playqueue.old_kodi_pl != kodi_playqueue: + # compare old and new playqueue + self._compare_playqueues(playqueue, kodi_playqueue) + playqueue.old_kodi_pl = list(kodi_playqueue) + sleep(50) + log.info("----===## PlayQueue client stopped ##===----") diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 254c33a1..af6d6b8e 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -45,14 +45,14 @@ class PlayUtils(): log.info("File is direct playing.") playurl = tryEncode(playurl) # Set playmethod property - window('emby_%s.playmethod' % playurl, "DirectPlay") + window('plex_%s.playmethod' % playurl, "DirectPlay") elif self.isDirectStream(): log.info("File is direct streaming.") playurl = tryEncode( self.API.getTranscodeVideoPath('DirectStream')) # Set playmethod property - window('emby_%s.playmethod' % playurl, "DirectStream") + window('plex_%s.playmethod' % playurl, "DirectStream") else: log.info("File is transcoding.") @@ -64,7 +64,7 @@ class PlayUtils(): 'videoQuality': '100' })) # Set playmethod property - window('emby_%s.playmethod' % playurl, value="Transcode") + window('plex_%s.playmethod' % playurl, value="Transcode") log.info("The playurl is: %s" % playurl) return playurl diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index 443b1c8f..5885253d 100644 --- a/resources/lib/plexbmchelper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -5,7 +5,7 @@ import string import xbmc -import embydb_functions as embydb +import plexdb_functions as plexdb ############################################################################### @@ -146,11 +146,11 @@ class jsonClass(): def skipTo(self, plexId, typus): # playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus))) # playerId = self. - with embydb.GetEmbyDB() as emby_db: - embydb_item = emby_db.getItem_byId(plexId) + with plexdb.Get_Plex_DB() as plex_db: + plexdb_item = plex_db.getItem_byId(plexId) try: - dbid = embydb_item[0] - mediatype = embydb_item[4] + dbid = plexdb_item[0] + mediatype = plexdb_item[4] except TypeError: log.info('Couldnt find item %s in Kodi db' % plexId) return @@ -163,7 +163,7 @@ class jsonClass(): "Access-Control-Allow-Origin": "*", "X-Plex-Version": self.settings['version'], "X-Plex-Client-Identifier": self.settings['uuid'], - "X-Plex-Provides": "player", + "X-Plex-Provides": "client,controller,player", "X-Plex-Product": "PlexKodiConnect", "X-Plex-Device-Name": self.settings['client_name'], "X-Plex-Platform": "Kodi", diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 71d17558..99636a9f 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -72,7 +72,7 @@ class plexgdm: "Protocol: plex\r\n" "Protocol-Version: 1\r\n" "Protocol-Capabilities: timeline,playback,navigation," - "mirror,playqueues\r\n" + "playqueues\r\n" "Device-Class: HTPC" ) % ( options['uuid'], diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index bf6941a9..54a8207e 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__) class SubscriptionManager: - def __init__(self, jsonClass, RequestMgr, player, playlist): + def __init__(self, jsonClass, RequestMgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} @@ -36,7 +36,7 @@ class SubscriptionManager: self.playerprops = {} self.doUtils = downloadutils.DownloadUtils().downloadUrl self.xbmcplayer = player - self.playlist = playlist + self.playqueue = mgr.playqueue self.js = jsonClass self.RequestMgr = RequestMgr @@ -160,8 +160,8 @@ class SubscriptionManager: with threading.RLock(): for sub in self.subscribers.values(): sub.send_update(msg, len(players) == 0) - self.notifyServer(players) - self.lastplayers = players + self.notifyServer(players) + self.lastplayers = players return True def notifyServer(self, players): @@ -231,6 +231,8 @@ class SubscriptionManager: def getPlayerProperties(self, playerid): try: + # Get the playqueue + playqueue = self.playqueue.playqueues[playerid] # get info from the player props = self.js.jsonrpc( "Player.GetProperties", @@ -248,18 +250,16 @@ class SubscriptionManager: 'shuffle': ("0", "1")[props.get('shuffled', False)], 'repeat': pf.getPlexRepeat(props.get('repeat')), } - if self.playlist is not None: - if self.playlist.QueueId() is not None: - info['playQueueID'] = self.playlist.QueueId() - info['playQueueVersion'] = self.playlist.PlayQueueVersion() - info['guid'] = self.playlist.Guid() - # Get the playlist position - pos = self.js.jsonrpc( - "Player.GetProperties", - {"playerid": playerid, - "properties": ["position"]}) - info['playQueueItemID'] = \ - self.playlist.getQueueIdFromPosition(pos['position']) + if playqueue.ID is not None: + info['playQueueID'] = playqueue.ID + info['playQueueVersion'] = playqueue.version + # Get the playlist position + pos = self.js.jsonrpc( + "Player.GetProperties", + {"playerid": playerid, + "properties": ["position"]})['position'] + info['playQueueItemID'] = playqueue.items[pos].ID + info['guid'] = playqueue.items[pos].guid except: import traceback log.error("Traceback:\n%s" % traceback.format_exc()) diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py new file mode 100644 index 00000000..3859fab4 --- /dev/null +++ b/resources/lib/plexdb_functions.py @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- + +############################################################################### + +from utils import kodiSQL +import logging + +############################################################################### + +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +class Get_Plex_DB(): + """ + Usage: with Get_Plex_DB() as plex_db: + plex_db.do_something() + + On exiting "with" (no matter what), commits get automatically committed + and the db gets closed + """ + def __enter__(self): + self.plexconn = kodiSQL('plex') + return Plex_DB_Functions(self.plexconn.cursor()) + + def __exit__(self, type, value, traceback): + self.plexconn.commit() + self.plexconn.close() + + +class Plex_DB_Functions(): + + def __init__(self, plexcursor): + self.plexcursor = plexcursor + + def getViews(self): + """ + Returns a list of view_id + """ + views = [] + query = ''' + SELECT view_id + FROM view + ''' + self.plexcursor.execute(query) + rows = self.plexcursor.fetchall() + for row in rows: + views.append(row[0]) + return views + + def getAllViewInfo(self): + """ + Returns a list of dicts: + {'id': view_id, 'name': view_name, 'itemtype': kodi_type} + """ + plexcursor = self.plexcursor + views = [] + query = ''' + SELECT view_id, view_name, kodi_type + FROM view + ''' + plexcursor.execute(query) + rows = plexcursor.fetchall() + for row in rows: + views.append({'id': row[0], + 'name': row[1], + 'itemtype': row[2]}) + return views + + def getView_byId(self, view_id): + """ + Returns tuple (view_name, kodi_type, kodi_tagid) for view_id + """ + query = ''' + SELECT view_name, kodi_type, kodi_tagid + FROM view + WHERE view_id = ? + ''' + self.plexcursor.execute(query, (view_id,)) + view = self.plexcursor.fetchone() + return view + + def getView_byType(self, kodi_type): + """ + Returns a list of dicts for kodi_type: + {'id': view_id, 'name': view_name, 'itemtype': kodi_type} + """ + views = [] + query = ''' + SELECT view_id, view_name, kodi_type + FROM view + WHERE kodi_type = ? + ''' + self.plexcursor.execute(query, (kodi_type,)) + rows = self.plexcursor.fetchall() + for row in rows: + views.append({ + 'id': row[0], + 'name': row[1], + 'itemtype': row[2] + }) + return views + + def getView_byName(self, view_name): + """ + Returns the view_id for view_name (or None) + """ + query = ''' + SELECT view_id + FROM view + WHERE view_name = ? + ''' + self.plexcursor.execute(query, (view_name,)) + try: + view = self.plexcursor.fetchone()[0] + except TypeError: + view = None + return view + + def addView(self, view_id, view_name, kodi_type, kodi_tagid): + """ + Appends an entry to the view table + """ + query = ''' + INSERT INTO view( + view_id, view_name, kodi_type, kodi_tagid) + VALUES (?, ?, ?, ?) + ''' + self.plexcursor.execute(query, + (view_id, view_name, kodi_type, kodi_tagid)) + + def updateView(self, view_name, kodi_tagid, view_id): + """ + Updates the view_id with view_name and kodi_tagid + """ + query = ''' + UPDATE view + SET view_name = ?, kodi_tagid = ? + WHERE view_id = ? + ''' + self.plexcursor.execute(query, (view_name, kodi_tagid, view_id)) + + def removeView(self, view_id): + query = ''' + DELETE FROM view + WHERE view_id = ? + ''' + self.plexcursor.execute(query, (view_id,)) + + def getItem_byFileId(self, kodi_fileid, kodi_type): + """ + Returns plex_id for kodi_fileid and kodi_type + + None if not found + """ + query = ''' + SELECT plex_id + FROM plex + WHERE kodi_fileid = ? AND kodi_type = ? + ''' + try: + self.plexcursor.execute(query, (kodi_fileid, kodi_type)) + item = self.plexcursor.fetchone()[0] + return item + except: + return None + + def getMusicItem_byFileId(self, kodi_id, kodi_type): + """ + Returns the plex_id for kodi_id and kodi_type + + None if not found + """ + query = ''' + SELECT plex_id + FROM plex + WHERE kodi_id = ? AND kodi_type = ? + ''' + try: + self.plexcursor.execute(query, (kodi_id, kodi_type)) + item = self.plexcursor.fetchone()[0] + return item + except: + return None + + def getItem_byId(self, plex_id): + """ + For plex_id, returns the tuple + (kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type) + + None if not found + """ + query = ''' + SELECT kodi_id, kodi_fileid, kodi_pathid, + parent_id, kodi_type, plex_type + FROM plex + WHERE plex_id = ? + ''' + try: + self.plexcursor.execute(query, (plex_id,)) + item = self.plexcursor.fetchone() + return item + except: + return None + + def getItem_byWildId(self, plex_id): + """ + Returns a list of tuples (kodi_id, kodi_type) for plex_id (% appended) + """ + query = ''' + SELECT kodi_id, kodi_type + FROM plex + WHERE plex_id LIKE ? + ''' + self.plexcursor.execute(query, (plex_id+"%",)) + return self.plexcursor.fetchall() + + def getItem_byView(self, view_id): + """ + Returns kodi_id for view_id + """ + query = ''' + SELECT kodi_id + FROM plex + WHERE view_id = ? + ''' + self.plexcursor.execute(query, (view_id,)) + return self.plexcursor.fetchall() + + def getItem_byKodiId(self, kodi_id, kodi_type): + """ + Returns the tuple (plex_id, parent_id) for kodi_id and kodi_type + """ + query = ''' + SELECT plex_id, parent_id + FROM plex + WHERE kodi_id = ? + AND kodi_type = ? + ''' + self.plexcursor.execute(query, (kodi_id, kodi_type,)) + return self.plexcursor.fetchone() + + def getItem_byParentId(self, parent_id, kodi_type): + """ + Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id, + kodi_type + """ + query = ''' + SELECT plex_id, kodi_id, kodi_fileid + FROM plex + WHERE parent_id = ? + AND kodi_type = ?" + ''' + self.plexcursor.execute(query, (parent_id, kodi_type,)) + return self.plexcursor.fetchall() + + def getItemId_byParentId(self, parent_id, kodi_type): + """ + Returns the tuple (plex_id, kodi_id) for parent_id, kodi_type + """ + query = ''' + SELECT plex_id, kodi_id + FROM plex + WHERE parent_id = ? + AND kodi_type = ? + ''' + self.plexcursor.execute(query, (parent_id, kodi_type,)) + return self.plexcursor.fetchall() + + def getChecksum(self, plex_type): + """ + Returns a list of tuples (plex_id, checksum) for plex_type + """ + query = ''' + SELECT plex_id, checksum + FROM plex + WHERE plex_type = ? + ''' + self.plexcursor.execute(query, (plex_type,)) + return self.plexcursor.fetchall() + + def getMediaType_byId(self, plex_id): + """ + Returns plex_type for plex_id + + Or None if not found + """ + query = ''' + SELECT plex_type + FROM plex + WHERE plex_id = ? + ''' + self.plexcursor.execute(query, (plex_id,)) + try: + itemtype = self.plexcursor.fetchone()[0] + except TypeError: + itemtype = None + return itemtype + + def addReference(self, plex_id, plex_type, kodi_id, kodi_type, + kodi_fileid=None, kodi_pathid=None, parent_id=None, + checksum=None, view_id=None): + """ + Appends or replaces an entry into the plex table + """ + query = ''' + INSERT OR REPLACE INTO plex( + plex_id, kodi_id, kodi_fileid, kodi_pathid, plex_type, + kodi_type, parent_id, checksum, view_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.plexcursor.execute(query, (plex_id, kodi_id, kodi_fileid, + kodi_pathid, plex_type, kodi_type, + parent_id, checksum, view_id)) + + def updateReference(self, plex_id, checksum): + """ + Updates checksum for plex_id + """ + query = "UPDATE plex SET checksum = ? WHERE plex_id = ?" + self.plexcursor.execute(query, (checksum, plex_id)) + + def updateParentId(self, plexid, parent_kodiid): + """ + Updates parent_id for plex_id + """ + query = "UPDATE plex SET parent_id = ? WHERE plex_id = ?" + self.plexcursor.execute(query, (parent_kodiid, plexid)) + + def removeItems_byParentId(self, parent_id, kodi_type): + """ + Removes all entries with parent_id and kodi_type + """ + query = ''' + DELETE FROM plex + WHERE parent_id = ? + AND kodi_type = ? + ''' + self.plexcursor.execute(query, (parent_id, kodi_type,)) + + def removeItem_byKodiId(self, kodi_id, kodi_type): + """ + Removes the one entry with kodi_id and kodi_type + """ + query = ''' + DELETE FROM plex + WHERE kodi_id = ? + AND kodi_type = ? + ''' + self.plexcursor.execute(query, (kodi_id, kodi_type,)) + + def removeItem(self, plex_id): + """ + Removes the one entry with plex_id + """ + query = "DELETE FROM plex WHERE plex_id = ?" + self.plexcursor.execute(query, (plex_id,)) + + def removeWildItem(self, plex_id): + """ + Removes all entries with plex_id with % added + """ + query = "DELETE FROM plex WHERE plex_id LIKE ?" + self.plexcursor.execute(query, (plex_id+"%",)) + + def itemsByType(self, plex_type): + """ + Returns a list of dicts for plex_type: + { + 'plexId': plex_id + 'kodiId': kodi_id + 'kodi_type': kodi_type + 'plex_type': plex_type + } + """ + query = ''' + SELECT plex_id, kodi_id, kodi_type + FROM plex + WHERE plex_type = ? + ''' + self.plexcursor.execute(query, (plex_type, )) + result = [] + for row in self.plexcursor.fetchall(): + result.append({ + 'plexId': row[0], + 'kodiId': row[1], + 'kodi_type': row[2], + 'plex_type': plex_type + }) + return result diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index 3388e847..badedca6 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -101,7 +101,7 @@ class Read_EmbyServer(): viewId = view['Id'] # Compare to view table in emby database - emby = kodiSQL('emby') + emby = kodiSQL('plex') cursor_emby = emby.cursor() query = ' '.join(( diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 659586b2..75ff98bb 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -32,8 +32,10 @@ class UserClient(threading.Thread): # Borg - multiple instances, shared state __shared_state = {} - def __init__(self): + def __init__(self, callback=None): self.__dict__ = self.__shared_state + if callback is not None: + self.mgr = callback self.auth = True self.retry = 0 diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 5bc89c9d..c55af70e 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -15,7 +15,6 @@ from functools import wraps from calendar import timegm import os - import xbmc import xbmcaddon import xbmcgui @@ -57,6 +56,24 @@ def window(property, value=None, clear=False, windowid=10000): return tryDecode(win.getProperty(property)) +def pickl_window(property, value=None, clear=False, windowid=10000): + """ + Get or set window property - thread safe! For use with Pickle + Property and value must be string + """ + if windowid != 10000: + win = xbmcgui.Window(windowid) + else: + win = WINDOW + + if clear: + win.clearProperty(property) + elif value is not None: + win.setProperty(property, value) + else: + return win.getProperty(property) + + def settings(setting, value=None): """ Get or add addon setting. Returns unicode @@ -134,21 +151,21 @@ def tryDecode(string, encoding='utf-8'): def DateToKodi(stamp): - """ - converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a - propper, human-readable time stamp used by Kodi + """ + converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a + propper, human-readable time stamp used by Kodi - Output: Y-m-d h:m:s = 2009-04-05 23:16:04 + Output: Y-m-d h:m:s = 2009-04-05 23:16:04 - None if an error was encountered - """ - try: - stamp = float(stamp) + float(window('kodiplextimeoffset')) - date_time = time.localtime(stamp) - localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time) - except: - localdate = None - return localdate + None if an error was encountered + """ + try: + stamp = float(stamp) + float(window('kodiplextimeoffset')) + date_time = time.localtime(stamp) + localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time) + except: + localdate = None + return localdate def IfExists(path): @@ -200,8 +217,8 @@ def getUnixTimestamp(secondsIntoTheFuture=None): def kodiSQL(media_type="video"): - if media_type == "emby": - dbPath = tryDecode(xbmc.translatePath("special://database/emby.db")) + if media_type == "plex": + dbPath = tryDecode(xbmc.translatePath("special://database/plex.db")) elif media_type == "music": dbPath = getKodiMusicDBPath() elif media_type == "texture": @@ -346,7 +363,7 @@ def reset(): # Wipe the Plex database log.info("Resetting the Plex database.") - connection = kodiSQL('emby') + connection = kodiSQL('plex') cursor = connection.cursor() cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor.fetchall() @@ -939,9 +956,33 @@ def ThreadMethods(cls): return cls +class Lock_Function: + """ + Decorator for class methods and functions to lock them with lock. + + Initialize this class first + lockfunction = Lock_Function(lock), where lock is a threading.Lock() object + + To then lock a function or method: + + @lockfunction.lockthis + def some_function(args, kwargs) + """ + def __init__(self, lock): + self.lock = lock + + def lockthis(self, func): + @wraps(func) + def wrapper(*args, **kwargs): + with self.lock: + result = func(*args, **kwargs) + return result + return wrapper + ############################################################################### # UNUSED METHODS + def changePlayState(itemType, kodiId, playCount, lastplayed): """ YET UNUSED @@ -985,3 +1026,27 @@ def changePlayState(itemType, kodiId, playCount, lastplayed): result = json.loads(result) result = result.get('result') log.debug("JSON result was: %s" % result) + + +class JSONRPC(object): + id_ = 1 + jsonrpc = "2.0" + + def __init__(self, method, **kwargs): + self.method = method + for arg in kwargs: # id_(int), jsonrpc(str) + self.arg = arg + + def _query(self): + query = { + 'jsonrpc': self.jsonrpc, + 'id': self.id_, + 'method': self.method, + } + if self.params is not None: + query['params'] = self.params + return json.dumps(query) + + def execute(self, params=None): + self.params = params + return json.loads(xbmc.executeJSONRPC(self._query())) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index cc81a617..312080b5 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -2,13 +2,13 @@ ############################################################################### import logging -import json -import threading -import Queue import websocket -import ssl +from json import loads +from threading import Thread +from Queue import Queue +from ssl import CERT_NONE -import xbmc +from xbmc import sleep from utils import window, settings, ThreadMethodsAdditionalSuspend, \ ThreadMethods @@ -22,21 +22,23 @@ log = logging.getLogger("PLEX."+__name__) @ThreadMethodsAdditionalSuspend('suspend_LibraryThread') @ThreadMethods -class WebSocket(threading.Thread): +class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) - def __init__(self, queue): + def __init__(self, callback=None): + if callback is not None: + self.mgr = callback self.ws = None # Communication with librarysync - self.queue = queue - threading.Thread.__init__(self) + self.queue = Queue() + Thread.__init__(self) def process(self, opcode, message): if opcode not in self.opcode_data: return False try: - message = json.loads(message) + message = loads(message) except Exception as ex: log.error('Error decoding message from websocket: %s' % ex) log.error(message) @@ -57,13 +59,8 @@ class WebSocket(threading.Thread): return True # Put PMS message on queue and let libsync take care of it - try: - self.queue.put(message) - return True - except Queue.Full: - # Queue only takes 200 messages. No worries if we miss one or two - log.info('Queue is full, dropping PMS message %s' % message) - return False + self.queue.put(message) + return True def receive(self, ws): # Not connected yet @@ -97,7 +94,7 @@ class WebSocket(threading.Thread): uri += '?X-Plex-Token=%s' % token sslopt = {} if settings('sslverify') == "false": - sslopt["cert_reqs"] = ssl.CERT_NONE + sslopt["cert_reqs"] = CERT_NONE log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt @@ -122,7 +119,7 @@ class WebSocket(threading.Thread): # Abort was requested while waiting. We should exit log.info("##===---- WebSocketClient Stopped ----===##") return - xbmc.sleep(1000) + sleep(1000) try: self.process(*self.receive(self.ws)) except websocket.WebSocketTimeoutException: @@ -148,11 +145,11 @@ class WebSocket(threading.Thread): "declaring the connection dead") window('plex_online', value='false') counter = 0 - xbmc.sleep(1000) + sleep(1000) except websocket.WebSocketTimeoutException: log.info("timeout while connecting, trying again") self.ws = None - xbmc.sleep(1000) + sleep(1000) except websocket.WebSocketException as e: log.info('WebSocketException: %s' % e) if 'Handshake Status 401' in e.args: @@ -162,14 +159,14 @@ class WebSocket(threading.Thread): 'WebSocketClient now') break self.ws = None - xbmc.sleep(1000) + sleep(1000) except Exception as e: log.error("Unknown exception encountered in connecting: %s" % e) import traceback log.error("Traceback:\n%s" % traceback.format_exc()) self.ws = None - xbmc.sleep(1000) + sleep(1000) else: counter = 0 handshake_counter = 0 diff --git a/resources/settings.xml b/resources/settings.xml index 5f1d2456..aab11e2d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -87,7 +87,7 @@ - + diff --git a/service.py b/service.py index 35499474..de3ca487 100644 --- a/service.py +++ b/service.py @@ -5,7 +5,6 @@ import logging import os import sys -import Queue import xbmc import xbmcaddon @@ -33,17 +32,20 @@ sys.path.append(_base_resource) ############################################################################### from utils import settings, window, language as lang -import userclient +from userclient import UserClient import clientinfo import initialsetup -import kodimonitor -import librarysync +from kodimonitor import KodiMonitor +from librarysync import LibrarySync import videonodes -import websocket_client as wsc +from websocket_client import WebSocket import downloadutils +from playqueue import Playqueue import PlexAPI -import PlexCompanion +from PlexCompanion import PlexCompanion +from monitor_kodi_play import Monitor_Kodi_Play +from playback_starter import Playback_Starter ############################################################################### @@ -61,11 +63,19 @@ class Service(): server_online = True warn_auth = True - userclient_running = False - websocket_running = False + user = None + ws = None + library = None + plexCompanion = None + playqueue = None + + user_running = False + ws_running = False library_running = False - kodimonitor_running = False plexCompanion_running = False + playqueue_running = False + kodimonitor_running = False + playback_starter_running = False def __init__(self): @@ -96,13 +106,14 @@ class Service(): "plex_online", "plex_serverStatus", "plex_onWake", "plex_dbCheck", "plex_kodiScan", "plex_shouldStop", "currUserId", "plex_dbScan", - "plex_initialScan", "plex_customplaylist", "plex_playbackProps", + "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", "plex_runLibScan", "plex_username", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", "suspend_LibraryThread", "plex_terminateNow", "kodiplextimeoffset", "countError", "countUnauthorized", - "plex_restricteduser", "plex_allows_mediaDeletion" + "plex_restricteduser", "plex_allows_mediaDeletion", + "plex_play_new_item", "plex_result" ] for prop in properties: window(prop, clear=True) @@ -111,7 +122,7 @@ class Service(): videonodes.VideoNodes().clearProperties() # Set the minimum database version - window('plex_minDBVersion', value="1.1.5") + window('plex_minDBVersion', value="1.5.2") def getLogLevel(self): try: @@ -126,16 +137,21 @@ class Service(): monitor = self.monitor kodiProfile = xbmc.translatePath("special://profile") + # Detect playback start early on + self.monitor_kodi_play = Monitor_Kodi_Play(self) + self.monitor_kodi_play.start() + # Server auto-detect initialsetup.InitialSetup().setup() - # Queue for background sync - queue = Queue.Queue() + # Initialize important threads, handing over self for callback purposes + self.user = UserClient(self) + self.ws = WebSocket(self) + self.library = LibrarySync(self) + self.plexCompanion = PlexCompanion(self) + self.playqueue = Playqueue(self) + self.playback_starter = Playback_Starter(self) - # Initialize important threads - user = userclient.UserClient() - ws = wsc.WebSocket(queue) - library = librarysync.LibrarySync(queue) plx = PlexAPI.PlexAPI() welcome_msg = True @@ -157,7 +173,7 @@ class Service(): if window('plex_online') == "true": # Plex server is online # Verify if user is set and has access to the server - if (user.currUser is not None) and user.HasAccess: + if (self.user.currUser is not None) and self.user.HasAccess: if not self.kodimonitor_running: # Start up events self.warn_auth = True @@ -166,38 +182,46 @@ class Service(): welcome_msg = False xbmcgui.Dialog().notification( heading=addonName, - message="%s %s" % (lang(33000), user.currUser), - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", + message="%s %s" % (lang(33000), + self.user.currUser), + icon="special://home/addons/plugin." + "video.plexkodiconnect/icon.png", time=2000, sound=False) # Start monitoring kodi events - self.kodimonitor_running = kodimonitor.KodiMonitor() - + self.kodimonitor_running = KodiMonitor(self) + # Start playqueue client + if not self.playqueue_running: + self.playqueue_running = True + self.playqueue.start() # Start the Websocket Client - if not self.websocket_running: - self.websocket_running = True - ws.start() + if not self.ws_running: + self.ws_running = True + self.ws.start() # Start the syncing thread if not self.library_running: self.library_running = True - library.start() + self.library.start() # Start the Plex Companion thread if not self.plexCompanion_running: self.plexCompanion_running = True - plexCompanion = PlexCompanion.PlexCompanion() - plexCompanion.start() + self.plexCompanion.start() + if not self.playback_starter_running: + self.playback_starter_running = True + self.playback_starter.start() else: - if (user.currUser is None) and self.warn_auth: - # Alert user is not authenticated and suppress future warning + if (self.user.currUser is None) and self.warn_auth: + # Alert user is not authenticated and suppress future + # warning self.warn_auth = False log.warn("Not authenticated yet.") # User access is restricted. # Keep verifying until access is granted # unless server goes offline or Kodi is shut down. - while user.HasAccess == False: + while self.user.HasAccess is False: # Verify access with an API call - user.hasAccess() + self.user.hasAccess() if window('plex_online') != "true": # Server went offline @@ -211,7 +235,7 @@ class Service(): # Wait until Plex server is online # or Kodi is shut down. while not monitor.abortRequested(): - server = user.getServer() + server = self.user.getServer() if server is False: # No server info set in add-on settings pass @@ -268,9 +292,9 @@ class Service(): window('suspend_LibraryThread', clear=True) # Start the userclient thread - if not self.userclient_running: - self.userclient_running = True - user.start() + if not self.user_running: + self.user_running = True + self.user.start() break @@ -286,27 +310,22 @@ class Service(): # Tell all threads to terminate (e.g. several lib sync threads) window('plex_terminateNow', value='true') - try: - plexCompanion.stopThread() + self.plexCompanion.stopThread() except: log.warn('plexCompanion already shut down') - try: - library.stopThread() + self.library.stopThread() except: log.warn('Library sync already shut down') - try: - ws.stopThread() + self.ws.stopThread() except: log.warn('Websocket client already shut down') - try: - user.stopThread() + self.user.stopThread() except: log.warn('User client already shut down') - try: downloadutils.DownloadUtils().stopSession() except: