From db02a001a88f8698e674f9244afa67262e6d5689 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 3 Dec 2016 11:50:05 +0100 Subject: [PATCH 01/50] Playlist major upgrade part 1 --- resources/lib/PlexFunctions.py | 11 ++ resources/lib/kodimonitor.py | 9 ++ resources/lib/playlist.py | 286 +++++++++++++++++++++++++++++---- resources/lib/utils.py | 25 ++- 4 files changed, 301 insertions(+), 30 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 70776f38..6b6cb3d8 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -60,6 +60,17 @@ KODITYPE_FROM_PLEXTYPE = { 'XXXXXXX': 'genre' } +KODIAUDIOVIDEO_FROM_MEDIA_TYPE = { + 'movie': 'video', + 'episode': 'video', + 'season': 'video', + 'tvshow': 'video', + 'artist': 'audio', + 'album': 'audio', + 'track': 'audio', + 'song': 'audio' +} + def ConvertPlexToKodiTime(plexTime): """ diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7686d734..d17b56d5 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,6 +14,7 @@ import kodidb_functions as kodidb import playbackutils as pbutils from utils import window, settings, CatchExceptions, tryDecode, tryEncode from PlexFunctions import scrobble +from playlist import Playlist ############################################################################### @@ -28,6 +29,7 @@ class KodiMonitor(xbmc.Monitor): self.doUtils = downloadutils.DownloadUtils().downloadUrl self.xbmcplayer = xbmc.Player() + self.playlist = Playlist('video') xbmc.Monitor.__init__(self) log.info("Kodi monitor started.") @@ -157,6 +159,13 @@ class KodiMonitor(xbmc.Monitor): elif method == "Playlist.OnClear": pass + elif method == "Playlist.OnAdd": + # User manipulated Kodi playlist + # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, + # u'position': 0} + self.playlist.kodi_onadd(data) + Playlist() + def PlayBackStart(self, data): """ Called whenever a playback is started diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index f6886a9c..3e055156 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -7,14 +7,17 @@ import json from urllib import urlencode from threading import Lock from functools import wraps +from urllib import quote, urlencode import xbmc import embydb_functions as embydb -from utils import window, tryEncode +import kodidb_functions as kodidb +from utils import window, tryEncode, JSONRPC import playbackutils -import PlexFunctions +import PlexFunctions as PF import PlexAPI +from downloadutils import DownloadUtils ############################################################################### @@ -22,6 +25,13 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### +PLEX_PLAYQUEUE_ARGS = ( + 'playQueueID', + 'playQueueVersion', + 'playQueueSelectedItemID', + 'playQueueSelectedItemOffset' +) + class lockMethod: """ @@ -45,43 +55,115 @@ class lockMethod: class Playlist(): """ Initiate with Playlist(typus='video' or 'music') + + ATTRIBUTES: + id: integer + position: integer, default -1 + type: string, default "unknown" + "unknown", + "video", + "audio", + "picture", + "mixed" + size: integer """ # Borg - multiple instances, shared state _shared_state = {} - typus = None - queueId = None - playQueueVersion = None - guid = None - playlistId = None player = xbmc.Player() - # "interal" PKC playlist - items = [] + + playlists = None @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: + # If already initiated, return + if self.playlists is not None: 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() + + self.doUtils = DownloadUtils().downloadUrl + # Get all playlists from Kodi + self.playlists = JSONRPC('Playlist.GetPlaylists').execute() + try: + self.playlists = self.playlists['result'] + except KeyError: + log.error('Could not get Kodi playlists. JSON Result was: %s' + % self.playlists) + self.playlists = None + return + # Example return: [{u'playlistid': 0, u'type': u'audio'}, + # {u'playlistid': 1, u'type': u'video'}, + # {u'playlistid': 2, u'type': u'picture'}] + # Initiate the Kodi playlists + for playlist in self.playlists: + # Initialize each Kodi playlist + if playlist['type'] == 'audio': + playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + elif playlist['type'] == 'video': + playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + else: + # Currently, only video or audio playlists available + playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + # Initialize Plex info on the playQueue + for arg in PLEX_PLAYQUEUE_ARGS: + playlist[arg] = None + + # Build a list of all items within each playlist + playlist['items'] = [] + for item in self._get_kodi_items(playlist['playlistid']): + playlist['items'].append({ + 'kodi_id': item.get('id'), + 'type': item['type'], + 'file': item['file'], + 'playQueueItemID': None, + 'plex_id': self._get_plexid(item) + }) + log.debug('self.playlist: %s' % playlist) + + def _init_pl_item(self): + return { + 'plex_id': None, + 'kodi_id': None, + 'file': None, + 'type': None, # 'audio' or 'video' + 'playQueueItemID': None, + 'uri': None, + # To be able to drag Kodi JSON data along: + 'playlistid': None, + 'position': None, + 'item': None, + } + + def _get_plexid(self, item): + """ + Supply with data['item'] as returned from Kodi JSON-RPC interface + """ + with embydb.GetEmbyDB() as emby_db: + emby_dbitem = emby_db.getItem_byKodiId(item.get('id'), + item.get('type')) + try: + plex_id = emby_dbitem[0] + except TypeError: + plex_id = None + return plex_id + + def _get_kodi_items(self, playlistid): + params = { + 'playlistid': playlistid, + 'properties': ["title", "file"] + } + answ = JSONRPC('Playlist.GetItems').execute(params) + # returns 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'}] + try: + answ = answ['result']['items'] + except KeyError: + answ = [] + return answ @lockMethod.decorate def getQueueIdFromPosition(self, playlistPosition): @@ -138,7 +220,7 @@ class Playlist(): mediatype = embydb_item[4] except TypeError: log.info('Couldnt find item %s in Kodi db' % itemid) - item = PlexFunctions.GetPlexMetadata(itemid) + item = PF.GetPlexMetadata(itemid) if item in (None, 401): log.info('Couldnt find item %s on PMS, trying next' % itemid) @@ -177,7 +259,7 @@ class Playlist(): mediatype = embydb_item[4] except TypeError: log.info('Couldnt find item %s in Kodi db' % plexId) - xml = PlexFunctions.GetPlexMetadata(plexId) + xml = PF.GetPlexMetadata(plexId) if xml in (None, 401): log.error('Could not download plexId %s' % plexId) else: @@ -335,3 +417,149 @@ class Playlist(): } } log.debug(xbmc.executeJSONRPC(json.dumps(pl))) + + def _get_uri(self, plex_id=None, item=None): + """ + Supply with either plex_id or data['item'] as received from Kodi JSON- + RPC + """ + uri = None + if plex_id is None: + plex_id = self._get_plexid(item) + self._cur_item['plex_id'] = plex_id + if plex_id is not None: + xml = PF.GetPlexMetadata(plex_id) + try: + uri = ('library://%s/item/%s%s' % + (xml.attrib.get('librarySectionUUID'), + quote('library/metadata/', safe=''), plex_id)) + except: + pass + if uri is None: + try: + uri = 'library://whatever/item/%s' % quote(item['file'], + safe='') + except: + raise KeyError('Could not get file/url with item: %s' % item) + self._cur_item['uri'] = uri + return uri + + def _init_plex_playQueue(self, plex_id=None, data=None): + """ + Supply either plex_id or the data supplied by Kodi JSON-RPC + """ + if plex_id is None: + plex_id = self._get_plexid(data['item']) + self._cur_item['plex_id'] = plex_id + + if data is not None: + playlistid = data['playlistid'] + plex_type = self.playlists[playlistid]['type'] + else: + with embydb.GetEmbyDB() as emby_db: + plex_type = emby_db.getItem_byId(plex_id) + try: + plex_type = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[plex_type[4]] + except TypeError: + raise KeyError('Unknown plex_type %s' % plex_type) + for playlist in self.playlists: + if playlist['type'] == plex_type: + playlistid = playlist['playlistid'] + self._cur_item['playlistid'] = playlistid + self._cur_item['type'] = plex_type + + params = { + 'next': 0, + 'type': plex_type, + 'uri': self._get_uri(plex_id=plex_id, item=data['item']) + } + log.debug('params: %s' % urlencode(params)) + xml = self.doUtils(url="{server}/playQueues", + action_type="POST", + parameters=params) + try: + xml.attrib + except (TypeError, AttributeError): + raise KeyError('Could not post to PMS, received: %s' % xml) + self._Plex_item_updated(xml) + + def _Plex_item_updated(self, xml): + """ + Called if a new item has just been added/updated @ Plex playQueue + + Call with the PMS' xml reply + """ + # Update the ITEM + log.debug('xml.attrib: %s' % xml.attrib) + args = { + 'playQueueItemID': 'playQueueLastAddedItemID', # for playlist PUT + 'playQueueItemID': 'playQueueSelectedItemID' # for playlist INIT + } + for old, new in args.items(): + if new in xml.attrib: + self._cur_item[old] = xml.attrib[new] + # Update the PLAYLIST + for arg in PLEX_PLAYQUEUE_ARGS: + if arg in xml.attrib: + self.playlists[self._cur_item['playlistid']][arg] = xml.attrib[arg] + + def _init_Kodi_item(self, item): + """ + Call with Kodi's JSON-RPC data['item'] + """ + self._cur_item['kodi_id'] = item.get('id') + try: + self._cur_item['type'] = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[ + item.get('type')] + except KeyError: + log.error('Could not get media_type for %s' % item) + + def _add_curr_item(self): + self.playlists[self._cur_item['playlistid']]['items'].insert( + self._cur_item['position'], + self._cur_item) + + @lockMethod.decorate + def kodi_onadd(self, data): + """ + Called if Kodi playlist is modified. Data is Kodi JSON-RPC output, e.g. + { + u'item': {u'type': u'movie', u'id': 3}, + u'playlistid': 1, + u'position': 0 + } + """ + self._cur_item = self._init_pl_item() + self._cur_item.update(data) + self._init_Kodi_item(data['item']) + + pl = self.playlists[data['playlistid']] + if pl['playQueueID'] is None: + # Playlist needs to be initialized! + try: + self._init_plex_playQueue(data=data) + except KeyError as e: + log.error('Error encountered while init playQueue: %s' % e) + return + else: + next_item = data['position'] + if next_item != 0: + next_item = pl['items'][data['position']-1]['playQueueItemID'] + params = { + 'next': next_item, + 'type': pl['type'], + 'uri': self._get_uri(item=data['item']) + } + xml = self.doUtils(url="{server}/playQueues/%s" + % pl['playQueueID'], + action_type="PUT", + parameters=params) + try: + xml.attrib + except AttributeError: + log.error('Could not add item %s to playQueue' % data) + return + self._Plex_item_updated(xml) + # Add the new item to our playlist + self._add_curr_item() + log.debug('self.playlists are now: %s' % self.playlists) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 52d5c415..34a63a1f 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 @@ -964,3 +963,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())) From ad8b7c7d9061227fdcf7ff7bb1d1c3da320ad35c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 20 Dec 2016 16:13:19 +0100 Subject: [PATCH 02/50] Merge Master --- README.md | 88 +++++----- addon.xml | 2 +- changelog.txt | 34 ++++ resources/language/English/strings.xml | 6 +- resources/language/German/strings.xml | 7 +- resources/lib/PlexAPI.py | 50 ++---- resources/lib/PlexFunctions.py | 14 ++ resources/lib/artwork.py | 10 +- resources/lib/downloadutils.py | 10 +- resources/lib/initialsetup.py | 20 +-- resources/lib/itemtypes.py | 30 ++-- resources/lib/kodidb_functions.py | 218 +++++++++++-------------- resources/lib/kodimonitor.py | 13 +- resources/lib/librarysync.py | 113 +++++++++---- resources/lib/playutils.py | 108 ++++++------ resources/lib/userclient.py | 52 +++--- resources/lib/utils.py | 25 ++- resources/lib/videonodes.py | 11 +- resources/lib/websocket_client.py | 8 +- resources/settings.xml | 9 +- service.py | 55 ++++--- 21 files changed, 473 insertions(+), 410 deletions(-) diff --git a/README.md b/README.md index 253850e5..a16f4dee 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,65 @@ # PlexKodiConnect (PKC) **Combine the best frontend media player Kodi with the best multimedia backend server Plex** -PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly customizable user interfaces and playback of any file under the sun, and the Plex Media Server to manage all your media without lifting a finger. +PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly customizable user interfaces and playback of any file under the sun - and the Plex Media Server. Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible. + +### Content +* [**Warning**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#warning) +* [**What does PKC do and how is it different from the official 'Plex for Kodi'**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#what-does-pkc-do-and-how-is-it-different-from-the-official-plex-for-kod) +* [**Download and Installation**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#download-and-installation) +* [**Important notes**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#important-notes) +* [**Donations**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#donations) +* [**What is currently supported?**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#what-is-currently-supported) +* [**Known Larger Issues**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#known-larger-issues) +* [**Issues being worked on**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#issues-being-worked-on) +* [**Pipeline for future development**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#what-could-be-in-the-pipeline-for-future-development) +* [**Checkout the PKC Wiki**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#checkout-the-pkc-wiki) +* [**Credits**](https://github.com/croneter/PlexKodiConnect/tree/hotfixes#credits) + ### Warning -This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases (as this plugin directly changes them). Use at your own risk! +Use at your own risk! This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases as this plugin directly changes them. Don't worry if you want Plex to manage all your media (like you should ;-)). + +### What does PKC do and how is it different from the official ['Plex for Kodi'](https://www.plex.tv/apps/computer/kodi/)? + +With other Plex addons for Kodi such as the official [Plex for Kodi](https://www.plex.tv/apps/computer/kodi/) or [PlexBMC](https://forums.plex.tv/discussion/106593/plexbmc-xbmc-add-on-to-connect-to-plex-media-server) there are a couple of issues: +- Other Kodi addons such as NextAired, remote apps and others won't work +- You can only use special Kodi skins +- Slow speed: when browsing data has to be retrieved from the server. Especially on slower devices this can take too much time and you will notice artwork being loaded slowly while you browse the library +- All kinds of workarounds are needed to get the best experience on Kodi clients + +PKC synchronizes your media from your Plex server to the native Kodi database. Because PKC uses the native Kodi database, the above limitations are gone! +- Use any Kodi skin you want! +- You can browse your media at full speed, images are cached +- All other Kodi addons will be able to "see" your media, thinking it's normal Kodi stuff + +Some people argue that PKC is 'hacky' because of the way it directly accesses the Kodi database. See [here for a more thorough discussion](https://github.com/croneter/PlexKodiConnect/wiki/Is-PKC-'hacky'%3F). ### Download and Installation [ ![Download](https://api.bintray.com/packages/croneter/PlexKodiConnect/PlexKodiConnect/images/download.svg) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -The easiest way to install PKC is via our PlexKodiConnect Kodi repository (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [installation guideline on how to do this](https://github.com/croneter/PlexKodiConnect/wiki/Installation). +Install PKC via the PlexKodiConnect Kodi repository (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [installation guideline on how to do this](https://github.com/croneter/PlexKodiConnect/wiki/Installation). **Possibly UNSTABLE BETA version:** [ ![Download](https://api.bintray.com/packages/croneter/PlexKodiConnect_BETA/PlexKodiConnect_BETA/images/download.svg) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +### Important Notes + +1. If you are using a **low CPU device like a Raspberry Pi or a CuBox**, PKC might be instable or crash during initial sync. Lower the number of threads in the [PKC settings under Sync Options](https://github.com/croneter/PlexKodiConnect/wiki/PKC-settings#sync-options): `Limit artwork cache threads: 5` +Don't forget to reboot Kodi after that. +2. **Compatibility**: + * PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing. + * PKC is not (and will never be) compatible with the **MySQL database replacement** in Kodi. In fact, PKC replaces the MySQL functionality because it acts as a "man in the middle" for your entire media library. + * If **another plugin is not working** like it's supposed to, try to use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths) +3. If you post logs, your **Plex tokens** might be included. Be sure to double and triple check for tokens before posting any logs anywhere by searching for `token` + ### Donations I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal if you appreciate PKC. **Full disclaimer:** I will see your name and address on my PayPal account. Rest assured that I will not share this with anyone. [ ![Download](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a|alt=Buy Me a Coffee)](https://ko-fi.com/A8182EB) -### IMPORTANT NOTES - -1. If you are using a **low CPU device like a Raspberry Pi or a CuBox**, PKC might be instable or crash during initial sync. Lower the number of threads in the [PKC settings under Sync Options](https://github.com/croneter/PlexKodiConnect/wiki/PKC-settings#sync-options): `Limit artwork cache threads: 5` -Don't forget to reboot Kodi after that. -2. If you post logs, your **Plex tokens** might be included. Be sure to double and triple check for tokens before posting any logs anywhere by searching for `token` -3. **Compatibility**: PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing. - - -### Checkout the PKC Wiki -The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions. - - -### What does PKC do? - -With other addons for Kodi there are a couple of issues: -- 3rd party addons such as NextAired, remote apps etc. won't work -- Slow speed: when browsing the data has to be retrieved from the server. Especially on slower devices this can take too much time -- You can only use special Kodi skins -- All kinds of workarounds are needed to get the best experience on Kodi clients - -PKC synchronizes your media from your Plex server to the native Kodi database. Because PKC uses the native Kodi database, the above limitations are gone! -- You can browse your media full speed, images are cached -- All other Kodi addons will be able to "see" your media, thinking it's normal Kodi stuff -- Use any Kodi skin you want! - - ### What is currently supported? PKC currently provides the following features: @@ -71,14 +84,14 @@ PKC currently provides the following features: + Extra fanart backgrounds - Automatically group movies into [movie sets](http://kodi.wiki/view/movie_sets) - Direct play from network paths (e.g. "\\\\server\\Plex\\movie.mkv") instead of streaming from slow HTTP (e.g. "192.168.1.1:32400"). You have to setup all your Plex libraries to point to such network paths. Do have a look at [the wiki here](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths) - +- Delete PMS items from the Kodi context menu ### Known Larger Issues Solutions are unlikely due to the nature of these issues +- A Plex Media Server "bug" leads to frequent and slow syncs, see [here for more info](https://github.com/croneter/PlexKodiConnect/issues/135) - *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issue](https://github.com/croneter/PlexKodiConnect/issues/14) for more details - *Plex Music when using Addon paths instead of Native Direct Paths:* You must have a static IP address for your Plex media server if you plan to use Plex Music features -- If something on the PMS has changed, this change is synced to Kodi. Hence if you rescan your entire library, a long PlexKodiConnect re-sync is triggered. You can [change your PMS settings to avoid that](https://github.com/croneter/PlexKodiConnect/wiki/Configure-PKC-on-the-First-Run#deactivate-frequent-updates) - External Plex subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly and tell what language they are in. However, this is not the case if you use direct paths *Background Sync:* @@ -91,21 +104,18 @@ However, some changes to individual items are instantly detected, e.g. if you ma ### Issues being worked on -Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues). +Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues). Before you open your own issue, please read [How to report a bug](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug). ### What could be in the pipeline for future development? +- Plex channels +- Movie extras (trailers already work) - Playlists - Music Videos -- Deleting PMS items from Kodi -- TV Shows Theme Music (ultra-low prio) - - -### Important note about MySQL database in Kodi - -The addon is not (and will not be) compatible with the MySQL database replacement in Kodi. In fact, PlexKodiConnect takes over the point of having a MySQL database because it acts as a "man in the middle" for your entire media library. +### Checkout the PKC Wiki +The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions. You can even edit the wiki yourself! ### Credits diff --git a/addon.xml b/addon.xml index 5c765bbd..85d4d694 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/changelog.txt b/changelog.txt index e7602d8c..876f8593 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,37 @@ +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 +- New setting to always transcode if the video bitrate is above a certain threshold (will not work with direct paths) +- Be smarter when deciding when to transcode +- Only sign the user out if the PMS says so +- Improvements to PMS on/offline notifications +- Note to PLEASE read the Wiki if one is using several Plex libraries (shows on first PKC install only) +- Get rid of low powered device option (always use low powered option) +- Don't show a notification when searching for PMS +- Combine h265 und HEVC into one setting +- Less traffic when PKC is checking whether a PMS is still offline +- Improve logging + +version 1.5.0 +Out for everyone: +- reatly speed up the database sync. Please report if you experience any issues! +- Only show database sync progress for NEW PMS items +- Speed up the pathname verifications +- Update readme to reflect the advent of the official Plex for Kodi +- Fix for not getting tv show additional fanart +- Fix for fanart url containing spaces +- Fix library AttributeError +- Catch websocket handshake errors correctly + +version 1.4.10 (beta only) +- Fix library AttributeError + +version 1.4.9 (beta only) +- Greatly speed up the database sync. Please report if you experience any issues! +- Only show database sync progress for NEW PMS items +- Speed up the pathname verifications +- Update readme to reflect the advent of the official Plex for Kodi + version 1.4.8 (beta only) - Fix for not getting tv show additional fanart - Fix for fanart url containing spaces diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 9208715e..4cf19292 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -126,6 +126,8 @@ - Loop Theme Music Enable Background Image (Requires Restart) Services + + Always transcode if video bitrate is above Skin does not support setting views Select item action (Requires Restart) @@ -294,7 +296,7 @@ Ask to play trailers Skip Plex delete confirmation for the context menu (use at your own risk) Jump back on resume (in seconds) - Force transcode H265 + Force transcode h265/HEVC Music metadata options (not compatible with direct stream) Import music song rating directly from files Convert music song rating to Emby rating @@ -423,7 +425,6 @@ Sync when screensaver is deactivated Force Transcode Hi10P Recently Added: Also show already watched episodes - Force Transcode HEVC Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!) Your current Plex Media Server: [COLOR yellow]Manually enter Plex Media Server address[/COLOR] @@ -434,6 +435,7 @@ Appearance Tweaks TV Shows Always use default Plex subtitle if possible + If you use several Plex libraries of one kind, e.g. "Kids Movies" and "Parents Movies", be sure to check the Wiki: https://goo.gl/JFtQV9 Log-out Plex Home User diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index 1d569543..bdd2f012 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -18,7 +18,7 @@ Plex Musik-Bibliotheken aktivieren Plex Trailer aktivieren (Plexpass benötigt) Nachfragen, ob Trailer gespielt werden sollen - H265 Codec Transkodierung erzwingen + h265/HEVC Codec Transkodierung erzwingen Netzwerk Credentials eingeben PlexKodiConnect Start Verzögerung (in Sekunden) Extras ignorieren, wenn Nächste Episode gespielt wird @@ -151,7 +151,8 @@ - Themen-Musik in Schleife abspielen Laden im Hintergrund aktivieren (Erfordert Neustart) Dienste - Info-Loader aktivieren (Erfordert Neustart) + Immer transkodieren falls Bitrate höher als + Menü-Loader aktivieren (Erfordert Neustart) WebSocket Fernbedienung aktivieren (Erfordert Neustart) 'Laufende Medien'-Loader aktivieren (Erfordert Neustart) @@ -373,7 +374,6 @@ Sync wenn Bildschirmschoner deaktiviert wird Hi10p Codec Transkodierung erzwingen "Zuletzt hinzugefügt": gesehene Folgen anzeigen - HEVC Codec Transkodierung erzwingen "Zuletzt hinzugefügt": gesehene Filme anzeigen (Plex Playlisten und Nodes zurücksetzen!) Aktueller Plex Media Server: [COLOR yellow]Plex Media Server Adresse manuell eingeben[/COLOR] @@ -384,6 +384,7 @@ Tweaks Aussehen TV Serien Falls möglich, Plex Standard-Untertitel anzeigen + Falls du mehrere Plex Bibliotheken einer Art nutzt, z.B. "Filme Kinder" und "Filme Eltern", lies unbedingt das Wiki unter https://goo.gl/JFtQV9 Plex Home Benutzer abmelden: diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 01d9ea08..b63bf71a 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -49,7 +49,8 @@ import clientinfo import downloadutils from utils import window, settings, language as lang, tryDecode, tryEncode, \ DateToKodi, KODILANGUAGE -from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled +from PlexFunctions import PLEX_TO_KODI_TIMEFACTOR, PMSHttpsEnabled, \ + REMAP_TYPE_FROM_PLEXTYPE import embydb_functions as embydb ############################################################################### @@ -288,18 +289,18 @@ class PlexAPI(): url = 'https://plex.tv/api/home/users' else: url = url + '/library/onDeck' - log.info("Checking connection to server %s with verifySSL=%s" - % (url, verifySSL)) + log.debug("Checking connection to server %s with verifySSL=%s" + % (url, verifySSL)) # Check up to 3 times before giving up count = 0 - while count < 3: + while count < 1: answer = self.doUtils(url, authenticate=False, headerOptions=headerOptions, verifySSL=verifySSL, timeout=4) if answer is None: - log.info("Could not connect to %s" % url) + log.debug("Could not connect to %s" % url) count += 1 xbmc.sleep(500) continue @@ -316,7 +317,7 @@ class PlexAPI(): # We could connect but maybe were not authenticated. No worries log.debug("Checking connection successfull. Answer: %s" % answer) return answer - log.info('Failed to connect to %s too many times. PMS is dead' % url) + log.debug('Failed to connect to %s too many times. PMS is dead' % url) return False def GetgPMSKeylist(self): @@ -510,13 +511,6 @@ class PlexAPI(): self.g_PMS dict set """ self.g_PMS = {} - # "Searching for Plex Server" - xbmcgui.Dialog().notification( - heading=addonName, - message=lang(39055), - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - time=4000, - sound=False) # Look first for local PMS in the LAN pmsList = self.PlexGDM() @@ -2515,29 +2509,17 @@ class API(): """ if path is None: return None - types = { - 'movie': 'movie', - 'show': 'tv', - 'season': 'tv', - 'episode': 'tv', - 'artist': 'music', - 'album': 'music', - 'song': 'music', - 'track': 'music', - 'clip': 'clip', - 'photo': 'photo' - } - typus = types[typus] - if settings('remapSMB') == 'true': - path = path.replace(settings('remapSMB%sOrg' % typus), - settings('remapSMB%sNew' % typus), + typus = REMAP_TYPE_FROM_PLEXTYPE[typus] + if window('remapSMB') == 'true': + path = path.replace(window('remapSMB%sOrg' % typus), + window('remapSMB%sNew' % typus), 1) # There might be backslashes left over: path = path.replace('\\', '/') - elif settings('replaceSMB') == 'true': + elif window('replaceSMB') == 'true': if path.startswith('\\\\'): path = 'smb:' + path.replace('\\', '/') - if settings('plex_pathverified') == 'true' and forceCheck is False: + if window('plex_pathverified') == 'true' and forceCheck is False: return path # exist() needs a / or \ at the end to work for directories @@ -2558,12 +2540,12 @@ class API(): if self.askToValidate(path): window('plex_shouldStop', value="true") path = None - settings('plex_pathverified', value='true') + window('plex_pathverified', value='true') else: path = None elif forceCheck is False: - if settings('plex_pathverified') != 'true': - settings('plex_pathverified', value='true') + if window('plex_pathverified') != 'true': + window('plex_pathverified', value='true') return path def askToValidate(self, url): diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 6b6cb3d8..f93ffb47 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -72,6 +72,20 @@ KODIAUDIOVIDEO_FROM_MEDIA_TYPE = { } +REMAP_TYPE_FROM_PLEXTYPE = { + 'movie': 'movie', + 'show': 'tv', + 'season': 'tv', + 'episode': 'tv', + 'artist': 'music', + 'album': 'music', + 'song': 'music', + 'track': 'music', + 'clip': 'clip', + 'photo': 'photo' +} + + def ConvertPlexToKodiTime(plexTime): """ Converts Plextime to Koditime. Returns an int (in seconds). diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 7c789bce..db7aa97e 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -131,13 +131,9 @@ class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() sleep_between = 50 - if settings('low_powered_device') == 'true': - # Low CPU, potentially issues with limited number of threads - # Hence let Kodi wait till download is successful - timeout = (35.1, 35.1) - else: - # High CPU, no issue with limited number of threads - timeout = (0.01, 0.01) + # Potentially issues with limited number of threads + # Hence let Kodi wait till download is successful + timeout = (35.1, 35.1) def __init__(self, queue): self.queue = queue diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index c77a123a..b92d010d 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -215,12 +215,12 @@ class DownloadUtils(): # THE EXCEPTIONS except requests.exceptions.ConnectionError as e: # Connection error - log.warn("Server unreachable at: %s" % url) - log.warn(e) + log.debug("Server unreachable at: %s" % url) + log.debug(e) - except requests.exceptions.ConnectTimeout as e: - log.warn("Server timeout at: %s" % url) - log.warn(e) + except requests.exceptions.Timeout as e: + log.debug("Server timeout at: %s" % url) + log.debug(e) except requests.exceptions.HTTPError as e: log.warn('HTTP Error at %s' % url) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 0dfdd221..a895ec8c 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -242,14 +242,6 @@ class InitialSetup(): log.warn('The PMS you have used before with a unique ' 'machineIdentifier of %s and name %s is ' 'offline' % (self.serverid, name)) - # "PMS xyz offline" - if settings('show_pms_offline') == 'true': - self.dialog.notification(addonName, - '%s %s' - % (name, lang(39213)), - xbmcgui.NOTIFICATION_ERROR, - 7000, - False) return chk = self._checkServerCon(server) if chk == 504 and httpsUpdated is False: @@ -441,15 +433,6 @@ class InitialSetup(): if settings('InstallQuestionsAnswered') == 'true': return - # Is your Kodi installed on a low-powered device like a Raspberry Pi? - # If yes, then we will reduce the strain on Kodi to prevent it from - # crashing. - if dialog.yesno(heading=addonName, line1=lang(39072)): - settings('low_powered_device', value="true") - settings('syncThreadNumber', value="1") - else: - settings('low_powered_device', value="false") - # Additional settings where the user needs to choose # Direct paths (\\NAS\mymovie.mkv) or addon (http)? goToSettings = False @@ -496,6 +479,9 @@ class InitialSetup(): log.debug("User opted to use FanArtTV") settings('FanartTV', value="true") + # If you use several Plex libraries of one kind, e.g. "Kids Movies" and + # "Parents Movies", be sure to check https://goo.gl/JFtQV9 + dialog.ok(heading=addonName, line1=lang(39076)) # Make sure that we only ask these questions upon first installation settings('InstallQuestionsAnswered', value='true') diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index be88694d..0ddc8351 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -7,12 +7,11 @@ from urllib import urlencode from ntpath import dirname from datetime import datetime -import xbmc import xbmcgui import artwork from utils import tryEncode, tryDecode, settings, window, kodiSQL, \ - CatchExceptions + CatchExceptions, KODIVERSION import embydb_functions as embydb import kodidb_functions as kodidb @@ -36,7 +35,6 @@ class Items(object): """ def __init__(self): - self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) self.directpath = window('useDirectPaths') == 'true' self.artwork = artwork.Artwork() @@ -435,7 +433,7 @@ class Movies(Items): % (itemid, title)) # Update the movie entry - if self.kodiversion > 16: + if KODIVERSION > 16: query = ' '.join(( "UPDATE movie", "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?," @@ -466,7 +464,7 @@ class Movies(Items): ##### OR ADD THE MOVIE ##### else: log.info("ADD movie itemid: %s - Title: %s" % (itemid, title)) - if self.kodiversion > 16: + if KODIVERSION > 16: query = ( ''' INSERT INTO movie( idMovie, idFile, c00, c01, c02, c03, @@ -985,7 +983,7 @@ class TVShows(Items): log.info("UPDATE episode itemid: %s" % (itemid)) # Update the movie entry - if self.kodiversion in (16, 17): + if KODIVERSION in (16, 17): # Kodi Jarvis, Krypton query = ' '.join(( "UPDATE episode", @@ -1018,7 +1016,7 @@ class TVShows(Items): else: log.info("ADD episode itemid: %s - Title: %s" % (itemid, title)) # Create the episode entry - if self.kodiversion in (16, 17): + if KODIVERSION in (16, 17): # Kodi Jarvis, Krypton query = ( ''' @@ -1318,7 +1316,7 @@ class Music(Items): itemid, artistid, artisttype, "artist", checksum=checksum) # Process the artist - if self.kodiversion in (16, 17): + if KODIVERSION in (16, 17): query = ' '.join(( "UPDATE artist", @@ -1411,7 +1409,7 @@ class Music(Items): itemid, albumid, "MusicAlbum", "album", checksum=checksum) # Process the album info - if self.kodiversion == 17: + if KODIVERSION == 17: # Kodi Krypton query = ' '.join(( @@ -1424,7 +1422,7 @@ class Music(Items): kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, "album", studio, albumid)) - elif self.kodiversion == 16: + elif KODIVERSION == 16: # Kodi Jarvis query = ' '.join(( @@ -1437,7 +1435,7 @@ class Music(Items): kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, "album", studio, albumid)) - elif self.kodiversion == 15: + elif KODIVERSION == 15: # Kodi Isengard query = ' '.join(( @@ -1679,7 +1677,7 @@ class Music(Items): log.info("Failed to add album. Creating singles.") kodicursor.execute("select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 - if self.kodiversion == 16: + if KODIVERSION == 16: # Kodi Jarvis query = ( ''' @@ -1689,7 +1687,7 @@ class Music(Items): ''' ) kodicursor.execute(query, (albumid, genre, year, "single")) - elif self.kodiversion == 15: + elif KODIVERSION == 15: # Kodi Isengard query = ( ''' @@ -1767,7 +1765,7 @@ class Music(Items): artist_edb = emby_db.getItem_byId(artist_eid) artistid = artist_edb[0] finally: - if self.kodiversion >= 17: + if KODIVERSION >= 17: # Kodi Krypton query = ( ''' @@ -1842,11 +1840,11 @@ class Music(Items): result = kodicursor.fetchone() if result and result[0] != album_artists: # Field is empty - if self.kodiversion in (16, 17): + if KODIVERSION in (16, 17): # Kodi Jarvis, Krypton query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" kodicursor.execute(query, (album_artists, albumid)) - elif self.kodiversion == 15: + elif KODIVERSION == 15: # Kodi Isengard query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" kodicursor.execute(query, (album_artists, albumid)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index d2950017..f2046fd8 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -3,13 +3,10 @@ ############################################################################### import logging - -import xbmc from ntpath import dirname import artwork -import clientinfo -from utils import settings, kodiSQL +from utils import kodiSQL, KODIVERSION ############################################################################### @@ -43,13 +40,8 @@ class GetKodiDB(): class Kodidb_Functions(): - - kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) - def __init__(self, cursor): self.cursor = cursor - - self.clientInfo = clientinfo.ClientInfo() self.artwork = artwork.Artwork() def pathHack(self): @@ -212,8 +204,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (pathid, filename,)) def addCountries(self, kodiid, countries, mediatype): - - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton for country in countries: query = ' '.join(( @@ -284,85 +275,74 @@ class Kodidb_Functions(): ) self.cursor.execute(query, (idCountry, kodiid)) + def _getactorid(self, name): + """ + Crucial für sync speed! + """ + query = ' '.join(( + "SELECT actor_id", + "FROM actor", + "WHERE name = ?", + "LIMIT 1" + )) + self.cursor.execute(query, (name,)) + try: + actorid = self.cursor.fetchone()[0] + except TypeError: + # Cast entry does not exists + self.cursor.execute("select coalesce(max(actor_id),0) from actor") + actorid = self.cursor.fetchone()[0] + 1 + query = "INSERT INTO actor(actor_id, name) VALUES (?, ?)" + self.cursor.execute(query, (actorid, name)) + return actorid + + def _addPerson(self, role, person_type, actorid, kodiid, mediatype, + castorder): + if "Actor" == person_type: + query = ''' + INSERT OR REPLACE INTO actor_link( + actor_id, media_id, media_type, role, cast_order) + VALUES (?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (actorid, kodiid, mediatype, role, + castorder)) + castorder += 1 + elif "Director" == person_type: + query = ''' + INSERT OR REPLACE INTO director_link( + actor_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (actorid, kodiid, mediatype)) + elif person_type == "Writer": + query = ''' + INSERT OR REPLACE INTO writer_link( + actor_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (actorid, kodiid, mediatype)) + elif "Artist" == person_type: + query = ''' + INSERT OR REPLACE INTO actor_link( + actor_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (actorid, kodiid, mediatype)) + return castorder + def addPeople(self, kodiid, people, mediatype): - castorder = 1 for person in people: - - name = person['Name'] - person_type = person['Type'] - thumb = person['imageurl'] - # Kodi Isengard, Jarvis, Krypton - if self.kodiversion in (15, 16, 17): - query = ' '.join(( - - "SELECT actor_id", - "FROM actor", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - - try: - actorid = self.cursor.fetchone()[0] - - except TypeError: - # Cast entry does not exists - self.cursor.execute("select coalesce(max(actor_id),0) from actor") - actorid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO actor(actor_id, name) values(?, ?)" - self.cursor.execute(query, (actorid, name)) - log.debug("Add people to media, processing: %s" % name) - - finally: - # Link person to content - if "Actor" in person_type: - role = person.get('Role') - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type, role, cast_order) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) - castorder += 1 - - elif "Director" in person_type: - query = ( - ''' - INSERT OR REPLACE INTO director_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) - - elif person_type in ("Writing", "Writer"): - query = ( - ''' - INSERT OR REPLACE INTO writer_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) - - elif "Artist" in person_type: - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) + if KODIVERSION > 14: + actorid = self._getactorid(person['Name']) + # Link person to content + castorder = self._addPerson(person.get('Role'), + person['Type'], + actorid, + kodiid, + mediatype, + castorder) # Kodi Helix else: query = ' '.join(( @@ -372,23 +352,19 @@ class Kodidb_Functions(): "WHERE strActor = ?", "COLLATE NOCASE" )) - self.cursor.execute(query, (name,)) - + self.cursor.execute(query, (person['Name'],)) try: actorid = self.cursor.fetchone()[0] - except TypeError: # Cast entry does not exists self.cursor.execute("select coalesce(max(idActor),0) from actors") actorid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO actors(idActor, strActor) values(?, ?)" - self.cursor.execute(query, (actorid, name)) - log.debug("Add people to media, processing: %s" % name) - + self.cursor.execute(query, (actorid, person['Name'])) finally: # Link person to content - if "Actor" in person_type: + if "Actor" == person['Type']: role = person.get('Role') if "movie" in mediatype: @@ -418,12 +394,13 @@ class Kodidb_Functions(): VALUES (?, ?, ?, ?) ''' ) - else: return # Item is invalid - + else: + # Item is invalid + return self.cursor.execute(query, (actorid, kodiid, role, castorder)) castorder += 1 - elif "Director" in person_type: + elif "Director" == person['Type']: if "movie" in mediatype: query = ( ''' @@ -465,7 +442,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (actorid, kodiid)) - elif person_type in ("Writing", "Writer"): + elif person['Type'] == "Writer": if "movie" in mediatype: query = ( ''' @@ -484,29 +461,25 @@ class Kodidb_Functions(): VALUES (?, ?) ''' ) - else: return # Item is invalid - + else: + # Item is invalid + return self.cursor.execute(query, (actorid, kodiid)) - - elif "Artist" in person_type: + elif "Artist" == person['Type']: query = ( ''' INSERT OR REPLACE INTO artistlinkmusicvideo( idArtist, idMVideo) - VALUES (?, ?) ''' ) self.cursor.execute(query, (actorid, kodiid)) # Add person image to art table - if thumb: - arttype = person_type.lower() - - if "writing" in arttype: - arttype = "writer" - - self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor) + if person['imageurl']: + self.artwork.addOrUpdateArt(person['imageurl'], actorid, + person['Type'].lower(), "thumb", + self.cursor) def existingArt(self, kodiId, mediaType, refresh=False): """ @@ -554,7 +527,7 @@ class Kodidb_Functions(): # Kodi Isengard, Jarvis, Krypton - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Delete current genres for clean slate query = ' '.join(( @@ -667,10 +640,8 @@ class Kodidb_Functions(): self.cursor.execute(query, (idGenre, kodiid)) def addStudios(self, kodiid, studios, mediatype): - for studio in studios: - - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -989,9 +960,8 @@ class Kodidb_Functions(): "DVDPlayer", 1)) def addTags(self, kodiid, tags, mediatype): - # First, delete any existing tags associated to the id - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1016,8 +986,7 @@ class Kodidb_Functions(): self.addTag(kodiid, tag, mediatype) def addTag(self, kodiid, tag, mediatype): - - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1077,9 +1046,8 @@ class Kodidb_Functions(): self.cursor.execute(query, (tag_id, kodiid, mediatype)) def createTag(self, name): - # This will create and return the tag_id - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1123,12 +1091,9 @@ class Kodidb_Functions(): return tag_id def updateTag(self, oldtag, newtag, kodiid, mediatype): - - log.debug("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid)) - - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton - try: + try: query = ' '.join(( "UPDATE tag_link", @@ -1174,8 +1139,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (kodiid, mediatype, oldtag,)) def removeTag(self, kodiid, tagname, mediatype): - - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: # Kodi Isengard, Jarvis, Krypton query = ' '.join(( @@ -1349,7 +1313,7 @@ class Kodidb_Functions(): # Create the album self.cursor.execute("select coalesce(max(idAlbum),0) from album") albumid = self.cursor.fetchone()[0] + 1 - if self.kodiversion in (15, 16, 17): + if KODIVERSION > 14: query = ( ''' INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d17b56d5..5606198f 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -13,7 +13,7 @@ import embydb_functions as embydb import kodidb_functions as kodidb import playbackutils as pbutils from utils import window, settings, CatchExceptions, tryDecode, tryEncode -from PlexFunctions import scrobble +from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE from playlist import Playlist ############################################################################### @@ -51,8 +51,17 @@ class KodiMonitor(xbmc.Monitor): items = { 'logLevel': 'plex_logLevel', 'enableContext': 'plex_context', - 'plex_restricteduser': 'plex_restricteduser' + 'plex_restricteduser': 'plex_restricteduser', + 'dbSyncIndicator': 'dbSyncIndicator', + 'remapSMB': 'remapSMB', + 'replaceSMB': 'replaceSMB', } + # Path replacement + for typus in REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + items[key] = key + # Reset the window variables from the settings variables for settings_value, window_value in items.iteritems(): if window(window_value) != settings(settings_value): log.debug('PKC settings changed: %s is now %s' diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 6db495cc..7b37ce4c 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -15,7 +15,7 @@ from utils import window, settings, getUnixTimestamp, kodiSQL, sourcesXML,\ ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ advancedSettingsXML, getKodiVideoDBPath, tryDecode, deletePlaylists,\ - deleteNodes, ThreadMethodsAdditionalSuspend + deleteNodes, ThreadMethodsAdditionalSuspend, create_actor_db_index import clientinfo import downloadutils import itemtypes @@ -386,12 +386,19 @@ class LibrarySync(Thread): self.syncThreadNumber = int(settings('syncThreadNumber')) self.installSyncDone = settings('SyncInstallRunDone') == 'true' - self.showDbSync = settings('dbSyncIndicator') == 'true' + window('dbSyncIndicator', value=settings('dbSyncIndicator')) self.enableMusic = settings('enableMusic') == "true" self.enableBackgroundSync = settings( 'enableBackgroundSync') == "true" self.limitindex = int(settings('limitindex')) + # Init for replacing paths + window('remapSMB', value=settings('remapSMB')) + window('replaceSMB', value=settings('replaceSMB')) + for typus in PF.REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + window(key, value=settings(key)) # Just in case a time sync goes wrong self.timeoffset = int(settings('kodiplextimeoffset')) window('kodiplextimeoffset', value=str(self.timeoffset)) @@ -407,7 +414,7 @@ class LibrarySync(Thread): forced: always show popup, even if user setting to off """ - if not self.showDbSync: + if settings('dbSyncIndicator') != 'true': if not forced: return if icon == "plex": @@ -551,6 +558,9 @@ class LibrarySync(Thread): # content sync: movies, tvshows, musicvideos, music embyconn.close() + # Create an index for actors to speed up sync + create_actor_db_index() + @LogTime def fullSync(self, repair=False): """ @@ -560,18 +570,32 @@ class LibrarySync(Thread): # True: we're syncing only the delta, e.g. different checksum self.compare = not repair + self.new_items_only = True + log.info('Running fullsync for NEW PMS items with rapair=%s' % repair) + if self._fullSync() is False: + return False + self.new_items_only = False + log.info('Running fullsync for CHANGED PMS items with repair=%s' + % repair) + if self._fullSync() is False: + return False + return True + + def _fullSync(self): xbmc.executebuiltin('InhibitIdleShutdown(true)') screensaver = getScreensaver() setScreensaver(value="") - # Add sources - sourcesXML() + if self.new_items_only is True: + # Only do the following once for new items + # Add sources + sourcesXML() - # Set views. Abort if unsuccessful - if not self.maintainViews(): - xbmc.executebuiltin('InhibitIdleShutdown(false)') - setScreensaver(value=screensaver) - return False + # Set views. Abort if unsuccessful + if not self.maintainViews(): + xbmc.executebuiltin('InhibitIdleShutdown(false)') + setScreensaver(value=screensaver) + return False process = { 'movies': self.PlexMovies, @@ -583,6 +607,8 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: if self.threadStopped(): + xbmc.executebuiltin('InhibitIdleShutdown(false)') + setScreensaver(value=screensaver) return False if not process[itemtype](): xbmc.executebuiltin('InhibitIdleShutdown(false)') @@ -862,14 +888,34 @@ class LibrarySync(Thread): self.allPlexElementsId APPENDED(!!) dict = {itemid: checksum} """ + if self.new_items_only is True: + # Only process Plex items that Kodi does not already have in lib + for item in xml: + itemId = item.attrib.get('ratingKey') + if not itemId: + # Skipping items 'title=All episodes' without a 'ratingKey' + continue + self.allPlexElementsId[itemId] = ("K%s%s" % + (itemId, item.attrib.get('updatedAt', ''))) + if itemId not in self.allKodiElementsId: + self.updatelist.append({ + 'itemId': itemId, + 'itemType': itemType, + 'method': method, + 'viewName': viewName, + 'viewId': viewId, + 'title': item.attrib.get('title', 'Missing Title'), + 'mediaType': item.attrib.get('type') + }) + return + if self.compare: # Only process the delta - new or changed items for item in xml: itemId = item.attrib.get('ratingKey') - # Skipping items 'title=All episodes' without a 'ratingKey' if not itemId: + # Skipping items 'title=All episodes' without a 'ratingKey' continue - title = item.attrib.get('title', 'Missing Title Name') plex_checksum = ("K%s%s" % (itemId, item.attrib.get('updatedAt', ''))) self.allPlexElementsId[itemId] = plex_checksum @@ -883,31 +929,29 @@ class LibrarySync(Thread): 'method': method, 'viewName': viewName, 'viewId': viewId, - 'title': title, + 'title': item.attrib.get('title', 'Missing Title'), 'mediaType': item.attrib.get('type') }) else: # Initial or repair sync: get all Plex movies for item in xml: itemId = item.attrib.get('ratingKey') - # Skipping items 'title=All episodes' without a 'ratingKey' if not itemId: + # Skipping items 'title=All episodes' without a 'ratingKey' continue - title = item.attrib.get('title', 'Missing Title Name') - plex_checksum = ("K%s%s" - % (itemId, item.attrib.get('updatedAt', ''))) - self.allPlexElementsId[itemId] = plex_checksum + self.allPlexElementsId[itemId] = ("K%s%s" + % (itemId, item.attrib.get('updatedAt', ''))) self.updatelist.append({ 'itemId': itemId, 'itemType': itemType, 'method': method, 'viewName': viewName, 'viewId': viewId, - 'title': title, + 'title': item.attrib.get('title', 'Missing Title'), 'mediaType': item.attrib.get('type') }) - def GetAndProcessXMLs(self, itemType, showProgress=True): + def GetAndProcessXMLs(self, itemType): """ Downloads all XMLs for itemType (e.g. Movies, TV-Shows). Processes them by then calling itemtypes.() @@ -959,19 +1003,18 @@ class LibrarySync(Thread): thread.start() threads.append(thread) log.info("Processing thread spawned") - # Start one thread to show sync progress - if showProgress: - if self.showDbSync: - dialog = xbmcgui.DialogProgressBG() - thread = ThreadedShowSyncInfo( - dialog, - [getMetadataLock, processMetadataLock], - itemNumber, - itemType) - thread.setDaemon(True) - thread.start() - threads.append(thread) - log.info("Kodi Infobox thread spawned") + # Start one thread to show sync progress ONLY for new PMS items + if self.new_items_only is True and window('dbSyncIndicator') == 'true': + dialog = xbmcgui.DialogProgressBG() + thread = ThreadedShowSyncInfo( + dialog, + [getMetadataLock, processMetadataLock], + itemNumber, + itemType) + thread.setDaemon(True) + thread.start() + threads.append(thread) + log.info("Kodi Infobox thread spawned") # Wait until finished getMetadataQueue.join() @@ -1349,9 +1392,9 @@ class LibrarySync(Thread): """ typus = message.get('type') if typus == 'playing': - self.process_playing(message['_children']) + self.process_playing(message['PlaySessionStateNotification']) elif typus == 'timeline': - self.process_timeline(message['_children']) + self.process_timeline(message['TimelineEntry']) def multi_delete(self, liste, deleteListe): """ diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index abd695c4..254c33a1 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -59,8 +59,8 @@ class PlayUtils(): playurl = tryEncode(self.API.getTranscodeVideoPath( 'Transcode', quality={ - 'maxVideoBitrate': self.getBitrate(), - 'videoResolution': self.getResolution(), + 'maxVideoBitrate': self.get_bitrate(), + 'videoResolution': self.get_resolution(), 'videoQuality': '100' })) # Set playmethod property @@ -157,34 +157,45 @@ class PlayUtils(): - HEVC codec - window variable 'plex_forcetranscode' set to 'true' (excepting trailers etc.) + - video bitrate above specified settings bitrate if the corresponding file settings are set to 'true' """ videoCodec = self.API.getVideoCodec() log.info("videoCodec: %s" % videoCodec) + if self.API.getType() in ('clip', 'track'): + log.info('Plex clip or music track, not transcoding') + return False + if window('plex_forcetranscode') == 'true': + log.info('User chose to force-transcode') + return True if (settings('transcodeHi10P') == 'true' and videoCodec['bitDepth'] == '10'): log.info('Option to transcode 10bit video content enabled.') return True codec = videoCodec['videocodec'] - if (settings('transcodeHEVC') == 'true' and codec == 'hevc'): - log.info('Option to transcode HEVC video codec enabled.') - return True if codec is None: # e.g. trailers. Avoids TypeError with "'h265' in codec" log.info('No codec from PMS, not transcoding.') return False - if window('plex_forcetranscode') == 'true': - log.info('User chose to force-transcode') + try: + bitrate = int(videoCodec['bitrate']) + except (TypeError, ValueError): + log.info('No video bitrate from PMS, not transcoding.') + return False + if bitrate > self.get_max_bitrate(): + log.info('Video bitrate of %s is higher than the maximal video' + 'bitrate of %s that the user chose. Transcoding' + % (bitrate, self.get_max_bitrate())) return True try: resolution = int(videoCodec['resolution']) except (TypeError, ValueError): log.info('No video resolution from PMS, not transcoding.') return False - if 'h265' in codec: + if 'h265' in codec or 'hevc' in codec: if resolution >= self.getH265(): - log.info("Option to transcode h265 enabled. Resolution of " - "the media: %s, transcoding limit resolution: %s" + log.info("Option to transcode h265/HEVC enabled. Resolution " + "of the media: %s, transcoding limit resolution: %s" % (str(resolution), str(self.getH265()))) return True return False @@ -200,32 +211,47 @@ class PlayUtils(): return False if self.mustTranscode(): return False - # Verify the bitrate - if not self.isNetworkSufficient(): - log.info("The network speed is insufficient to direct stream " - "file. Transcoding") - return False return True - def isNetworkSufficient(self): - """ - Returns True if the network is sufficient (set in file settings) - """ - try: - sourceBitrate = int(self.API.getDataFromPartOrMedia('bitrate')) - except: - log.info('Could not detect source bitrate. It is assumed to be' - 'sufficient') - return True - settings = self.getBitrate() - log.info("The add-on settings bitrate is: %s, the video bitrate" - "required is: %s" % (settings, sourceBitrate)) - if settings < sourceBitrate: - return False - return True - - def getBitrate(self): + def get_max_bitrate(self): # get the addon video quality + videoQuality = settings('maxVideoQualities') + bitrate = { + '0': 320, + '1': 720, + '2': 1500, + '3': 2000, + '4': 3000, + '5': 4000, + '6': 8000, + '7': 10000, + '8': 12000, + '9': 20000, + '10': 40000, + '11': 99999999 # deactivated + } + # max bit rate supported by server (max signed 32bit integer) + return bitrate.get(videoQuality, 2147483) + + def getH265(self): + """ + Returns the user settings for transcoding h265: boundary resolutions + of 480, 720 or 1080 as an int + + OR 9999999 (int) if user chose not to transcode + """ + H265 = { + '0': 99999999, + '1': 480, + '2': 720, + '3': 1080 + } + return H265[settings('transcodeH265')] + + def get_bitrate(self): + """ + Get the desired transcoding bitrate from the settings + """ videoQuality = settings('transcoderVideoQualities') bitrate = { '0': 320, @@ -243,22 +269,10 @@ class PlayUtils(): # max bit rate supported by server (max signed 32bit integer) return bitrate.get(videoQuality, 2147483) - def getH265(self): + def get_resolution(self): """ - Returns the user settings for transcoding h265: boundary resolutions - of 480, 720 or 1080 as an int - - OR 9999999 (int) if user chose not to transcode + Get the desired transcoding resolutions from the settings """ - H265 = { - '0': 9999999, - '1': 480, - '2': 720, - '3': 1080 - } - return H265[settings('transcodeH265')] - - def getResolution(self): chosen = settings('transcoderVideoQualities') res = { '0': '420x420', diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index a7a9fa75..659586b2 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -91,7 +91,7 @@ class UserClient(threading.Thread): if self.machineIdentifier is None: self.machineIdentifier = '' settings('plex_machineIdentifier', value=self.machineIdentifier) - log.info('Returning active server: %s' % server) + log.debug('Returning active server: %s' % server) return server def getSSLverify(self): @@ -104,7 +104,7 @@ class UserClient(threading.Thread): else settings('sslcert') def setUserPref(self): - log.info('Setting user preferences') + log.debug('Setting user preferences') # Only try to get user avatar if there is a token if self.currToken: url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser) @@ -138,7 +138,7 @@ class UserClient(threading.Thread): lang(33007)) def loadCurrUser(self, username, userId, usertoken, authenticated=False): - log.info('Loading current user') + log.debug('Loading current user') doUtils = self.doUtils self.currUserId = userId @@ -148,16 +148,16 @@ class UserClient(threading.Thread): self.sslcert = self.getSSL() if authenticated is False: - log.info('Testing validity of current token') + log.debug('Testing validity of current token') res = PlexAPI.PlexAPI().CheckConnection(self.currServer, token=self.currToken, verifySSL=self.ssl) if res is False: - log.error('Answer from PMS is not as expected. Retrying') + # PMS probably offline return False elif res == 401: - log.warn('Token is no longer valid') - return False + log.error('Token is no longer valid') + return 401 elif res >= 400: log.error('Answer from PMS is not as expected. Retrying') return False @@ -190,23 +190,10 @@ class UserClient(threading.Thread): settings('username', value=username) settings('userid', value=userId) settings('accessToken', value=usertoken) - - dialog = xbmcgui.Dialog() - if settings('connectMsg') == "true": - if username: - dialog.notification( - heading=addonName, - message="Welcome " + username, - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png") - else: - dialog.notification( - heading=addonName, - message="Welcome", - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png") return True def authenticate(self): - log.info('Authenticating user') + log.debug('Authenticating user') dialog = xbmcgui.Dialog() # Give attempts at entering password / selecting user @@ -243,19 +230,22 @@ class UserClient(threading.Thread): enforceLogin = settings('enforceUserLogin') # Found a user in the settings, try to authenticate if username and enforceLogin == 'false': - log.info('Trying to authenticate with old settings') - if self.loadCurrUser(username, - userId, - usertoken, - authenticated=False): + log.debug('Trying to authenticate with old settings') + answ = self.loadCurrUser(username, + userId, + usertoken, + authenticated=False) + if answ is True: # SUCCESS: loaded a user from the settings return True - else: - # Failed to use the settings - delete them! - log.info("Failed to use settings credentials. Deleting them") + elif answ == 401: + log.error("User token no longer valid. Sign user out") settings('username', value='') settings('userid', value='') settings('accessToken', value='') + else: + log.debug("Could not yet authenticate user") + return False plx = PlexAPI.PlexAPI() @@ -288,7 +278,7 @@ class UserClient(threading.Thread): return False def resetClient(self): - log.info("Reset UserClient authentication.") + log.debug("Reset UserClient authentication.") self.doUtils.stopSession() window('plex_authenticated', clear=True) @@ -365,7 +355,7 @@ class UserClient(threading.Thread): # Or retried too many times if server and status != "Stop": # Only if there's information found to login - log.info("Server found: %s" % server) + log.debug("Server found: %s" % server) self.auth = True # Minimize CPU load diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 34a63a1f..a0a5179a 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -29,6 +29,7 @@ WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) +KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) ############################################################################### # Main methods @@ -227,6 +228,25 @@ def getKodiVideoDBPath(): % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], ""))) return dbPath + +def create_actor_db_index(): + """ + Index the "actors" because we got a TON - speed up SELECT and WHEN + """ + conn = kodiSQL('video') + cursor = conn.cursor() + try: + cursor.execute(""" + CREATE UNIQUE INDEX index_name + ON actor (name); + """) + except sqlite3.OperationalError: + # Index already exists + pass + conn.commit() + conn.close() + + def getKodiMusicDBPath(): dbVersion = { @@ -402,7 +422,7 @@ def profiling(sortby="cumulative"): s = StringIO.StringIO() ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() - log.debug(s.getvalue()) + log.info(s.getvalue()) return result @@ -835,7 +855,8 @@ def LogTime(func): starttotal = datetime.now() result = func(*args, **kwargs) elapsedtotal = datetime.now() - starttotal - log.debug('It took %s to run the function.' % (elapsedtotal)) + log.info('It took %s to run the function %s' + % (elapsedtotal, func.__name__)) return result return wrapper diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 5e7afaa1..0aed244e 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -10,7 +10,7 @@ import xbmc import xbmcvfs from utils import window, settings, language as lang, IfExists, tryDecode, \ - tryEncode, indent, normalize_nodes + tryEncode, indent, normalize_nodes, KODIVERSION ############################################################################### @@ -21,9 +21,6 @@ log = logging.getLogger("PLEX."+__name__) class VideoNodes(object): - def __init__(self): - self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - def commonRoot(self, order, label, tagname, roottype=1): if roottype == 0: @@ -235,7 +232,7 @@ class VideoNodes(object): # Custom query path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s" % (viewid, mediatype, tagname, limit)) - elif self.kodiversion == 14 and nodetype == "inprogressepisodes": + elif KODIVERSION == 14 and nodetype == "inprogressepisodes": # Custom query path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit) elif nodetype == 'ondeck': @@ -252,7 +249,7 @@ class VideoNodes(object): if mediatype == "photos": windowpath = "ActivateWindow(Pictures,%s,return)" % path else: - if self.kodiversion >= 17: + if KODIVERSION >= 17: # Krypton windowpath = "ActivateWindow(Videos,%s,return)" % path else: @@ -374,7 +371,7 @@ class VideoNodes(object): "special://profile/library/video/")) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) path = "library://video/plex_%s.xml" % cleantagname - if self.kodiversion >= 17: + if KODIVERSION >= 17: # Krypton windowpath = "ActivateWindow(Videos,%s,return)" % path else: diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 898941f0..cc81a617 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -41,7 +41,11 @@ class WebSocket(threading.Thread): log.error('Error decoding message from websocket: %s' % ex) log.error(message) return False - + try: + message = message['NotificationContainer'] + except KeyError: + log.error('Could not parse PMS message: %s' % message) + return False # Triage typus = message.get('type') if typus is None: @@ -139,7 +143,7 @@ class WebSocket(threading.Thread): log.info("Error connecting") self.ws = None counter += 1 - if counter > 10: + if counter > 3: log.warn("Repeatedly could not connect to PMS, " "declaring the connection dead") window('plex_online', value='false') diff --git a/resources/settings.xml b/resources/settings.xml index dd533c4a..5f1d2456 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -49,7 +49,6 @@ - @@ -68,7 +67,6 @@ - @@ -100,10 +98,10 @@ - - + + + - @@ -131,7 +129,6 @@ --> - diff --git a/service.py b/service.py index 16dd13ed..35499474 100644 --- a/service.py +++ b/service.py @@ -58,7 +58,6 @@ addonName = 'PlexKodiConnect' class Service(): - welcome_msg = True server_online = True warn_auth = True @@ -87,8 +86,8 @@ class Service(): log.warn("%s Version: %s" % (addonName, self.clientInfo.getVersion())) log.warn("Using plugin paths: %s" % (settings('useDirectPaths') != "true")) - log.warn("Using a low powered device: %s" - % settings('low_powered_device')) + log.warn("Number of sync threads: %s" + % settings('syncThreadNumber')) log.warn("Log Level: %s" % logLevel) # Reset window props for profile switch @@ -133,14 +132,13 @@ class Service(): # Queue for background sync queue = Queue.Queue() - connectMsg = True if settings('connectMsg') == 'true' else False - # Initialize important threads user = userclient.UserClient() ws = wsc.WebSocket(queue) library = librarysync.LibrarySync(queue) plx = PlexAPI.PlexAPI() + welcome_msg = True counter = 0 while not monitor.abortRequested(): @@ -163,9 +161,9 @@ class Service(): if not self.kodimonitor_running: # Start up events self.warn_auth = True - if connectMsg and self.welcome_msg: + if welcome_msg is True: # Reset authentication warnings - self.welcome_msg = False + welcome_msg = False xbmcgui.Dialog().notification( heading=addonName, message="%s %s" % (lang(33000), user.currUser), @@ -221,21 +219,22 @@ class Service(): # Server is offline or cannot be reached # Alert the user and suppress future warning if self.server_online: - log.error("Server is offline.") + self.server_online = False window('plex_online', value="false") # Suspend threads window('suspend_LibraryThread', value='true') - xbmcgui.Dialog().notification( - heading=lang(33001), - message="%s %s" - % (addonName, lang(33002)), - icon="special://home/addons/plugin.video." - "plexkodiconnect/icon.png", - sound=False) - self.server_online = False + log.error("Plex Media Server went offline") + if settings('show_pms_offline') == 'true': + xbmcgui.Dialog().notification( + heading=lang(33001), + message="%s %s" + % (addonName, lang(33002)), + icon="special://home/addons/plugin.video." + "plexkodiconnect/icon.png", + sound=False) counter += 1 # Periodically check if the IP changed, e.g. per minute - if counter > 30: + if counter > 20: counter = 0 setup = initialsetup.InitialSetup() tmp = setup.PickPMS() @@ -250,16 +249,18 @@ class Service(): if monitor.waitForAbort(5): # Abort was requested while waiting. break + self.server_online = True # Alert the user that server is online. - xbmcgui.Dialog().notification( - heading=addonName, - message=lang(33003), - icon="special://home/addons/plugin.video." - "plexkodiconnect/icon.png", - time=5000, - sound=False) - self.server_online = True - log.warn("Server %s is online and ready." % server) + if (welcome_msg is False and + settings('show_pms_offline') == 'true'): + xbmcgui.Dialog().notification( + heading=addonName, + message=lang(33003), + icon="special://home/addons/plugin.video." + "plexkodiconnect/icon.png", + time=5000, + sound=False) + log.info("Server %s is online and ready." % server) window('plex_online', value="true") if window('plex_authenticated') == 'true': # Server got offline when we were authenticated. @@ -273,7 +274,7 @@ class Service(): break - if monitor.waitForAbort(2): + if monitor.waitForAbort(3): # Abort was requested while waiting. break From 17fc11d8bb492a6f699fa4bda701493bd6661905 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 20 Dec 2016 16:27:22 +0100 Subject: [PATCH 03/50] Optimize websocket client queue --- resources/lib/websocket_client.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index cc81a617..535b941a 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -2,9 +2,8 @@ ############################################################################### import logging -import json +from json import loads import threading -import Queue import websocket import ssl @@ -36,7 +35,7 @@ class WebSocket(threading.Thread): 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 +56,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 From 9d2cf86091732d58bc0e0d6a5e1024215d50f2fe Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 20 Dec 2016 16:30:22 +0100 Subject: [PATCH 04/50] Optimize websocket imports --- resources/lib/websocket_client.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 535b941a..d83b9ce0 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -2,12 +2,12 @@ ############################################################################### import logging -from json import loads -import threading import websocket -import ssl +from json import loads +from threading import Thread +from ssl import CERT_NONE -import xbmc +from xbmc import sleep from utils import window, settings, ThreadMethodsAdditionalSuspend, \ ThreadMethods @@ -21,14 +21,14 @@ 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): self.ws = None # Communication with librarysync self.queue = queue - threading.Thread.__init__(self) + Thread.__init__(self) def process(self, opcode, message): if opcode not in self.opcode_data: @@ -91,7 +91,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 @@ -116,7 +116,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: @@ -142,11 +142,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: @@ -156,14 +156,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 From 95c87065ed514c56dec442143ad6aaf9c112e24b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 20 Dec 2016 16:38:04 +0100 Subject: [PATCH 05/50] Optimize companion imports --- resources/lib/PlexCompanion.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 25c4e7de..26b9132a 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -1,11 +1,10 @@ # -*- 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, \ @@ -24,7 +23,7 @@ log = logging.getLogger("PLEX."+__name__) @ThreadMethodsAdditionalSuspend('plex_serverStatus') @ThreadMethods -class PlexCompanion(threading.Thread): +class PlexCompanion(Thread): """ Initialize with a Queue for callbacks """ @@ -41,7 +40,7 @@ class PlexCompanion(threading.Thread): # kodi player instance self.player = player.Player() - threading.Thread.__init__(self) + Thread.__init__(self) def _getStartItem(self, string): """ @@ -151,9 +150,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 +168,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 +177,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 +202,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 +215,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: From 0c2d4984ab02988ba1d7cb30c842f598dc4c7a57 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 27 Dec 2016 17:33:52 +0100 Subject: [PATCH 06/50] Playqueues major haul-over --- resources/lib/PlexCompanion.py | 44 +-- resources/lib/PlexFunctions.py | 3 +- resources/lib/entrypoint.py | 1 - resources/lib/initialsetup.py | 5 +- resources/lib/kodimonitor.py | 8 +- resources/lib/librarysync.py | 16 +- resources/lib/playbackutils.py | 9 +- resources/lib/playlist.py | 565 ------------------------------ resources/lib/playlist_func.py | 394 +++++++++++++++++++++ resources/lib/playqueue.py | 150 ++++++++ resources/lib/userclient.py | 4 +- resources/lib/utils.py | 50 ++- resources/lib/websocket_client.py | 7 +- service.py | 95 ++--- 14 files changed, 673 insertions(+), 678 deletions(-) delete mode 100644 resources/lib/playlist.py create mode 100644 resources/lib/playlist_func.py create mode 100644 resources/lib/playqueue.py diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 26b9132a..ddff3abf 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -11,7 +11,6 @@ from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings from PlexFunctions import ParseContainerKey, GetPlayQueue, \ ConvertPlexToKodiTime -import playlist import player ############################################################################### @@ -25,18 +24,18 @@ log = logging.getLogger("PLEX."+__name__) @ThreadMethods 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.playqueue = self.mgr.playqueue 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() @@ -72,49 +71,44 @@ class PlexCompanion(Thread): data = task['data'] if 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(): + self.mgr.playqueue.update_playqueue_with_companion(data) + + self.playqueue = self.mgr.playqueue.get_playqueue_from_plextype( + data.get('type')) + if queueId != self.playqueue.ID: 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() + self.playqueue.clear() # Set new values - self.playlist.QueueId(queueId) - self.playlist.PlayQueueVersion(int( + self.playqueue.QueueId(queueId) + self.playqueue.PlayQueueVersion(int( xml.attrib.get('playQueueVersion'))) - self.playlist.Guid(xml.attrib.get('guid')) + self.playqueue.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( + self.playqueue.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())) + % (self.playqueue.QueueId(), + self.playqueue.PlayQueueVersion())) else: log.error('This has never happened before!') @@ -129,7 +123,7 @@ class PlexCompanion(Thread): requestMgr = httppersist.RequestMgr() jsonClass = functions.jsonClass(requestMgr, self.settings) subscriptionManager = subscribers.SubscriptionManager( - jsonClass, requestMgr, self.player, self.playlist) + jsonClass, requestMgr, self.player, self.playqueue) queue = Queue.Queue(maxsize=100) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index f93ffb47..2705e0a6 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -60,11 +60,12 @@ KODITYPE_FROM_PLEXTYPE = { 'XXXXXXX': 'genre' } -KODIAUDIOVIDEO_FROM_MEDIA_TYPE = { +KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { 'movie': 'video', 'episode': 'video', 'season': 'video', 'tvshow': 'video', + 'clip': 'video', 'artist': 'audio', 'album': 'audio', 'track': 'audio', diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 75fcd2d5..de68e73f 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -19,7 +19,6 @@ import clientinfo import downloadutils import embydb_functions as embydb import playbackutils as pbutils -import playlist import PlexFunctions import PlexAPI 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/kodimonitor.py b/resources/lib/kodimonitor.py index 5606198f..09c42445 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,7 +14,6 @@ import kodidb_functions as kodidb import playbackutils as pbutils from utils import window, settings, CatchExceptions, tryDecode, tryEncode from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE -from playlist import Playlist ############################################################################### @@ -25,11 +24,11 @@ log = logging.getLogger("PLEX."+__name__) class KodiMonitor(xbmc.Monitor): - def __init__(self): - + def __init__(self, callback): + self.mgr = callback self.doUtils = downloadutils.DownloadUtils().downloadUrl self.xbmcplayer = xbmc.Player() - self.playlist = Playlist('video') + self.playqueue = self.mgr.playqueue xbmc.Monitor.__init__(self) log.info("Kodi monitor started.") @@ -173,7 +172,6 @@ class KodiMonitor(xbmc.Monitor): # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, # u'position': 0} self.playlist.kodi_onadd(data) - Playlist() def PlayBackStart(self, data): """ diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 7b37ce4c..9b6f8cc4 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -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() @@ -1720,7 +1711,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/playbackutils.py b/resources/lib/playbackutils.py index 8205ff3e..40a984f9 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -11,7 +11,7 @@ import xbmcgui import xbmcplugin import playutils as putils -import playlist +from playqueue import Playqueue from utils import window, settings, tryEncode, tryDecode import downloadutils @@ -37,10 +37,7 @@ class PlaybackUtils(): 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') + self.pl = Playqueue().get_playqueue_from_plextype(self.API.getType()) def play(self, itemid, dbid=None): @@ -89,7 +86,7 @@ class PlaybackUtils(): contextmenu_play = window('plex_contextplay') == 'true' window('plex_contextplay', clear=True) homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - kodiPl = self.pl.playlist + kodiPl = self.pl.kodi_pl sizePlaylist = kodiPl.size() if contextmenu_play: # Need to start with the items we're inserting here diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py deleted file mode 100644 index 3e055156..00000000 --- a/resources/lib/playlist.py +++ /dev/null @@ -1,565 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################### - -import logging -import json -from urllib import urlencode -from threading import Lock -from functools import wraps -from urllib import quote, urlencode - -import xbmc - -import embydb_functions as embydb -import kodidb_functions as kodidb -from utils import window, tryEncode, JSONRPC -import playbackutils -import PlexFunctions as PF -import PlexAPI -from downloadutils import DownloadUtils - -############################################################################### - -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - -PLEX_PLAYQUEUE_ARGS = ( - 'playQueueID', - 'playQueueVersion', - 'playQueueSelectedItemID', - 'playQueueSelectedItemOffset' -) - - -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') - - ATTRIBUTES: - id: integer - position: integer, default -1 - type: string, default "unknown" - "unknown", - "video", - "audio", - "picture", - "mixed" - size: integer - """ - # Borg - multiple instances, shared state - _shared_state = {} - - player = xbmc.Player() - - playlists = None - - @lockMethod.decorate - def __init__(self, typus=None): - # Borg - self.__dict__ = self._shared_state - - # If already initiated, return - if self.playlists is not None: - return - - self.doUtils = DownloadUtils().downloadUrl - # Get all playlists from Kodi - self.playlists = JSONRPC('Playlist.GetPlaylists').execute() - try: - self.playlists = self.playlists['result'] - except KeyError: - log.error('Could not get Kodi playlists. JSON Result was: %s' - % self.playlists) - self.playlists = None - return - # Example return: [{u'playlistid': 0, u'type': u'audio'}, - # {u'playlistid': 1, u'type': u'video'}, - # {u'playlistid': 2, u'type': u'picture'}] - # Initiate the Kodi playlists - for playlist in self.playlists: - # Initialize each Kodi playlist - if playlist['type'] == 'audio': - playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - elif playlist['type'] == 'video': - playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - else: - # Currently, only video or audio playlists available - playlist['kodi_pl'] = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - - # Initialize Plex info on the playQueue - for arg in PLEX_PLAYQUEUE_ARGS: - playlist[arg] = None - - # Build a list of all items within each playlist - playlist['items'] = [] - for item in self._get_kodi_items(playlist['playlistid']): - playlist['items'].append({ - 'kodi_id': item.get('id'), - 'type': item['type'], - 'file': item['file'], - 'playQueueItemID': None, - 'plex_id': self._get_plexid(item) - }) - log.debug('self.playlist: %s' % playlist) - - def _init_pl_item(self): - return { - 'plex_id': None, - 'kodi_id': None, - 'file': None, - 'type': None, # 'audio' or 'video' - 'playQueueItemID': None, - 'uri': None, - # To be able to drag Kodi JSON data along: - 'playlistid': None, - 'position': None, - 'item': None, - } - - def _get_plexid(self, item): - """ - Supply with data['item'] as returned from Kodi JSON-RPC interface - """ - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(item.get('id'), - item.get('type')) - try: - plex_id = emby_dbitem[0] - except TypeError: - plex_id = None - return plex_id - - def _get_kodi_items(self, playlistid): - params = { - 'playlistid': playlistid, - 'properties': ["title", "file"] - } - answ = JSONRPC('Playlist.GetItems').execute(params) - # returns 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'}] - try: - answ = answ['result']['items'] - except KeyError: - answ = [] - return answ - - @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 = PF.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 = PF.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))) - - def _get_uri(self, plex_id=None, item=None): - """ - Supply with either plex_id or data['item'] as received from Kodi JSON- - RPC - """ - uri = None - if plex_id is None: - plex_id = self._get_plexid(item) - self._cur_item['plex_id'] = plex_id - if plex_id is not None: - xml = PF.GetPlexMetadata(plex_id) - try: - uri = ('library://%s/item/%s%s' % - (xml.attrib.get('librarySectionUUID'), - quote('library/metadata/', safe=''), plex_id)) - except: - pass - if uri is None: - try: - uri = 'library://whatever/item/%s' % quote(item['file'], - safe='') - except: - raise KeyError('Could not get file/url with item: %s' % item) - self._cur_item['uri'] = uri - return uri - - def _init_plex_playQueue(self, plex_id=None, data=None): - """ - Supply either plex_id or the data supplied by Kodi JSON-RPC - """ - if plex_id is None: - plex_id = self._get_plexid(data['item']) - self._cur_item['plex_id'] = plex_id - - if data is not None: - playlistid = data['playlistid'] - plex_type = self.playlists[playlistid]['type'] - else: - with embydb.GetEmbyDB() as emby_db: - plex_type = emby_db.getItem_byId(plex_id) - try: - plex_type = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[plex_type[4]] - except TypeError: - raise KeyError('Unknown plex_type %s' % plex_type) - for playlist in self.playlists: - if playlist['type'] == plex_type: - playlistid = playlist['playlistid'] - self._cur_item['playlistid'] = playlistid - self._cur_item['type'] = plex_type - - params = { - 'next': 0, - 'type': plex_type, - 'uri': self._get_uri(plex_id=plex_id, item=data['item']) - } - log.debug('params: %s' % urlencode(params)) - xml = self.doUtils(url="{server}/playQueues", - action_type="POST", - parameters=params) - try: - xml.attrib - except (TypeError, AttributeError): - raise KeyError('Could not post to PMS, received: %s' % xml) - self._Plex_item_updated(xml) - - def _Plex_item_updated(self, xml): - """ - Called if a new item has just been added/updated @ Plex playQueue - - Call with the PMS' xml reply - """ - # Update the ITEM - log.debug('xml.attrib: %s' % xml.attrib) - args = { - 'playQueueItemID': 'playQueueLastAddedItemID', # for playlist PUT - 'playQueueItemID': 'playQueueSelectedItemID' # for playlist INIT - } - for old, new in args.items(): - if new in xml.attrib: - self._cur_item[old] = xml.attrib[new] - # Update the PLAYLIST - for arg in PLEX_PLAYQUEUE_ARGS: - if arg in xml.attrib: - self.playlists[self._cur_item['playlistid']][arg] = xml.attrib[arg] - - def _init_Kodi_item(self, item): - """ - Call with Kodi's JSON-RPC data['item'] - """ - self._cur_item['kodi_id'] = item.get('id') - try: - self._cur_item['type'] = PF.KODIAUDIOVIDEO_FROM_MEDIA_TYPE[ - item.get('type')] - except KeyError: - log.error('Could not get media_type for %s' % item) - - def _add_curr_item(self): - self.playlists[self._cur_item['playlistid']]['items'].insert( - self._cur_item['position'], - self._cur_item) - - @lockMethod.decorate - def kodi_onadd(self, data): - """ - Called if Kodi playlist is modified. Data is Kodi JSON-RPC output, e.g. - { - u'item': {u'type': u'movie', u'id': 3}, - u'playlistid': 1, - u'position': 0 - } - """ - self._cur_item = self._init_pl_item() - self._cur_item.update(data) - self._init_Kodi_item(data['item']) - - pl = self.playlists[data['playlistid']] - if pl['playQueueID'] is None: - # Playlist needs to be initialized! - try: - self._init_plex_playQueue(data=data) - except KeyError as e: - log.error('Error encountered while init playQueue: %s' % e) - return - else: - next_item = data['position'] - if next_item != 0: - next_item = pl['items'][data['position']-1]['playQueueItemID'] - params = { - 'next': next_item, - 'type': pl['type'], - 'uri': self._get_uri(item=data['item']) - } - xml = self.doUtils(url="{server}/playQueues/%s" - % pl['playQueueID'], - action_type="PUT", - parameters=params) - try: - xml.attrib - except AttributeError: - log.error('Could not add item %s to playQueue' % data) - return - self._Plex_item_updated(xml) - # Add the new item to our playlist - self._add_curr_item() - log.debug('self.playlists are now: %s' % self.playlists) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py new file mode 100644 index 00000000..6dc6a02c --- /dev/null +++ b/resources/lib/playlist_func.py @@ -0,0 +1,394 @@ +import logging +from urllib import quote + +import embydb_functions as embydb +from downloadutils import DownloadUtils as DU +from utils import JSONRPC, tryEncode + +############################################################################### + +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +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 object: " % (self.__class__.__name__) + for key in self.__dict__: + answ += '%s: %s, ' % (key, getattr(self, key)) + return answ[:-2] + ">" + + +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 + + +def playlist_item_from_kodi_item(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() + if kodi_item.get('id'): + item.kodi_id = kodi_item['id'] + with embydb.GetEmbyDB() as emby_db: + emby_dbitem = emby_db.getItem_byKodiId(kodi_item['id'], + kodi_item['type']) + try: + item.plex_id = emby_dbitem[0] + item.plex_UUID = emby_dbitem[0] + except TypeError: + pass + item.file = kodi_item.get('file') if kodi_item.get('file') else None + item.kodi_type = kodi_item.get('type') if kodi_item.get('type') else None + if item.plex_id is None: + item.uri = 'library://whatever/item/%s' % quote(item.file, safe='') + else: + 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") + """ + item = Playlist_Item() + item.plex_id = plex_id + with embydb.GetEmbyDB() as emby_db: + emby_dbitem = emby_db.getItem_byId(plex_id) + try: + item.kodi_id = emby_dbitem[0] + item.kodi_type = emby_dbitem[4] + except: + raise KeyError('Could not find plex_id %s in database' % plex_id) + 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: %s' % 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.selectedItemID = xml.attrib['%sSelectedItemID' % playlist.kind] + playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' % playlist.kind] + playlist.shuffled = xml.attrib['%sShuffled' % 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 + + +def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): + """ + Supply either plex_id or the data supplied by Kodi JSON-RPC + """ + if plex_id is not None: + item = playlist_item_from_plex(plex_id) + else: + item = playlist_item_from_kodi_item(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(xml) + playlist.items.append(item) + log.debug('Initialized the playlist: %s' % playlist) + + +def add_playlist_item(playlist, kodi_item, after_pos): + """ + Adds the new kodi_item to playlist after item at position after_pos + [int] + """ + item = playlist_item_from_kodi_item(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 playlist + xml = DU().downloadUrl(url, action_type="PUT") + try: + item.ID = xml.attrib['%sLastAddedItemID' % playlist.kind] + except (TypeError, AttributeError, KeyError): + log.error('Could not add item %s to playlist %s' + % (kodi_item, playlist)) + _log_xml(xml) + return + playlist.items.append(item) + if after_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, + after_pos) + + +def move_playlist_item(playlist, before_pos, after_pos): + """ + Moves playlist item from before_pos [int] to after_pos [int] + """ + 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)) + + +def delete_playlist_item(playlist, pos): + """ + Delete the item at position pos [int] + """ + 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 insertintoPlaylist(self, + position, + dbid=None, + mediatype=None, + url=None): + params = { + 'playlistid': self.playlistId, + 'position': position + } + if dbid is not None: + params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} + else: + params['item'] = {'file': url} + JSONRPC('Playlist.Insert').execute(params) + + +def addtoPlaylist(self, dbid=None, mediatype=None, url=None): + params = { + 'playlistid': self.playlistId + } + if dbid is not None: + params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} + else: + params['item'] = {'file': url} + JSONRPC('Playlist.Add').execute(params) + + +def removefromPlaylist(self, position): + params = { + 'playlistid': self.playlistId, + 'position': position + } + JSONRPC('Playlist.Remove').execute(params) + + +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) + + +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 = PF.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) + +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) + + +def clear(self): + """ + Empties current Kodi playlist and associated variables + """ + self.playlist.clear() + self.items = [] + self.queueId = None + self.playQueueVersion = None + self.guid = None + log.info('Playlist cleared') \ No newline at end of file diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py new file mode 100644 index 00000000..997c6bef --- /dev/null +++ b/resources/lib/playqueue.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Lock, Thread + +import xbmc + +from utils import ThreadMethods, ThreadMethodsAdditionalSuspend, Lock_Function +import playlist_func as PL +from PlexFunctions import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, GetPlayQueue, \ + ParseContainerKey + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +# Lock used to lock methods +lock = Lock() +lockmethod = Lock_Function(lock) +############################################################################### + + +@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 + + @lockmethod.lockthis + def __init__(self, callback=None): + self.__dict__ = self.__shared_state + Thread.__init__(self) + if self.playqueues is not None: + return + self.mgr = callback + + # Initialize Kodi playqueues + 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 = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + elif playqueue.type == 'video': + playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + else: + # Currently, only video or audio playqueues available + playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + self.playqueues.append(playqueue) + log.debug('Initialized the Kodi play queues: %s' % self.playqueues) + + @lockmethod.lockthis + def update_playqueue_with_companion(self, data): + """ + Feed with Plex companion data + """ + + # Get the correct queue + for playqueue in self.playqueues: + if playqueue.type == KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[ + data['type']]: + break + + @lockmethod.lockthis + def kodi_onadd(self, data): + """ + Called if an item is added to a Kodi playqueue. Data is Kodi JSON-RPC + output, e.g. + { + u'item': {u'type': u'movie', u'id': 3}, + u'playlistid': 1, + u'position': 0 + } + """ + for playqueue in self.playqueues: + if playqueue.playlistid == data['playlistid']: + break + if playqueue.ID is None: + # Need to initialize the queue for the first time + PL.init_Plex_playlist(playqueue, kodi_item=data['item']) + else: + PL.add_playlist_item(playqueue, data['item'], data['position']) + + @lockmethod.lockthis + def _compare_playqueues(self, playqueue, new): + """ + Used to poll the Kodi playqueue and update the Plex playqueue if needed + """ + old = playqueue.old_kodi_pl + log.debug('Comparing new Kodi playqueue %s with our play queue %s' + % (new, playqueue)) + index = list(range(0, len(old))) + for i, new_item in enumerate(new): + for j, old_item in enumerate(old): + if old_item.get('id') is None: + identical = old_item['file'] == new_item['file'] + else: + identical = (old_item['id'] == new_item['id'] and + old_item['type'] == new_item['type']) + if j == 0 and identical: + del old[j], index[j] + break + elif identical: + # item now at pos i has been moved from original pos i+j + PL.move_playlist_item(playqueue, i + j, i) + # Delete the item we just found + del old[i + j], index[i + j] + break + else: + # Did not find element i in the old list - Kodi monitor should + # pick this up! + # PL.add_playlist_item(playqueue, new_item, i-1) + pass + for i in index: + # Still got some old items left that need deleting + PL.delete_playlist_item(playqueue, i) + log.debug('New playqueue: %s' % playqueue) + + 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_playlist_item(playqueue, item, i) + while not threadStopped(): + while threadSuspended(): + if threadStopped(): + break + xbmc.sleep(1000) + for playqueue in self.playqueues: + if not playqueue.items: + # Skip empty playqueues as items can't be modified + continue + 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) + xbmc.sleep(1000) + log.info("----===## PlayQueue client stopped ##===----") 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 a0a5179a..be6d96d3 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -133,21 +133,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): @@ -938,9 +938,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 diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index d83b9ce0..312080b5 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -5,6 +5,7 @@ import logging import websocket from json import loads from threading import Thread +from Queue import Queue from ssl import CERT_NONE from xbmc import sleep @@ -24,10 +25,12 @@ log = logging.getLogger("PLEX."+__name__) 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 + self.queue = Queue() Thread.__init__(self) def process(self, opcode, message): diff --git a/service.py b/service.py index 35499474..c5bf737d 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,18 @@ 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 ############################################################################### @@ -61,11 +61,18 @@ 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 def __init__(self): @@ -96,7 +103,7 @@ 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", @@ -129,13 +136,13 @@ class Service(): # 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) - # Initialize important threads - user = userclient.UserClient() - ws = wsc.WebSocket(queue) - library = librarysync.LibrarySync(queue) plx = PlexAPI.PlexAPI() welcome_msg = True @@ -157,7 +164,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 +173,43 @@ 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() 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 +223,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 +280,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 +298,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: From 9d2902baa5caec9d1c7e7150687767cd29f93e02 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 13:14:21 +0100 Subject: [PATCH 07/50] Playqueues overhaul continued --- resources/lib/PlexAPI.py | 2 +- resources/lib/PlexCompanion.py | 59 +++----- resources/lib/PlexFunctions.py | 16 -- resources/lib/kodimonitor.py | 4 +- resources/lib/playbackutils.py | 3 +- resources/lib/playlist_func.py | 167 ++++++++++++++------- resources/lib/playqueue.py | 76 +++++++++- resources/lib/plexbmchelper/subscribers.py | 28 ++-- 8 files changed, 216 insertions(+), 139 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b63bf71a..06aa9484 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1646,7 +1646,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): """ diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index ddff3abf..002a6f91 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -9,8 +9,7 @@ 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 +from PlexFunctions import ParseContainerKey import player ############################################################################### @@ -29,7 +28,6 @@ class PlexCompanion(Thread): log.info("----===## Starting PlexCompanion ##===----") if callback is not None: self.mgr = callback - self.playqueue = self.mgr.playqueue self.settings = plexsettings.getSettings() # Start GDM for server/client discovery self.client = plexgdm.plexgdm() @@ -60,12 +58,18 @@ class PlexCompanion(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'] @@ -79,36 +83,11 @@ class PlexCompanion(Thread): import traceback log.error("Traceback:\n%s" % traceback.format_exc()) return - self.mgr.playqueue.update_playqueue_with_companion(data) - - self.playqueue = self.mgr.playqueue.get_playqueue_from_plextype( - data.get('type')) - if queueId != self.playqueue.ID: - 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.playqueue.clear() - # Set new values - self.playqueue.QueueId(queueId) - self.playqueue.PlayQueueVersion(int( - xml.attrib.get('playQueueVersion'))) - self.playqueue.Guid(xml.attrib.get('guid')) - items = [] - for item in xml: - items.append({ - 'playQueueItemID': item.get('playQueueItemID'), - 'plexId': item.get('ratingKey'), - 'kodiId': None}) - self.playqueue.playAll( - items, - startitem=self._getStartItem(data.get('key', '')), - offset=ConvertPlexToKodiTime(data.get('offset', 0))) - log.info('Initiated playlist no %s with version %s' - % (self.playqueue.QueueId(), - self.playqueue.PlayQueueVersion())) + playqueue = self.mgr.playqueue.get_playqueue_from_type( + data['type']) + if ID != playqueue.ID: + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, ID, int(query['repeat'])) else: log.error('This has never happened before!') @@ -123,7 +102,7 @@ class PlexCompanion(Thread): requestMgr = httppersist.RequestMgr() jsonClass = functions.jsonClass(requestMgr, self.settings) subscriptionManager = subscribers.SubscriptionManager( - jsonClass, requestMgr, self.player, self.playqueue) + jsonClass, requestMgr, self.player, self.mgr) queue = Queue.Queue(maxsize=100) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 2705e0a6..621aae90 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -171,22 +171,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. diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 09c42445..09d797fd 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -168,10 +168,10 @@ class KodiMonitor(xbmc.Monitor): pass elif method == "Playlist.OnAdd": - # User manipulated Kodi playlist + # User (or PKC) manipulated Kodi playlist # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, # u'position': 0} - self.playlist.kodi_onadd(data) + self.playqueue.kodi_onadd(data) def PlayBackStart(self, data): """ diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 40a984f9..17342df7 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -37,7 +37,8 @@ class PlaybackUtils(): self.userid = window('currUserId') self.server = window('pms_server') - self.pl = Playqueue().get_playqueue_from_plextype(self.API.getType()) + self.pl = Playqueue().get_playqueue_from_type( + PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.API.getType()]) def play(self, itemid, dbid=None): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 6dc6a02c..2b243899 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -4,6 +4,7 @@ from urllib import quote import embydb_functions as embydb from downloadutils import DownloadUtils as DU from utils import JSONRPC, tryEncode +from PlexAPI import API ############################################################################### @@ -24,13 +25,35 @@ class Playlist_Object_Baseclase(object): selectedItemOffset = None shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? + # Hack to later ignore all Kodi playlist adds that PKC did (Kodimonitor) + PKC_playlist_edits = [] def __repr__(self): - answ = "<%s object: " % (self.__class__.__name__) + answ = "<%s: " % (self.__class__.__name__) for key in self.__dict__: 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 + self.PKC_playlist_edits = [] + 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' @@ -48,6 +71,13 @@ class Playlist_Item(object): 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_item(kodi_item): @@ -127,8 +157,10 @@ def _get_playlist_details_from_xml(playlist, xml): try: playlist.ID = xml.attrib['%sID' % playlist.kind] playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.selectedItemID = xml.attrib['%sSelectedItemID' % playlist.kind] - playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' % playlist.kind] + playlist.selectedItemID = xml.attrib['%sSelectedItemID' + % playlist.kind] + playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' + % playlist.kind] playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] except: log.error('Could not parse xml answer from PMS for playlist %s' @@ -141,9 +173,9 @@ def _get_playlist_details_from_xml(playlist, xml): def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ - Supply either plex_id or the data supplied by Kodi JSON-RPC + Supply either with a plex_id OR the data supplied by Kodi JSON-RPC """ - if plex_id is not None: + if plex_id: item = playlist_item_from_plex(plex_id) else: item = playlist_item_from_kodi_item(kodi_item) @@ -155,7 +187,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", parameters=params) - _get_playlist_details_from_xml(xml) + _get_playlist_details_from_xml(playlist, xml) playlist.items.append(item) log.debug('Initialized the playlist: %s' % playlist) @@ -176,6 +208,10 @@ def add_playlist_item(playlist, kodi_item, after_pos): % (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 after_pos == len(playlist.items) - 1: # Item was added at the end @@ -259,6 +295,39 @@ def get_kodi_playqueues(): # 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). + + Will return a Playlist_Item + """ + item = Playlist_Item() + api = API(xml_video_element) + params = { + 'playlistid': playlist.playlistid + } + 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 embydb.GetEmbyDB() as emby_db: + db_element = emby_db.getItem_byId(item.plex_id) + try: + item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] + except TypeError: + pass + if item.kodi_id: + params['item'] = {'%sid' % item.kodi_type: item.kodi_id} + else: + item.file = api.getFilePath() + params['item'] = {'file': tryEncode(item.file)} + log.debug(JSONRPC('Playlist.Add').execute(params)) + playlist.PKC_playlist_edits.append( + item.kodi_id if item.kodi_id else item.file) + return item + + def insertintoPlaylist(self, position, dbid=None, @@ -275,57 +344,51 @@ def insertintoPlaylist(self, JSONRPC('Playlist.Insert').execute(params) -def addtoPlaylist(self, dbid=None, mediatype=None, url=None): - params = { - 'playlistid': self.playlistId - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - params['item'] = {'file': url} - JSONRPC('Playlist.Add').execute(params) - - def removefromPlaylist(self, position): params = { 'playlistid': self.playlistId, 'position': position } - JSONRPC('Playlist.Remove').execute(params) + log.debug(JSONRPC('Playlist.Remove').execute(params)) -def playAll(self, items, startitem, offset): +def get_PMS_playlist(playlist, playlist_id=None): """ - 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 - } + Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we + need to fetch a new playlist - 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) + Returns None if something went wrong """ - 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') + 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 update_playlist_from_PMS(playlist, playlist_id=None, repeat=None): + """ + Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we + need to fetch a new playqueue + """ + xml = get_PMS_playlist(playlist, playlist_id) + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not download Plex playlist.') 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) + # Clear our existing playlist and the associated Kodi playlist + playlist.clear() + # Set new values + _get_playlist_details_from_xml(playlist, xml) + if repeat: + playlist.repeat = repeat + for plex_item in xml: + playlist.items.append(add_to_Kodi_playlist(playlist, plex_item)) def _processItems(self, startitem, startPlayer=False): @@ -380,15 +443,3 @@ def _addtoPlaylist_xbmc(self, item): playbackutils.PlaybackUtils(item).setArtwork(listitem) self.playlist.add(playurl, listitem) - - -def clear(self): - """ - Empties current Kodi playlist and associated variables - """ - self.playlist.clear() - self.items = [] - self.queueId = None - self.playQueueVersion = None - self.guid = None - log.info('Playlist cleared') \ No newline at end of file diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 997c6bef..507fc6b9 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,10 +5,10 @@ from threading import Lock, Thread import xbmc -from utils import ThreadMethods, ThreadMethodsAdditionalSuspend, Lock_Function +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend, \ + Lock_Function import playlist_func as PL -from PlexFunctions import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, GetPlayQueue, \ - ParseContainerKey +from PlexFunctions import ConvertPlexToKodiTime ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -36,6 +36,7 @@ class Playqueue(Thread): if self.playqueues is not None: return self.mgr = callback + self.player = xbmc.Player() # Initialize Kodi playqueues self.playqueues = [] @@ -52,8 +53,64 @@ class Playqueue(Thread): # Currently, only video or audio playqueues available playqueue.kodi_pl = xbmc.PlayList(xbmc.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) + def get_playqueue_from_type(self, typus): + """ + Returns the playqueue according to the typus ('video', 'audio', + 'picture') passed in + """ + for playqueue in self.playqueues: + if playqueue.type == typus: + break + else: + raise ValueError('Wrong type was passed in: %s' % typus) + return playqueue + + def get_playqueue_from_playerid(self, kodi_player_id): + for playqueue in self.playqueues: + if playqueue.playlistid == kodi_player_id: + break + else: + raise ValueError('Wrong kodi_player_id passed was passed in: %s' + % kodi_player_id) + return playqueue + + @lockmethod.lockthis + def update_playqueue_from_PMS(self, + playqueue, + playqueue_id=None, + repeat=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 + """ + log.info('New playqueue received, updating!') + PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat) + log.debug('Updated playqueue: %s' % playqueue) + + window('plex_customplaylist', value="true") + if playqueue.selectedItemOffset not in (None, "0"): + window('plex_customplaylist.seektime', + str(ConvertPlexToKodiTime(playqueue.selectedItemOffset))) + for startpos, item in enumerate(playqueue.items): + if item.ID == playqueue.selectedItemID: + break + else: + startpos = None + # Start playback + if startpos: + self.player.play(playqueue.kodi_pl, startpos=startpos) + else: + self.player.play(playqueue.kodi_pl) + log.debug('Playqueue at the end: %s' % playqueue) + playqueue.log_Kodi_playlist() + @lockmethod.lockthis def update_playqueue_with_companion(self, data): """ @@ -61,10 +118,6 @@ class Playqueue(Thread): """ # Get the correct queue - for playqueue in self.playqueues: - if playqueue.type == KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[ - data['type']]: - break @lockmethod.lockthis def kodi_onadd(self, data): @@ -80,11 +133,20 @@ class Playqueue(Thread): for playqueue in self.playqueues: if playqueue.playlistid == data['playlistid']: break + if playqueue.PKC_playlist_edits: + old = (data['item'].get('id') if data['item'].get('id') + else data['item'].get('file')) + for i, item in enumerate(playqueue.PKC_playlist_edits): + if old == item: + log.debug('kodimonitor told us of a PKC edit - ignore.') + del playqueue.PKC_playlist_edits[i] + return if playqueue.ID is None: # Need to initialize the queue for the first time PL.init_Plex_playlist(playqueue, kodi_item=data['item']) else: PL.add_playlist_item(playqueue, data['item'], data['position']) + log.debug('Added a new item to the playqueue: %s' % playqueue) @lockmethod.lockthis def _compare_playqueues(self, playqueue, new): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index bf6941a9..b88ba0db 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 @@ -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()) From 14c9d10459699341640fc3aede8bf6dd9223a610 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 13:18:50 +0100 Subject: [PATCH 08/50] Hopefully fix Plex Companion RuntimeError - RuntimeError: dictionary changed size during iteration 12:42:00 T:8236 ERROR: Traceback (most recent call last): 12:42:00 T:8236 ERROR: File "C:\Users\Tom\AppData\Roaming\Kodi\addons\plugin.video.plexkodiconnect\resources\lib\plexbmchelper\listener.py", line 213, in answer_request 12:42:00 T:8236 ERROR: subMgr.notify() 12:42:00 T:8236 ERROR: File "C:\Users\Tom\AppData\Roaming\Kodi\addons\plugin.video.plexkodiconnect\resources\lib\plexbmchelper\subscribers.py", line 163, in notify 12:42:00 T:8236 ERROR: self.notifyServer(players) 12:42:00 T:8236 ERROR: File "C:\Users\Tom\AppData\Roaming\Kodi\addons\plugin.video.plexkodiconnect\resources\lib\plexbmchelper\subscribers.py", line 178, in notifyServer 12:42:00 T:8236 ERROR: for typus, p in self.lastplayers.iteritems(): --- resources/lib/plexbmchelper/subscribers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b88ba0db..54a8207e 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -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): From 4208bb9b7335c2fbc275946b0c3e09fa2a612135 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 14:48:23 +0100 Subject: [PATCH 09/50] playQueue fixes --- resources/lib/kodimonitor.py | 5 ++++ resources/lib/playlist_func.py | 13 +++++++--- resources/lib/playqueue.py | 46 ++++++++++++++++------------------ 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 09d797fd..1fcad0bc 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -173,6 +173,11 @@ class KodiMonitor(xbmc.Monitor): # u'position': 0} self.playqueue.kodi_onadd(data) + elif method == "Playlist.OnRemove": + # User (or PKC) deleted a playlist item + # Data: {u'position': 2, u'playlistid': 1} + self.playqueue.kodi_onremove(data) + def PlayBackStart(self, data): """ Called whenever a playback is started diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 2b243899..6a82e876 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -30,8 +30,12 @@ class Playlist_Object_Baseclase(object): 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__: - answ += '%s: %s, ' % (key, getattr(self, key)) + if key not in ("ID", 'items'): + answ += '%s: %s, ' % (key, getattr(self, key)) return answ[:-2] + ">" def clear(self): @@ -88,8 +92,8 @@ def playlist_item_from_kodi_item(kodi_item): kodi_item dict contains keys 'id', 'type', 'file' (if applicable) """ item = Playlist_Item() - if kodi_item.get('id'): - item.kodi_id = kodi_item['id'] + item.kodi_id = kodi_item.get('id') + if item.kodi_id: with embydb.GetEmbyDB() as emby_db: emby_dbitem = emby_db.getItem_byKodiId(kodi_item['id'], kodi_item['type']) @@ -227,6 +231,7 @@ def move_playlist_item(playlist, before_pos, after_pos): """ Moves playlist item from before_pos [int] to after_pos [int] """ + log.debug('Moving item from %s to %s' % (before_pos, after_pos)) if after_pos == 0: url = "{server}/%ss/%s/items/%s/move?after=0" % \ (playlist.kind, @@ -256,7 +261,7 @@ def delete_playlist_item(playlist, pos): playlist.repeat), action_type="DELETE") _get_playListVersion_from_xml(playlist, xml) - del playlist.items[pos] + del playlist.items[pos], playlist.old_kodi_pl[pos] def get_kodi_playlist_items(playlist): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 507fc6b9..e4f0f4f5 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -90,7 +90,7 @@ class Playqueue(Thread): repeat = 0, 1, 2 """ - log.info('New playqueue received, updating!') + log.info('New playqueue received from the PMS, updating!') PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat) log.debug('Updated playqueue: %s' % playqueue) @@ -108,17 +108,8 @@ class Playqueue(Thread): self.player.play(playqueue.kodi_pl, startpos=startpos) else: self.player.play(playqueue.kodi_pl) - log.debug('Playqueue at the end: %s' % playqueue) playqueue.log_Kodi_playlist() - @lockmethod.lockthis - def update_playqueue_with_companion(self, data): - """ - Feed with Plex companion data - """ - - # Get the correct queue - @lockmethod.lockthis def kodi_onadd(self, data): """ @@ -130,15 +121,13 @@ class Playqueue(Thread): u'position': 0 } """ - for playqueue in self.playqueues: - if playqueue.playlistid == data['playlistid']: - break + playqueue = self.playqueues[data['playlistid']] if playqueue.PKC_playlist_edits: old = (data['item'].get('id') if data['item'].get('id') else data['item'].get('file')) for i, item in enumerate(playqueue.PKC_playlist_edits): if old == item: - log.debug('kodimonitor told us of a PKC edit - ignore.') + log.debug('kodimonitor told us of a PKC edit - ignore') del playqueue.PKC_playlist_edits[i] return if playqueue.ID is None: @@ -148,15 +137,30 @@ class Playqueue(Thread): PL.add_playlist_item(playqueue, data['item'], data['position']) log.debug('Added a new item to the playqueue: %s' % playqueue) + @lockmethod.lockthis + def kodi_onremove(self, data): + """ + Called if an item is removed from a Kodi playqueue. Data is Kodi JSON- + RPC output, e.g. + {u'position': 2, u'playlistid': 1} + """ + playqueue = self.playqueues[data['playlistid']] + PL.delete_playlist_item(playqueue, data['position']) + log.debug('Deleted item at position %s. New playqueue: %s' + % (data['position'], playqueue)) + @lockmethod.lockthis def _compare_playqueues(self, playqueue, new): """ Used to poll the Kodi playqueue and update the Plex playqueue if needed """ + if self.threadStopped(): + # Chances are that we got an empty Kodi playlist due to Kodi exit + return old = playqueue.old_kodi_pl + index = list(range(0, len(old))) log.debug('Comparing new Kodi playqueue %s with our play queue %s' % (new, playqueue)) - index = list(range(0, len(old))) for i, new_item in enumerate(new): for j, old_item in enumerate(old): if old_item.get('id') is None: @@ -171,16 +175,10 @@ class Playqueue(Thread): # item now at pos i has been moved from original pos i+j PL.move_playlist_item(playqueue, i + j, i) # Delete the item we just found - del old[i + j], index[i + j] + del old[j], index[j] break - else: - # Did not find element i in the old list - Kodi monitor should - # pick this up! - # PL.add_playlist_item(playqueue, new_item, i-1) - pass - for i in index: - # Still got some old items left that need deleting - PL.delete_playlist_item(playqueue, i) + # New elements and left-over elements will be taken care of by the kodi + # monitor! log.debug('New playqueue: %s' % playqueue) def run(self): From e24f6b53fc91749cc93bf223755c12b8ae5f79f2 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 14:52:14 +0100 Subject: [PATCH 10/50] Fix logging --- resources/lib/playlist_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 6a82e876..4bdde005 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -32,7 +32,7 @@ class Playlist_Object_Baseclase(object): answ = "<%s: " % (self.__class__.__name__) # For some reason, can't use dir directly answ += "ID: %s, " % self.ID - answ += "items: %s" % self.items + answ += "items: %s, " % self.items for key in self.__dict__: if key not in ("ID", 'items'): answ += '%s: %s, ' % (key, getattr(self, key)) From 5b020910d3917957a26dbe7f21517a3b15754c99 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 14:57:10 +0100 Subject: [PATCH 11/50] Fix Plex Companion offset not working --- resources/lib/PlexCompanion.py | 2 +- resources/lib/playqueue.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 002a6f91..bdbb37b4 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -87,7 +87,7 @@ class PlexCompanion(Thread): data['type']) if ID != playqueue.ID: self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, ID, int(query['repeat'])) + playqueue, ID, int(query['repeat']), data['offset']) else: log.error('This has never happened before!') diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index e4f0f4f5..c969b5d5 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -83,21 +83,23 @@ class Playqueue(Thread): def update_playqueue_from_PMS(self, playqueue, playqueue_id=None, - repeat=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 """ log.info('New playqueue received from the PMS, updating!') PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat) log.debug('Updated playqueue: %s' % playqueue) window('plex_customplaylist', value="true") - if playqueue.selectedItemOffset not in (None, "0"): + if offset not in (None, "0"): window('plex_customplaylist.seektime', - str(ConvertPlexToKodiTime(playqueue.selectedItemOffset))) + str(ConvertPlexToKodiTime(offset))) for startpos, item in enumerate(playqueue.items): if item.ID == playqueue.selectedItemID: break From 49b5131dbb2d17217ab35b3a9a613b353072e76a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 15:21:29 +0100 Subject: [PATCH 12/50] Fix resume of playqueue initiated by companion --- resources/lib/PlexCompanion.py | 5 ++++- resources/lib/playlist_func.py | 18 +++++++++++++++--- resources/lib/playqueue.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index bdbb37b4..6a4b3b7d 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -86,10 +86,13 @@ class PlexCompanion(Thread): playqueue = self.mgr.playqueue.get_playqueue_from_type( data['type']) if ID != playqueue.ID: + # playqueue changed somehow self.mgr.playqueue.update_playqueue_from_PMS( playqueue, ID, int(query['repeat']), data['offset']) else: - log.error('This has never happened before!') + # No change to the playqueue + self.mgr.playqueue.start_playqueue_initiated_by_companion( + playqueue, int(query['repeat']), data['offset']) def run(self): httpd = False diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 4bdde005..e4a887a0 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -375,7 +375,21 @@ def get_PMS_playlist(playlist, playlist_id=None): return xml -def update_playlist_from_PMS(playlist, playlist_id=None, repeat=None): +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 update_playlist_from_PMS(playlist, playlist_id=None): """ Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we need to fetch a new playqueue @@ -390,8 +404,6 @@ def update_playlist_from_PMS(playlist, playlist_id=None, repeat=None): playlist.clear() # Set new values _get_playlist_details_from_xml(playlist, xml) - if repeat: - playlist.repeat = repeat for plex_item in xml: playlist.items.append(add_to_Kodi_playlist(playlist, plex_item)) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index c969b5d5..bc8eafbb 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -93,7 +93,8 @@ class Playqueue(Thread): offset = time offset in Plextime """ log.info('New playqueue received from the PMS, updating!') - PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat) + PL.update_playlist_from_PMS(playqueue, playqueue_id) + playqueue.repeat = repeat log.debug('Updated playqueue: %s' % playqueue) window('plex_customplaylist', value="true") @@ -112,6 +113,34 @@ class Playqueue(Thread): self.player.play(playqueue.kodi_pl) playqueue.log_Kodi_playlist() + @lockmethod.lockthis + def start_playqueue_initiated_by_companion(self, + playqueue, + playqueue_id=None, + repeat=None, + offset=None): + log.info('Plex companion wants to restart playback of playqueue %s' + % playqueue) + # Still need to get new playQueue from the server - don't know what has + # been selected + PL.refresh_playlist_from_PMS(playqueue) + playqueue.repeat = 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 = None + # Start playback + if startpos: + self.player.play(playqueue.kodi_pl, startpos=startpos) + else: + self.player.play(playqueue.kodi_pl) + playqueue.log_Kodi_playlist() + @lockmethod.lockthis def kodi_onadd(self, data): """ From eec2c10cb44ade75320989d54ebd18537c5a49bb Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 28 Dec 2016 19:38:43 +0100 Subject: [PATCH 13/50] Fixes to playQueues for Addon paths --- resources/lib/PlexCompanion.py | 9 +- resources/lib/playbackutils.py | 50 +++++++---- resources/lib/playlist_func.py | 154 +++++++++++++++++++-------------- resources/lib/playqueue.py | 28 ++++-- 4 files changed, 152 insertions(+), 89 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 6a4b3b7d..130cbb22 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -88,11 +88,16 @@ class PlexCompanion(Thread): if ID != playqueue.ID: # playqueue changed somehow self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, ID, int(query['repeat']), data['offset']) + playqueue, + ID, + query.get('repeat'), + data.get('offset')) else: # No change to the playqueue self.mgr.playqueue.start_playqueue_initiated_by_companion( - playqueue, int(query['repeat']), data['offset']) + playqueue, + query.get('repeat'), + data.get('offset')) def run(self): httpd = False diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 17342df7..af542ea6 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -17,6 +17,7 @@ import downloadutils import PlexAPI import PlexFunctions as PF +import playlist_func as PL ############################################################################### @@ -36,9 +37,12 @@ class PlaybackUtils(): self.userid = window('currUserId') self.server = window('pms_server') - - self.pl = Playqueue().get_playqueue_from_type( - PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.API.getType()]) + playqueue = Playqueue() + # We need to initialize already existing items as we have a completely + # different Python instance! + playqueue.init_playlists() + self.pl = playqueue.get_playqueue_from_type( + PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[item[0].attrib.get('type')]) def play(self, itemid, dbid=None): @@ -51,6 +55,7 @@ class PlaybackUtils(): playutils = putils.PlayUtils(item[0]) log.info("Play called.") + log.debug('Playqueue: %s' % self.pl) playurl = playutils.getPlayUrl() if not playurl: return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) @@ -108,6 +113,9 @@ class PlaybackUtils(): ############### 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. @@ -120,12 +128,20 @@ class PlaybackUtils(): window('plex_customplaylist') != "true" and not contextmenu_play): log.debug("Adding dummy file to playlist.") + # Make sure Kodimonitor recognizes dummy + listitem.setLabel('plex_dummyfile') dummyPlaylist = True - kodiPl.add(playurl, listitem, index=startPos) + PL.add_listitem_to_Kodi_playlist( + self.pl, + listitem, + playurl, + startPos) # 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( + PL.remove_from_Kodi_playlist(self.pl, startPos+1) + # Readd the original item to playlist - via jsonrpc so we have + # full metadata + PL.insert_into_Kodi_playlist( + self.pl, self.currentPosition+1, dbid, PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) @@ -146,9 +162,10 @@ class PlaybackUtils(): # 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()]) + PL.add_dbid_to_Kodi_playlist( + self.pl, + dbid=dbid, + mediatype=PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) elif contextmenu_play: if window('useDirectPaths') == 'true': @@ -168,10 +185,11 @@ class PlaybackUtils(): kodiPl.add(playurl, listitem, index=self.currentPosition+1) else: # Full metadata - self.pl.insertintoPlaylist( + PL.insert_into_Kodi_playlist( + self.pl, self.currentPosition+1, - dbid, - PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + dbid=dbid, + mediatype=PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) self.currentPosition += 1 if seektime: window('plex_customplaylist.seektime', value=str(seektime)) @@ -201,7 +219,6 @@ class PlaybackUtils(): kodiPl.add(additionalPlayurl, additionalListItem, index=self.currentPosition) - self.pl.verifyPlaylist() self.currentPosition += 1 API.setPartNumber(0) @@ -287,7 +304,10 @@ class PlaybackUtils(): introPlayurl = path + '?' + urlencode(params) log.info("Adding Intro: %s" % introPlayurl) - self.pl.insertintoPlaylist(self.currentPosition, url=introPlayurl) + PL.insert_into_Kodi_playlist( + self.pl, + self.currentPosition, + url=introPlayurl) self.currentPosition += 1 return True diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index e4a887a0..d9b756d3 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -3,7 +3,7 @@ from urllib import quote import embydb_functions as embydb from downloadutils import DownloadUtils as DU -from utils import JSONRPC, tryEncode +from utils import window, JSONRPC, tryEncode from PlexAPI import API ############################################################################### @@ -250,6 +250,57 @@ def move_playlist_item(playlist, before_pos, after_pos): playlist.items.insert(after_pos, playlist.items.pop(before_pos)) +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 update_playlist_from_PMS(playlist, playlist_id=None): + """ + Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we + need to fetch a new playqueue + """ + xml = get_PMS_playlist(playlist, playlist_id) + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not download 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 delete_playlist_item(playlist, pos): """ Delete the item at position pos [int] @@ -333,13 +384,43 @@ def add_to_Kodi_playlist(playlist, xml_video_element): return item -def insertintoPlaylist(self, - position, - dbid=None, - mediatype=None, - url=None): +def add_listitem_to_Kodi_playlist(playlist, listitem, file, index): + """ + Adds an xbmc listitem to the Kodi playlist. Will be ignored by kodimonitor + by settings window('plex_ignore_Playlist.OnAdd') + """ + playlist.kodi_pl.add(file, listitem, index=index) + + +def add_dbid_to_Kodi_playlist(playlist, dbid=None, mediatype=None, url=None): params = { - 'playlistid': self.playlistId, + 'playlistid': playlist.playlistid + } + if dbid is not None: + params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} + else: + params['item'] = {'file': url} + log.debug(JSONRPC('Playlist.Add').execute(params)) + + +def remove_from_Kodi_playlist(playlist, position): + """ + Removes the item at position from the Kodi playlist using JSON. Will be + ignored by kodimonitor by settings window('plex_ignore_Playlist.OnRemove') + """ + log.debug('Removing position %s from playlist %s' % (position, playlist)) + log.debug(JSONRPC('Playlist.Remove').execute({ + 'playlistid': playlist.playlistid, + 'position': position + })) + + +def insert_into_Kodi_playlist(playlist, position, dbid=None, mediatype=None, + url=None): + """ + """ + params = { + 'playlistid': playlist.playlistid, 'position': position } if dbid is not None: @@ -349,64 +430,7 @@ def insertintoPlaylist(self, JSONRPC('Playlist.Insert').execute(params) -def removefromPlaylist(self, position): - params = { - 'playlistid': self.playlistId, - 'position': position - } - log.debug(JSONRPC('Playlist.Remove').execute(params)) - - -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 update_playlist_from_PMS(playlist, playlist_id=None): - """ - Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we - need to fetch a new playqueue - """ - xml = get_PMS_playlist(playlist, playlist_id) - try: - xml.attrib['%sVersion' % playlist.kind] - except: - log.error('Could not download 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)) - +# NOT YET UPDATED!! def _processItems(self, startitem, startPlayer=False): startpos = None diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index bc8eafbb..f88c1fcb 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -94,7 +94,7 @@ class Playqueue(Thread): """ log.info('New playqueue received from the PMS, updating!') PL.update_playlist_from_PMS(playqueue, playqueue_id) - playqueue.repeat = repeat + playqueue.repeat = 0 if not repeat else int(repeat) log.debug('Updated playqueue: %s' % playqueue) window('plex_customplaylist', value="true") @@ -124,7 +124,7 @@ class Playqueue(Thread): # Still need to get new playQueue from the server - don't know what has # been selected PL.refresh_playlist_from_PMS(playqueue) - playqueue.repeat = repeat + playqueue.repeat = 0 if not repeat else int(repeat) window('plex_customplaylist', value="true") if offset not in (None, "0"): window('plex_customplaylist.seektime', @@ -153,6 +153,13 @@ class Playqueue(Thread): } """ playqueue = self.playqueues[data['playlistid']] + if data['item'].get('id') is None and data['item'].get('file') is None: + # Kodi screwed up. Let's try to get the data anyway + items = PL.get_kodi_playlist_items(playqueue) + if items[data['position']].get('id') is not None: + data['item']['id'] = items[data['position']].get('id') + else: + data['item']['file'] = items[data['position']].get('file') if playqueue.PKC_playlist_edits: old = (data['item'].get('id') if data['item'].get('id') else data['item'].get('file')) @@ -212,17 +219,24 @@ class Playqueue(Thread): # monitor! log.debug('New playqueue: %s' % playqueue) - def run(self): - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended - log.info("----===## Starting PlayQueue client ##===----") - # Initialize the playqueues, if Kodi already got items in them + def init_playlists(self): + """ + Initializes the playqueues with already existing items. + Called on startup AND for addon paths! + """ 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_playlist_item(playqueue, item, i) + + def run(self): + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended + log.info("----===## Starting PlayQueue client ##===----") + # Initialize the playqueues, if Kodi already got items in them + self.init_playlists() while not threadStopped(): while threadSuspended(): if threadStopped(): From ee86f58a3f452b61dab24510442dd48cd7476f26 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 29 Dec 2016 11:22:02 +0100 Subject: [PATCH 14/50] Move get Kodi_id from filename to function --- resources/lib/kodidb_functions.py | 23 +++++++++++++++++++++++ resources/lib/kodimonitor.py | 19 ++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f2046fd8..683962e2 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1397,3 +1397,26 @@ class Kodidb_Functions(): query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" self.cursor.execute(query, (genreid, kodiid)) + + +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 1fcad0bc..d165f926 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,10 +10,10 @@ import xbmcgui import downloadutils import embydb_functions as embydb -import kodidb_functions as kodidb import playbackutils as pbutils from utils import window, settings, CatchExceptions, tryDecode, tryEncode from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE +from kodidb_functions import get_kodiid_from_filename ############################################################################### @@ -227,20 +227,9 @@ 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 From 253feb9c69c11efbfe9deeadc5af0901292cf881 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 29 Dec 2016 15:41:14 +0100 Subject: [PATCH 15/50] Always post to PMS to get playQueue --- resources/lib/PlexFunctions.py | 9 ++++----- resources/lib/playbackutils.py | 35 +++++++++++++++++----------------- resources/lib/playqueue.py | 26 +++++++++++++++++-------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 621aae90..bd442e1d 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -384,23 +384,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/playbackutils.py b/resources/lib/playbackutils.py index af542ea6..b35b9001 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -37,11 +37,7 @@ class PlaybackUtils(): self.userid = window('currUserId') self.server = window('pms_server') - playqueue = Playqueue() - # We need to initialize already existing items as we have a completely - # different Python instance! - playqueue.init_playlists() - self.pl = playqueue.get_playqueue_from_type( + self.pl = Playqueue().get_playqueue_from_type( PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[item[0].attrib.get('type')]) def play(self, itemid, dbid=None): @@ -124,6 +120,22 @@ class PlaybackUtils(): window('plex_playbackProps', value="true") log.info("Setting up properties in playlist.") + # Post playQueue to PMS + trailers = False + if settings('enableCinema') == "true": + if settings('askCinema') == "true": + trailers = xbmcgui.Dialog().yesno(addonName, + "Play trailers?") + else: + trailers = True + xml = PF.GetPlexPlaylist( + itemid, + item.attrib.get('librarySectionUUID'), + mediatype=API.getType(), + trailers=trailers) + # Save playQueueID for other PKC python instance & kodimonitor + window('plex_playQueueID', value=xml.attrib.get('playQueueID')) + if (not homeScreen and not seektime and window('plex_customplaylist') != "true" and not contextmenu_play): @@ -148,12 +160,8 @@ class PlaybackUtils(): self.currentPosition += 1 ############### -- CHECK FOR INTROS ################ - if (settings('enableCinema') == "true" and not seektime): + if trailers 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 ############## @@ -278,13 +286,6 @@ class PlaybackUtils(): 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 - # Playurl needs to point back so we can get metadata! path = "plugin://plugin.video.plexkodiconnect.movies/" params = { diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index f88c1fcb..7f589cc8 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -79,6 +79,13 @@ class Playqueue(Thread): % kodi_player_id) return playqueue + def _grab_PMS_playqueue(self, playqueue, playqueue_id=None, repeat=None): + """ + For initiating out playqueues from the PMS because another PKC Python + instance already is setting up the Kodi playlists + """ + PL.grab_PMS_playqueue(playqueue, playqueue_id) + @lockmethod.lockthis def update_playqueue_from_PMS(self, playqueue, @@ -96,7 +103,6 @@ class Playqueue(Thread): PL.update_playlist_from_PMS(playqueue, playqueue_id) playqueue.repeat = 0 if not repeat else int(repeat) log.debug('Updated playqueue: %s' % playqueue) - window('plex_customplaylist', value="true") if offset not in (None, "0"): window('plex_customplaylist.seektime', @@ -153,13 +159,14 @@ class Playqueue(Thread): } """ playqueue = self.playqueues[data['playlistid']] - if data['item'].get('id') is None and data['item'].get('file') is None: - # Kodi screwed up. Let's try to get the data anyway - items = PL.get_kodi_playlist_items(playqueue) - if items[data['position']].get('id') is not None: - data['item']['id'] = items[data['position']].get('id') - else: - data['item']['file'] = items[data['position']].get('file') + if window('plex_playbackProps') == 'true': + log.debug('kodi_onadd called during PKC playback setup') + if window('plex_playQueueID'): + self._grab_PMS_playqueue(playqueue, window('plex_playQueueID')) + window('plex_playQueueID', clear=True) + log.debug('Done setting up playQueue') + return + if playqueue.PKC_playlist_edits: old = (data['item'].get('id') if data['item'].get('id') else data['item'].get('file')) @@ -182,6 +189,9 @@ class Playqueue(Thread): RPC output, e.g. {u'position': 2, u'playlistid': 1} """ + if window('plex_playbackProps') == 'true': + log.debug('kodi_onremove called during PKC playback setup') + return playqueue = self.playqueues[data['playlistid']] PL.delete_playlist_item(playqueue, data['position']) log.debug('Deleted item at position %s. New playqueue: %s' From a9f59868f0c603c87b0b431657f3c8c5fd76ecbe Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 29 Dec 2016 15:42:19 +0100 Subject: [PATCH 16/50] Fix xml logging --- resources/lib/playlist_func.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index d9b756d3..9e26f631 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -135,7 +135,8 @@ def _log_xml(xml): 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: %s' % dump(xml)) + log.error('XML received from the PMS:') + dump(xml) def _get_playListVersion_from_xml(playlist, xml): From 146f063fc930b9a9df1ffb51149566b04619d7cf Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 14:07:24 +0100 Subject: [PATCH 17/50] Playqueues overhaul continued --- addon.xml | 2 - default.py | 95 ++++---- resources/lib/PKC_listitem.py | 334 ++++++++++++++++++++++++++ resources/lib/PlexAPI.py | 8 +- resources/lib/PlexCompanion.py | 18 +- resources/lib/context_entry.py | 2 +- resources/lib/dialogs/usersconnect.py | 2 +- resources/lib/entrypoint.py | 76 +----- resources/lib/itemtypes.py | 10 +- resources/lib/kodidb_functions.py | 2 +- resources/lib/kodimonitor.py | 14 -- resources/lib/monitor_kodi_play.py | 41 ++++ resources/lib/pickler.py | 44 ++++ resources/lib/playback_starter.py | 92 +++++++ resources/lib/playbackutils.py | 274 +++++++++++---------- resources/lib/playlist_func.py | 280 +++++++++++++-------- resources/lib/playqueue.py | 286 ++++++++-------------- resources/lib/utils.py | 18 ++ service.py | 14 +- 19 files changed, 1049 insertions(+), 563 deletions(-) create mode 100644 resources/lib/PKC_listitem.py create mode 100644 resources/lib/monitor_kodi_play.py create mode 100644 resources/lib/pickler.py create mode 100644 resources/lib/playback_starter.py diff --git a/addon.xml b/addon.xml index 85d4d694..192c6930 100644 --- a/addon.xml +++ b/addon.xml @@ -6,8 +6,6 @@ - - diff --git a/default.py b/default.py index 8dc00b25..235734e3 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, @@ -92,8 +106,8 @@ class Main(): 'playwatchlater': entrypoint.playWatchLater } - if "/extrafanart" in sys.argv[0]: - plexpath = sys.argv[2][1:] + if "/extrafanart" in ARGV[0]: + plexpath = ARGV[2][1:] plexid = params.get('id', [""])[0] entrypoint.getExtraFanArt(plexid, plexpath) entrypoint.getVideoFiles(plexid, plexpath) @@ -101,11 +115,11 @@ class Main(): 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]): + if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or + "/Extras" in ARGV[2]): plexId = params.get('id', [None])[0] entrypoint.getVideoFiles(plexId, params) @@ -143,7 +157,7 @@ class Main(): folderid = params['folderid'][0] modes[mode](itemid, folderid) elif mode == "companion": - modes[mode](itemid, params=sys.argv[2]) + modes[mode](itemid, params=ARGV[2]) elif mode == 'playwatchlater': modes[mode](params.get('id')[0], params.get('viewOffset')[0]) else: @@ -151,25 +165,24 @@ class Main(): 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 06aa9484..fa8ca2d1 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2393,7 +2393,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,14 +2456,14 @@ 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) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 130cbb22..8af5687b 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -85,19 +85,11 @@ class PlexCompanion(Thread): return playqueue = self.mgr.playqueue.get_playqueue_from_type( data['type']) - if ID != playqueue.ID: - # playqueue changed somehow - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - ID, - query.get('repeat'), - data.get('offset')) - else: - # No change to the playqueue - self.mgr.playqueue.start_playqueue_initiated_by_companion( - playqueue, - query.get('repeat'), - data.get('offset')) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + ID, + repeat=query.get('repeat'), + offset=data.get('offset')) def run(self): httpd = False diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index d5996b9e..f9772bfc 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -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/entrypoint.py b/resources/lib/entrypoint.py index de68e73f..27f305fc 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -32,40 +32,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 @@ -180,40 +146,6 @@ def playWatchLater(itemid, 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) - else: - # Video - return pbutils.PlaybackUtils(xml).play(itemid, dbid) - - ##### DO RESET AUTH ##### def resetAuth(): # User tried login and failed too many times @@ -497,7 +429,7 @@ def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils 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') + li.setArt({'icon': 'DefaultPicture.png'}) else: #normal video items li.setProperty('IsPlayable', 'true') @@ -541,7 +473,7 @@ def createListItemFromEmbyItem(item,art=None,doUtils=downloadutils.DownloadUtils if allart.get('Primary'): li.setThumbnailImage(allart.get('Primary')) else: li.setThumbnailImage('DefaultTVShows.png') - li.setIconImage('DefaultTVShows.png') + li.setArt({'icon': '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: @@ -663,7 +595,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(): @@ -1167,7 +1099,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]), diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 0ddc8351..04b83d0f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -407,7 +407,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, @@ -675,7 +675,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 @@ -956,7 +956,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 +966,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 @@ -1093,7 +1093,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(( diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 683962e2..fd682a07 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -61,7 +61,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): """ diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d165f926..64059295 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -164,20 +164,6 @@ class KodiMonitor(xbmc.Monitor): xbmc.sleep(5000) window('plex_runLibScan', value="full") - elif method == "Playlist.OnClear": - pass - - elif method == "Playlist.OnAdd": - # User (or PKC) manipulated Kodi playlist - # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, - # u'position': 0} - self.playqueue.kodi_onadd(data) - - elif method == "Playlist.OnRemove": - # User (or PKC) deleted a playlist item - # Data: {u'position': 2, u'playlistid': 1} - self.playqueue.kodi_onremove(data) - def PlayBackStart(self, data): """ Called whenever a playback is started 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..ddc10afe --- /dev/null +++ b/resources/lib/playback_starter.py @@ -0,0 +1,92 @@ +# -*- 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 +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) + if xml[0].attrib.get('type') == 'photo': + # Photo + result = Playback_Successful() + listitem = PKC_ListItem() + api = API(xml[0]) + listitem = api.CreateListItemFromPlexItem(listitem) + api.AddStreamInfo(listitem) + listitem = PlaybackUtils(xml[0], self.mgr).setArtwork(listitem) + result.listitem = listitem + else: + # Video and Music + with lock: + result = PlaybackUtils(xml[0], self.mgr).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 b35b9001..b146624d 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -3,21 +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 -from playqueue import Playqueue from utils import window, settings, tryEncode, tryDecode import downloadutils -import PlexAPI -import PlexFunctions as PF -import playlist_func as PL +from PlexAPI import API +from PlexFunctions import GetPlexPlaylist, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, \ + KODITYPE_FROM_PLEXTYPE +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 playqueue import lock +from pickler import Playback_Successful ############################################################################### @@ -30,47 +34,47 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item): - + def __init__(self, item, callback): + self.mgr = callback self.item = item - self.API = PlexAPI.API(item) - - self.userid = window('currUserId') - self.server = window('pms_server') - self.pl = Playqueue().get_playqueue_from_type( - PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[item[0].attrib.get('type')]) - - def play(self, itemid, dbid=None): + self.api = API(item) + self.playqueue = self.mgr.playqueue.get_playqueue_from_type( + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]) + 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.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.") - log.debug('Playqueue: %s' % self.pl) + api = self.api + 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) + api.CreateListItemFromPlexItem(listitem) self.setArtwork(listitem) - if dbid == 'plexnode': + if kodi_id == 'plexnode': # Need to get yet another xml to get final url window('emby_%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') @@ -82,20 +86,23 @@ class PlaybackUtils(): window('emby_%s.playmethod' % playurl, "Transcode") listitem.setPath(playurl) self.setProperties(playurl, listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, 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.kodi_pl - 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" @@ -107,8 +114,8 @@ 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')) @@ -116,64 +123,69 @@ class PlaybackUtils(): # 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.") - - # Post playQueue to PMS + # Where will the player need to start? + # Do we need to get trailers? trailers = False - if settings('enableCinema') == "true": + if (api.getType() == 'movie' and not seektime and + settings('enableCinema') == "true"): if settings('askCinema') == "true": - trailers = xbmcgui.Dialog().yesno(addonName, - "Play trailers?") + trailers = xbmcgui.Dialog().yesno( + addonName, + "Play trailers?") else: trailers = True - xml = PF.GetPlexPlaylist( - itemid, - item.attrib.get('librarySectionUUID'), - mediatype=API.getType(), + # Post to the PMS. REUSE THE PLAYQUEUE! + xml = GetPlexPlaylist( + plex_id, + plex_lib_UUID, + mediatype=api.getType(), trailers=trailers) - # Save playQueueID for other PKC python instance & kodimonitor - window('plex_playQueueID', value=xml.attrib.get('playQueueID')) + log.debug('xml: ID: %s' % xml.attrib['playQueueID']) + get_playlist_details_from_xml(playqueue, xml=xml) + log.debug('finished ') if (not homeScreen and not seektime 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.") - # Make sure Kodimonitor recognizes dummy - listitem.setLabel('plex_dummyfile') dummyPlaylist = True - PL.add_listitem_to_Kodi_playlist( - self.pl, - listitem, + add_listitem_to_Kodi_playlist( + playqueue, + startPos, + xbmcgui.ListItem(), playurl, - startPos) + xml[0]) # Remove the original item from playlist - PL.remove_from_Kodi_playlist(self.pl, startPos+1) + remove_from_Kodi_playlist( + playqueue, + startPos+1) # Readd the original item to playlist - via jsonrpc so we have # full metadata - PL.insert_into_Kodi_playlist( - self.pl, + 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 trailers and not seektime: - # if we have any play them when the movie/show is not being resumed + # -- ADD TRAILERS ################ + if trailers: introsPlaylist = self.AddTrailers(xml) - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## - + # -- 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.") - PL.add_dbid_to_Kodi_playlist( - self.pl, - dbid=dbid, - mediatype=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': @@ -187,17 +199,16 @@ class PlaybackUtils(): listitem, tryDecode(playurl))) window('emby_%s.playmethod' % playurl, value="Transcode") + api.CreateListItemFromPlexItem(listitem) self.setProperties(playurl, listitem) self.setArtwork(listitem) - API.CreateListItemFromPlexItem(listitem) kodiPl.add(playurl, listitem, index=self.currentPosition+1) else: # Full metadata - PL.insert_into_Kodi_playlist( - self.pl, + self.pl.insertintoPlaylist( self.currentPosition+1, - dbid=dbid, - mediatype=PF.KODITYPE_FROM_PLEXTYPE[API.getType()]) + kodi_id, + kodi_type) self.currentPosition += 1 if seektime: window('plex_customplaylist.seektime', value=str(seektime)) @@ -205,44 +216,50 @@ 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: + # -- CHECK FOR ADDITIONAL PARTS ################ + if len(item[0]) > 1: # Only add to the playlist after intros have played - for counter, part in enumerate(item[0][0]): + 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) + api.setPartNumber(counter) additionalListItem = xbmcgui.ListItem() additionalPlayurl = playutils.getPlayUrl( partNumber=counter) log.debug("Adding additional part: %s, url: %s" % (counter, additionalPlayurl)) - + api.CreateListItemFromPlexItem(additionalListItem) self.setProperties(additionalPlayurl, additionalListItem) self.setArtwork(additionalListItem) - # NEW to Plex - API.CreateListItemFromPlexItem(additionalListItem) - - kodiPl.add(additionalPlayurl, additionalListItem, - index=self.currentPosition) + add_listitem_to_playlist( + playqueue, + self.currentPosition, + additionalListItem, + kodi_id=kodi_id, + kodi_type=kodi_type, + plex_id=plex_id, + file=additionalPlayurl) self.currentPosition += 1 - API.setPartNumber(0) + api.setPartNumber(0) 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! + with lock: + 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 not contextmenu_play): @@ -254,40 +271,42 @@ class PlaybackUtils(): listitem.setPath(playurl) self.setProperties(playurl, 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") + # 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): """ Adds trailers to a movie, if applicable. Returns True if trailers were added """ - # 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 - # 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' @@ -296,28 +315,29 @@ class PlaybackUtils(): # 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) + introAPI = API(intro) + listitem = introAPI.CreateListItemFromPlexItem() params['id'] = introAPI.getRatingKey() params['filename'] = introAPI.getKey() introPlayurl = path + '?' + urlencode(params) + self.setArtwork(listitem, introAPI) + # Overwrite the Plex url + listitem.setPath(introPlayurl) log.info("Adding Intro: %s" % introPlayurl) - - PL.insert_into_Kodi_playlist( - self.pl, + add_listitem_to_Kodi_playlist( + self.playqueue, self.currentPosition, - url=introPlayurl) + listitem, + introPlayurl, + intro) 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() + itemid = self.api.getRatingKey() + itemtype = self.api.getType() + userdata = self.api.getUserData() embyitem = "emby_%s" % playurl window('%s.runtime' % embyitem, value=str(userdata['Runtime'])) @@ -327,20 +347,22 @@ class PlaybackUtils(): if itemtype == "episode": window('%s.refreshid' % embyitem, - value=self.API.getParentRatingKey()) + 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) + subtitles = self.api.externalSubs(playurl) listitem.setSubtitles(subtitles) self.setArtwork(listitem) - def setArtwork(self, listItem): - allartwork = self.API.getAllArtwork(parentInfo=True) + def setArtwork(self, listItem, api=None): + if api is None: + api = self.api + allartwork = api.getAllArtwork(parentInfo=True) arttypes = { 'poster': "Primary", 'tvshow.poster': "Thumb", diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 9e26f631..5bc64dbb 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -3,7 +3,7 @@ from urllib import quote import embydb_functions as embydb from downloadutils import DownloadUtils as DU -from utils import window, JSONRPC, tryEncode +from utils import JSONRPC, tryEncode, tryDecode from PlexAPI import API ############################################################################### @@ -12,6 +12,9 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### +# kodi_item: +# {u'type': u'movie', u'id': 3, 'file': path-to-file} + class Playlist_Object_Baseclase(object): playlistid = None # Kodi playlist ID, [int] @@ -25,8 +28,6 @@ class Playlist_Object_Baseclase(object): selectedItemOffset = None shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? - # Hack to later ignore all Kodi playlist adds that PKC did (Kodimonitor) - PKC_playlist_edits = [] def __repr__(self): answ = "<%s: " % (self.__class__.__name__) @@ -52,7 +53,6 @@ class Playlist_Object_Baseclase(object): self.selectedItemOffset = None self.shuffled = 0 self.repeat = 0 - self.PKC_playlist_edits = [] log.debug('Playlist cleared: %s' % self) def log_Kodi_playlist(self): @@ -84,7 +84,7 @@ class Playlist_Item(object): return answ[:-2] + ">" -def playlist_item_from_kodi_item(kodi_item): +def playlist_item_from_kodi(kodi_item): """ Turns the JSON answer from Kodi into a playlist element @@ -99,14 +99,15 @@ def playlist_item_from_kodi_item(kodi_item): kodi_item['type']) try: item.plex_id = emby_dbitem[0] - item.plex_UUID = emby_dbitem[0] + item.plex_UUID = emby_dbitem[0] # we dont need the uuid yet :-) except TypeError: pass - item.file = kodi_item.get('file') if kodi_item.get('file') else None - item.kodi_type = kodi_item.get('type') if kodi_item.get('type') else None + 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 @@ -115,6 +116,8 @@ def playlist_item_from_kodi_item(kodi_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 @@ -128,6 +131,26 @@ def playlist_item_from_plex(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 embydb.GetEmbyDB() as emby_db: + db_element = emby_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 @@ -154,7 +177,7 @@ def _get_playListVersion_from_xml(playlist, xml): return True -def _get_playlist_details_from_xml(playlist, xml): +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 @@ -174,16 +197,42 @@ def _get_playlist_details_from_xml(playlist, xml): 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 download 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): """ - Supply either with a plex_id OR the data supplied by Kodi JSON-RPC + 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_item(kodi_item) + item = playlist_item_from_kodi(kodi_item) params = { 'next': 0, 'type': playlist.type, @@ -192,22 +241,74 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", parameters=params) - _get_playlist_details_from_xml(playlist, xml) + get_playlist_details_from_xml(playlist, xml) playlist.items.append(item) - log.debug('Initialized the playlist: %s' % playlist) + log.debug('Initialized the playlist on the Plex side: %s' % playlist) -def add_playlist_item(playlist, kodi_item, after_pos): +def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, + kodi_type=None, plex_id=None, file=None): """ - Adds the new kodi_item to playlist after item at position after_pos - [int] + Adds a listitem to both the Kodi and Plex playlist at position pos [int]. + + If file is not None, file will overrule kodi_id! """ - item = playlist_item_from_kodi_item(kodi_item) + 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 playlist + # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: - item.ID = xml.attrib['%sLastAddedItemID' % playlist.kind] + item.ID = xml[-1].attrib['%sItemID' % playlist.kind] except (TypeError, AttributeError, KeyError): log.error('Could not add item %s to playlist %s' % (kodi_item, playlist)) @@ -218,21 +319,48 @@ def add_playlist_item(playlist, kodi_item, after_pos): if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: item.guid = plex_item.attrib['guid'] playlist.items.append(item) - if after_pos == len(playlist.items) - 1: + 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, - after_pos) + 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] + 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' % (before_pos, after_pos)) + 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, @@ -249,6 +377,7 @@ def move_playlist_item(playlist, before_pos, after_pos): _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): @@ -280,32 +409,14 @@ def refresh_playlist_from_PMS(playlist): except: log.error('Could not download Plex playlist.') return - _get_playlist_details_from_xml(playlist, xml) + get_playlist_details_from_xml(playlist, xml) -def update_playlist_from_PMS(playlist, playlist_id=None): +def delete_playlist_item_from_PMS(playlist, pos): """ - Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we - need to fetch a new playqueue - """ - xml = get_PMS_playlist(playlist, playlist_id) - try: - xml.attrib['%sVersion' % playlist.kind] - except: - log.error('Could not download 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 delete_playlist_item(playlist, pos): - """ - Delete the item at position pos [int] + 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, @@ -313,7 +424,7 @@ def delete_playlist_item(playlist, pos): playlist.repeat), action_type="DELETE") _get_playListVersion_from_xml(playlist, xml) - del playlist.items[pos], playlist.old_kodi_pl[pos] + del playlist.items[pos] def get_kodi_playlist_items(playlist): @@ -357,78 +468,53 @@ 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). - Will return a Playlist_Item + Returns a Playlist_Item """ - item = Playlist_Item() - api = API(xml_video_element) + item = playlist_item_from_xml(playlist, xml_video_element) params = { 'playlistid': playlist.playlistid } - 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 embydb.GetEmbyDB() as emby_db: - db_element = emby_db.getItem_byId(item.plex_id) - try: - item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] - except TypeError: - pass if item.kodi_id: params['item'] = {'%sid' % item.kodi_type: item.kodi_id} else: - item.file = api.getFilePath() params['item'] = {'file': tryEncode(item.file)} log.debug(JSONRPC('Playlist.Add').execute(params)) - playlist.PKC_playlist_edits.append( - item.kodi_id if item.kodi_id else item.file) return item -def add_listitem_to_Kodi_playlist(playlist, listitem, file, index): +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. Will be ignored by kodimonitor - by settings window('plex_ignore_Playlist.OnAdd') + Adds an xbmc listitem to the Kodi playlist.xml_video_element + + WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS """ - playlist.kodi_pl.add(file, listitem, index=index) - - -def add_dbid_to_Kodi_playlist(playlist, dbid=None, mediatype=None, url=None): - params = { - 'playlistid': playlist.playlistid - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} + 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: - params['item'] = {'file': url} - log.debug(JSONRPC('Playlist.Add').execute(params)) + 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, position): +def remove_from_Kodi_playlist(playlist, pos): """ - Removes the item at position from the Kodi playlist using JSON. Will be - ignored by kodimonitor by settings window('plex_ignore_Playlist.OnRemove') + 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 playlist %s' % (position, playlist)) + log.debug('Removing position %s from Kodi only from %s' % (pos, playlist)) log.debug(JSONRPC('Playlist.Remove').execute({ 'playlistid': playlist.playlistid, - 'position': position + 'position': pos })) - - -def insert_into_Kodi_playlist(playlist, position, dbid=None, mediatype=None, - url=None): - """ - """ - params = { - 'playlistid': playlist.playlistid, - 'position': position - } - if dbid is not None: - params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)} - else: - params['item'] = {'file': url} - JSONRPC('Playlist.Insert').execute(params) + del playlist.items[pos] # NOT YET UPDATED!! @@ -478,7 +564,7 @@ def _addtoPlaylist_xbmc(self, item): 'id': API.getRatingKey(), 'filename': API.getKey() } - playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \ + playurl = "plugin://plugin.video.plexkodiconnect/movies/?%s" \ % urlencode(params) listitem = API.CreateListItemFromPlexItem() diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 7f589cc8..1d25b886 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- ############################################################################### import logging -from threading import Lock, Thread +from threading import RLock, Thread -import xbmc +from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend, \ - Lock_Function +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime ############################################################################### log = logging.getLogger("PLEX."+__name__) -# Lock used to lock methods -lock = Lock() -lockmethod = Lock_Function(lock) +# Lock used for playqueue manipulations +lock = RLock() ############################################################################### @@ -29,33 +27,32 @@ class Playqueue(Thread): __shared_state = {} playqueues = None - @lockmethod.lockthis def __init__(self, callback=None): self.__dict__ = self.__shared_state Thread.__init__(self) if self.playqueues is not None: return self.mgr = callback - self.player = xbmc.Player() # Initialize Kodi playqueues - 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 = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) - elif playqueue.type == 'video': - playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - else: - # Currently, only video or audio playqueues available - playqueue.kodi_pl = xbmc.PlayList(xbmc.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) + 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) def get_playqueue_from_type(self, typus): @@ -63,30 +60,14 @@ class Playqueue(Thread): Returns the playqueue according to the typus ('video', 'audio', 'picture') passed in """ - for playqueue in self.playqueues: - if playqueue.type == typus: - break - else: - raise ValueError('Wrong type was passed in: %s' % typus) - return playqueue + 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 get_playqueue_from_playerid(self, kodi_player_id): - for playqueue in self.playqueues: - if playqueue.playlistid == kodi_player_id: - break - else: - raise ValueError('Wrong kodi_player_id passed was passed in: %s' - % kodi_player_id) - return playqueue - - def _grab_PMS_playqueue(self, playqueue, playqueue_id=None, repeat=None): - """ - For initiating out playqueues from the PMS because another PKC Python - instance already is setting up the Kodi playlists - """ - PL.grab_PMS_playqueue(playqueue, playqueue_id) - - @lockmethod.lockthis def update_playqueue_from_PMS(self, playqueue, playqueue_id=None, @@ -97,169 +78,104 @@ class Playqueue(Thread): in playqueue_id if we need to fetch a new playqueue repeat = 0, 1, 2 - offset = time offset in Plextime + offset = time offset in Plextime (milliseconds) """ - log.info('New playqueue received from the PMS, updating!') - PL.update_playlist_from_PMS(playqueue, playqueue_id) - playqueue.repeat = 0 if not repeat else int(repeat) - log.debug('Updated playqueue: %s' % playqueue) - 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 = None - # Start playback - if startpos: - self.player.play(playqueue.kodi_pl, startpos=startpos) - else: - self.player.play(playqueue.kodi_pl) - playqueue.log_Kodi_playlist() + log.info('New playqueue %s received from Plex companion with offset ' + '%s, repeat %s' % (playqueue_id, offset, repeat)) + with lock: + if playqueue_id != playqueue.ID: + log.debug('Need to fetch new playQueue from the PMS') + PL.update_playlist_from_PMS(playqueue, playqueue_id) + else: + log.debug('Restarting existing playQueue') + PL.refresh_playlist_from_PMS(playqueue) + 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 = None + # Start playback. Player does not return in time + if startpos: + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, + None, + False, + startpos)) + else: + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl,)) + thread.setDaemon(True) + thread.start() - @lockmethod.lockthis - def start_playqueue_initiated_by_companion(self, - playqueue, - playqueue_id=None, - repeat=None, - offset=None): - log.info('Plex companion wants to restart playback of playqueue %s' - % playqueue) - # Still need to get new playQueue from the server - don't know what has - # been selected - PL.refresh_playlist_from_PMS(playqueue) - 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 = None - # Start playback - if startpos: - self.player.play(playqueue.kodi_pl, startpos=startpos) - else: - self.player.play(playqueue.kodi_pl) - playqueue.log_Kodi_playlist() - - @lockmethod.lockthis - def kodi_onadd(self, data): - """ - Called if an item is added to a Kodi playqueue. Data is Kodi JSON-RPC - output, e.g. - { - u'item': {u'type': u'movie', u'id': 3}, - u'playlistid': 1, - u'position': 0 - } - """ - playqueue = self.playqueues[data['playlistid']] - if window('plex_playbackProps') == 'true': - log.debug('kodi_onadd called during PKC playback setup') - if window('plex_playQueueID'): - self._grab_PMS_playqueue(playqueue, window('plex_playQueueID')) - window('plex_playQueueID', clear=True) - log.debug('Done setting up playQueue') - return - - if playqueue.PKC_playlist_edits: - old = (data['item'].get('id') if data['item'].get('id') - else data['item'].get('file')) - for i, item in enumerate(playqueue.PKC_playlist_edits): - if old == item: - log.debug('kodimonitor told us of a PKC edit - ignore') - del playqueue.PKC_playlist_edits[i] - return - if playqueue.ID is None: - # Need to initialize the queue for the first time - PL.init_Plex_playlist(playqueue, kodi_item=data['item']) - else: - PL.add_playlist_item(playqueue, data['item'], data['position']) - log.debug('Added a new item to the playqueue: %s' % playqueue) - - @lockmethod.lockthis - def kodi_onremove(self, data): - """ - Called if an item is removed from a Kodi playqueue. Data is Kodi JSON- - RPC output, e.g. - {u'position': 2, u'playlistid': 1} - """ - if window('plex_playbackProps') == 'true': - log.debug('kodi_onremove called during PKC playback setup') - return - playqueue = self.playqueues[data['playlistid']] - PL.delete_playlist_item(playqueue, data['position']) - log.debug('Deleted item at position %s. New playqueue: %s' - % (data['position'], playqueue)) - - @lockmethod.lockthis def _compare_playqueues(self, playqueue, new): """ Used to poll the Kodi playqueue and update the Plex playqueue if needed """ - if self.threadStopped(): - # Chances are that we got an empty Kodi playlist due to Kodi exit - return - old = playqueue.old_kodi_pl + old = list(playqueue.items) index = list(range(0, len(old))) log.debug('Comparing new Kodi playqueue %s with our play queue %s' - % (new, playqueue)) + % (new, old)) for i, new_item in enumerate(new): for j, old_item in enumerate(old): - if old_item.get('id') is None: - identical = old_item['file'] == new_item['file'] + 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['id'] == new_item['id'] and - old_item['type'] == new_item['type']) + 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: - # item now at pos i has been moved from original pos i+j + log.debug('Detected playqueue item %s moved to position %s' + % (i+j, i)) PL.move_playlist_item(playqueue, i + j, i) - # Delete the item we just found del old[j], index[j] break - # New elements and left-over elements will be taken care of by the kodi - # monitor! - log.debug('New playqueue: %s' % playqueue) - - def init_playlists(self): - """ - Initializes the playqueues with already existing items. - Called on startup AND for addon paths! - """ - 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: + log.debug('Detected new Kodi element: %s' % new_item) + if playqueue.ID is None: + PL.init_Plex_playlist(playqueue, + kodi_item=new_item) else: - PL.add_playlist_item(playqueue, item, i) + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + 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 - self.init_playlists() + 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 - xbmc.sleep(1000) - for playqueue in self.playqueues: - if not playqueue.items: - # Skip empty playqueues as items can't be modified - continue - 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) - xbmc.sleep(1000) + 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/utils.py b/resources/lib/utils.py index be6d96d3..eeabdb62 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -56,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 diff --git a/service.py b/service.py index c5bf737d..fc6053cd 100644 --- a/service.py +++ b/service.py @@ -44,6 +44,8 @@ from playqueue import Playqueue import PlexAPI from PlexCompanion import PlexCompanion +from monitor_kodi_play import Monitor_Kodi_Play +from playback_starter import Playback_Starter ############################################################################### @@ -73,6 +75,7 @@ class Service(): plexCompanion_running = False playqueue_running = False kodimonitor_running = False + playback_starter_running = False def __init__(self): @@ -109,7 +112,8 @@ class Service(): "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) @@ -133,6 +137,10 @@ 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() @@ -142,6 +150,7 @@ class Service(): self.library = LibrarySync(self) self.plexCompanion = PlexCompanion(self) self.playqueue = Playqueue(self) + self.playback_starter = Playback_Starter(self) plx = PlexAPI.PlexAPI() @@ -197,6 +206,9 @@ class Service(): if not self.plexCompanion_running: self.plexCompanion_running = True self.plexCompanion.start() + if not self.playback_starter_running: + self.playback_starter_running = True + self.playback_starter.start() else: if (self.user.currUser is None) and self.warn_auth: # Alert user is not authenticated and suppress future From cddde19ccecd3a20f04b698bc490ee9f844e6d5c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 14:17:28 +0100 Subject: [PATCH 18/50] Fix RuntimeErrors --- resources/lib/playbackutils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index b146624d..32152caa 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -34,12 +34,13 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item, callback): - self.mgr = callback + def __init__(self, item, callback=None): + if callback: + self.mgr = callback + self.playqueue = self.mgr.playqueue.get_playqueue_from_type( + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]) self.item = item self.api = API(item) - self.playqueue = self.mgr.playqueue.get_playqueue_from_type( - KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]) def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): """ From 5b9b432ca9b9d63509b0ca87f115b125f980cc12 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 15:41:38 +0100 Subject: [PATCH 19/50] Fixes to Watch later & Plex Companion --- default.py | 26 +++++++++++++------------- resources/lib/PlexCompanion.py | 12 +++++++++++- resources/lib/entrypoint.py | 18 ++++++++++-------- resources/lib/playbackutils.py | 13 ++++++++----- resources/lib/playqueue.py | 2 +- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/default.py b/default.py index 235734e3..fb7ea646 100644 --- a/default.py +++ b/default.py @@ -103,12 +103,12 @@ class Main(): 'watchlater': entrypoint.watchlater, 'enterPMS': entrypoint.enterPMS, 'togglePlexTV': entrypoint.togglePlexTV, - 'playwatchlater': entrypoint.playWatchLater + 'Plex_Node': entrypoint.Plex_Node } if "/extrafanart" in ARGV[0]: plexpath = ARGV[2][1:] - plexid = params.get('id', [""])[0] + plexid = params.get('id', [""]) entrypoint.getExtraFanArt(plexid, plexpath) entrypoint.getVideoFiles(plexid, plexpath) return @@ -120,7 +120,7 @@ class Main(): # Called by e.g. 3rd party plugin video extras if ("/Extras" in ARGV[0] or "/VideoFiles" in ARGV[0] or "/Extras" in ARGV[2]): - plexId = params.get('id', [None])[0] + plexId = params.get('id', None) entrypoint.getVideoFiles(plexId, params) if modes.get(mode): @@ -131,35 +131,35 @@ 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=ARGV[2]) - elif mode == 'playwatchlater': - modes[mode](params.get('id')[0], params.get('viewOffset')[0]) + elif mode == 'Plex_Node': + modes[mode](params.get('id'), params.get('viewOffset')) else: modes[mode]() else: diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 8af5687b..5eb3f896 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -11,6 +11,7 @@ from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings from PlexFunctions import ParseContainerKey import player +from entrypoint import Plex_Node ############################################################################### @@ -74,7 +75,16 @@ class PlexCompanion(Thread): 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=(data.get('key'), + data.get('offset'), + data.get('type'))) + thread.setDaemon(True) + thread.start() + elif task['action'] == 'playlist': # Get the playqueue ID try: _, ID, query = ParseContainerKey(data['containerKey']) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 27f305fc..7b2d14ce 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -22,6 +22,7 @@ import playbackutils as pbutils import PlexFunctions import PlexAPI +from playqueue import Playqueue ############################################################################### @@ -119,19 +120,18 @@ def PassPlaylist(xml, resume=None): resumeId=xml.attrib.get('playQueueSelectedItemID', None)) -def playWatchLater(itemid, viewOffset): +def Plex_Node(url, viewOffset, playlist_type): """ 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) + xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % url) if xml in (None, 401): - log.error("Could not resolve url %s" % itemid) + log.error("Could not resolve url %s" % url) return xbmcplugin.setResolvedUrl( int(sys.argv[1]), False, xbmcgui.ListItem()) if viewOffset != '0': @@ -143,7 +143,9 @@ 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') + pbutils.PlaybackUtils(xml, playlist_type=playlist_type).play( + None, 'plexnode') + return ##### DO RESET AUTH ##### @@ -1237,7 +1239,7 @@ 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) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 32152caa..656614d4 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -20,7 +20,7 @@ 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 playqueue import lock +from playqueue import lock, Playqueue from pickler import Playback_Successful ############################################################################### @@ -34,13 +34,16 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item, callback=None): + def __init__(self, item, callback=None, playlist_type=None): + self.item = item + self.api = API(item) + playlist_type = playlist_type if playlist_type else KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] if callback: self.mgr = callback self.playqueue = self.mgr.playqueue.get_playqueue_from_type( - KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()]) - self.item = item - self.api = API(item) + playlist_type) + else: + self.playqueue = Playqueue().get_playqueue_from_type(playlist_type) def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): """ diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 1d25b886..43076f37 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -29,7 +29,6 @@ class Playqueue(Thread): def __init__(self, callback=None): self.__dict__ = self.__shared_state - Thread.__init__(self) if self.playqueues is not None: return self.mgr = callback @@ -54,6 +53,7 @@ class Playqueue(Thread): 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): """ From 06bd8856b5196b9c3092386246f7cf8de7a9dd55 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 16:42:07 +0100 Subject: [PATCH 20/50] More fixes to Watch Later --- default.py | 4 +++- resources/lib/PlexCompanion.py | 5 +++-- resources/lib/PlexFunctions.py | 1 + resources/lib/entrypoint.py | 32 ++++++++++++++++++++++---------- resources/lib/playlist_func.py | 10 ++++++---- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/default.py b/default.py index fb7ea646..c2ccfddd 100644 --- a/default.py +++ b/default.py @@ -159,7 +159,9 @@ class Main(): elif mode == "companion": modes[mode](itemid, params=ARGV[2]) elif mode == 'Plex_Node': - modes[mode](params.get('id'), params.get('viewOffset')) + modes[mode](params.get('id'), + params.get('viewOffset'), + params.get('plex_type')) else: modes[mode]() else: diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 5eb3f896..27e9e82f 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -79,9 +79,10 @@ class PlexCompanion(Thread): data.get('address') == 'node.plexapp.com'): # E.g. watch later initiated by Companion thread = Thread(target=Plex_Node, - args=(data.get('key'), + args=('{server}%s' % data.get('key'), data.get('offset'), - data.get('type'))) + data.get('type'), + True),) thread.setDaemon(True) thread.start() elif task['action'] == 'playlist': diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index bd442e1d..bdc36ecd 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -61,6 +61,7 @@ KODITYPE_FROM_PLEXTYPE = { } KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { + 'video': 'video', 'movie': 'video', 'episode': 'video', 'season': 'video', diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 7b2d14ce..c6c4ec93 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -22,7 +22,7 @@ import playbackutils as pbutils import PlexFunctions import PlexAPI -from playqueue import Playqueue +from PKC_listitem import convert_PKC_to_listitem ############################################################################### @@ -120,7 +120,7 @@ def PassPlaylist(xml, resume=None): resumeId=xml.attrib.get('playQueueSelectedItemID', None)) -def Plex_Node(url, viewOffset, playlist_type): +def Plex_Node(url, viewOffset, plex_type, playdirectly=False): """ Called only for a SINGLE element for Plex.tv watch later @@ -129,11 +129,12 @@ def Plex_Node(url, viewOffset, playlist_type): 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('{server}%s' % url) - if xml in (None, 401): - log.error("Could not resolve url %s" % url) - 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 * @@ -143,9 +144,19 @@ def Plex_Node(url, viewOffset, playlist_type): else: window('plex_customplaylist.seektime', value=str(viewOffset)) log.info('Set resume point to %s' % str(viewOffset)) - pbutils.PlaybackUtils(xml, playlist_type=playlist_type).play( - None, 'plexnode') - return + typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type] + result = pbutils.PlaybackUtils(xml[0],playlist_type=typus).play( + None, + kodi_id='plexnode', + plex_lib_UUID=xml.attrib.get('librarySectionUUID')) + if result.listitem: + listitem = convert_PKC_to_listitem(result.listitem) + else: + return + if playdirectly: + xbmc.Player().play(listitem.getfilename(), listitem) + else: + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) ##### DO RESET AUTH ##### @@ -1248,6 +1259,7 @@ def watchlater(): pbutils.PlaybackUtils(item).setArtwork(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/playlist_func.py b/resources/lib/playlist_func.py index 5bc64dbb..8e355eda 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -185,11 +185,11 @@ def get_playlist_details_from_xml(playlist, xml): try: playlist.ID = xml.attrib['%sID' % playlist.kind] playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.selectedItemID = xml.attrib['%sSelectedItemID' - % playlist.kind] - playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' - % 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) @@ -309,6 +309,8 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): 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)) From c45250e74f18d1cfaf25a022ae14ad70f7cda86c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 19:07:42 +0100 Subject: [PATCH 21/50] Disable Plex trailers by default --- resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From 408648299183d5c7e4a5220344d8d1483eb058ad Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 19:16:35 +0100 Subject: [PATCH 22/50] Don't play trailer if we got items queued --- resources/lib/playbackutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 656614d4..7d661d25 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -132,8 +132,8 @@ class PlaybackUtils(): # Where will the player need to start? # Do we need to get trailers? trailers = False - if (api.getType() == 'movie' and not seektime and - settings('enableCinema') == "true"): + if (api.getType() == 'movie' and not seektime and sizePlaylist < 2 + and settings('enableCinema') == "true"): if settings('askCinema') == "true": trailers = xbmcgui.Dialog().yesno( addonName, From ed281ad8470ded0a242352a413286de7cd67746b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 2 Jan 2017 19:24:17 +0100 Subject: [PATCH 23/50] Fix playback init if there are items queued --- resources/lib/playbackutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 7d661d25..aff5af40 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -150,7 +150,7 @@ class PlaybackUtils(): get_playlist_details_from_xml(playqueue, xml=xml) log.debug('finished ') - 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 From 266ad70605c8620271a1c1443d789f10bc583860 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 4 Jan 2017 20:09:09 +0100 Subject: [PATCH 24/50] Rename database to plex.db --- resources/lib/embydb_functions.py | 2 +- resources/lib/entrypoint.py | 2 +- resources/lib/itemtypes.py | 4 ++-- resources/lib/kodimonitor.py | 2 +- resources/lib/librarysync.py | 2 +- resources/lib/read_embyserver.py | 2 +- resources/lib/utils.py | 6 +++--- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py index 3f173e70..d502a4b7 100644 --- a/resources/lib/embydb_functions.py +++ b/resources/lib/embydb_functions.py @@ -21,7 +21,7 @@ class GetEmbyDB(): and the db gets closed """ def __enter__(self): - self.embyconn = kodiSQL('emby') + self.embyconn = kodiSQL('plex') self.emby_db = Embydb_Functions(self.embyconn.cursor()) return self.emby_db diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index c6c4ec93..84fe180d 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -264,7 +264,7 @@ def deleteItem(): return from utils import kodiSQL - embyconn = kodiSQL('emby') + embyconn = kodiSQL('plex') embycursor = embyconn.cursor() emby_db = embydb.Embydb_Functions(embycursor) item = emby_db.getItem_byKodiId(dbid, itemtype) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 04b83d0f..e91ea674 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -45,7 +45,7 @@ class Items(object): """ Open DB connections and cursors """ - self.embyconn = kodiSQL('emby') + self.embyconn = kodiSQL('plex') self.embycursor = self.embyconn.cursor() self.kodiconn = kodiSQL('video') self.kodicursor = self.kodiconn.cursor() @@ -1249,7 +1249,7 @@ class Music(Items): OVERWRITE this method, because we need to open another DB. Open DB connections and cursors """ - self.embyconn = kodiSQL('emby') + self.embyconn = kodiSQL('plex') self.embycursor = self.embyconn.cursor() # Here it is, not 'video' but 'music' self.kodiconn = kodiSQL('music') diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 64059295..066613da 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -124,7 +124,7 @@ class KodiMonitor(xbmc.Monitor): log.info("Item is invalid for emby deletion.") else: # Send the delete action to the server. - embyconn = utils.kodiSQL('emby') + embyconn = utils.kodiSQL('plex') embycursor = embyconn.cursor() emby_db = embydb.Embydb_Functions(embycursor) emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 9b6f8cc4..749990cd 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -532,7 +532,7 @@ class LibrarySync(Thread): """ Run once during startup to verify that emby db exists. """ - embyconn = kodiSQL('emby') + embyconn = kodiSQL('plex') embycursor = embyconn.cursor() # Create the tables for the emby database # emby, view, version 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/utils.py b/resources/lib/utils.py index eeabdb62..c55af70e 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -217,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": @@ -363,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() From ad80fdfe1d0979e0ec0f47855f26d7b6f1556ef7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 4 Jan 2017 20:57:16 +0100 Subject: [PATCH 25/50] Rename embydb_functions to plexdb_functions --- resources/lib/PlexAPI.py | 10 +- resources/lib/context_entry.py | 10 +- resources/lib/entrypoint.py | 10 +- resources/lib/itemtypes.py | 500 ++++++------------ resources/lib/kodimonitor.py | 49 +- resources/lib/librarysync.py | 87 ++- resources/lib/player.py | 28 +- resources/lib/playlist_func.py | 82 +-- resources/lib/plexbmchelper/functions.py | 10 +- ...mbydb_functions.py => plexdb_functions.py} | 112 ++-- 10 files changed, 309 insertions(+), 589 deletions(-) rename resources/lib/{embydb_functions.py => plexdb_functions.py} (74%) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index fa8ca2d1..a437512c 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -51,7 +51,7 @@ 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 +import plexdb_functions as plexdb ############################################################################### @@ -2305,10 +2305,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)) @@ -2467,10 +2467,10 @@ class API(): 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 diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index f9772bfc..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) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 84fe180d..251ae0b5 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -17,7 +17,7 @@ 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 PlexFunctions @@ -263,12 +263,8 @@ def deleteItem(): log.error("Unknown type, unable to proceed.") return - from utils import kodiSQL - embyconn = kodiSQL('plex') - 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] diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index e91ea674..f31f1941 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -12,7 +12,7 @@ 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 @@ -45,11 +45,11 @@ class Items(object): """ Open DB connections and cursors """ - self.embyconn = kodiSQL('plex') - 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 +57,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 @@ -85,170 +85,6 @@ class Items(object): 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 +96,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 +141,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 +154,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 @@ -495,10 +331,10 @@ 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, + plex_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid) # Update the path @@ -543,23 +379,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 +406,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 +426,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 +440,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 +463,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() @@ -701,7 +532,7 @@ 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, showid, "Series", "tvshow", @@ -737,8 +568,8 @@ 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, + # Create the reference in plex table + plex_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, checksum=checksum, mediafolderid=viewid) # Update the path query = ' '.join(( @@ -765,11 +596,11 @@ 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): @@ -779,15 +610,15 @@ class TVShows(Items): log.error('Error getting itemid 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)) @@ -797,9 +628,9 @@ class TVShows(Items): checksum = API.getChecksum() # Check whether Season already exists update_item = True - emby_dbitem = emby_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(itemid) try: - embyDbItemId = emby_dbitem[0] + plexdbItemId = plex_dbitem[0] except TypeError: update_item = False @@ -808,11 +639,11 @@ 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(itemid, checksum) else: - # Create the reference in emby table - emby_db.addReference(itemid, + # Create the reference in plex table + plex_db.addReference(itemid, seasonid, "Season", "season", @@ -826,7 +657,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 +669,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 +741,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) @@ -1010,7 +832,7 @@ 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: @@ -1044,10 +866,10 @@ 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, + plex_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, seasonid, checksum) # Update the path @@ -1110,17 +932,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,8 +950,8 @@ class TVShows(Items): ##### PROCESS ITEM ##### - # Remove the emby reference - emby_db.removeItem(itemid) + # Remove the plex reference + plex_db.removeItem(itemid) ##### IF EPISODE ##### @@ -1139,19 +961,19 @@ class TVShows(Items): self.removeEpisode(kodiid, fileid) # Season verification - season = emby_db.getItem_byKodiId(parentid, "season") + season = plex_db.getItem_byKodiId(parentid, "season") try: showid = season[1] except TypeError: return - season_episodes = emby_db.getItem_byParentId(parentid, "episode") + season_episodes = plex_db.getItem_byParentId(parentid, "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, "tvshow") query = ' '.join(( "SELECT totalCount", @@ -1162,31 +984,31 @@ 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, "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, "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, "season") for season in seasons: seasonid = season[1] - season_episodes = emby_db.getItem_byParentId(seasonid, "episode") + season_episodes = plex_db.getItem_byParentId(seasonid, "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, "episode") else: - # Remove emby seasons - emby_db.removeItems_byParentId(kodiid, "season") + # Remove plex seasons + plex_db.removeItems_byParentId(kodiid, "season") # Remove tvshow self.removeShow(kodiid) @@ -1195,22 +1017,22 @@ class TVShows(Items): elif mediatype == "season": # Remove episodes, season, verify tvshow - season_episodes = emby_db.getItem_byParentId(kodiid, "episode") + season_episodes = plex_db.getItem_byParentId(kodiid, "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, "episode") # Remove season self.removeSeason(kodiid) # Show verification - seasons = emby_db.getItem_byParentId(parentid, "season") + seasons = plex_db.getItem_byParentId(parentid, "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, "tvshow") log.debug("Deleted %s: %s from kodi database" % (mediatype, itemid)) @@ -1249,12 +1071,12 @@ class Music(Items): OVERWRITE this method, because we need to open another DB. Open DB connections and cursors """ - self.embyconn = kodiSQL('plex') - 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 +1084,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,19 +1122,19 @@ 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( + # Create the reference in plex table + plex_db.addReference( itemid, artistid, artisttype, "artist", checksum=checksum) # Process the artist @@ -1343,7 +1165,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 +1174,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,19 +1215,19 @@ 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( + # Create the reference in plex table + plex_db.addReference( itemid, albumid, "MusicAlbum", "album", checksum=checksum) # Process the album info @@ -1462,41 +1284,41 @@ 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) # 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( + plex_db.addReference( parentId, parentId, "MusicArtist", "artist") - emby_db.updateParentId(itemid, parentId) + 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) 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 +1344,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 +1355,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 +1364,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 +1452,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 +1464,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 +1474,7 @@ 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), albumid, "MusicAlbum_", "album") else: # No album Id associated to the song. log.error("Song itemid: %s has no albumId associated." @@ -1662,15 +1484,15 @@ 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 = 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 @@ -1724,8 +1546,8 @@ class Music(Items): duration, year, filename, musicBrainzId, playcount, dateplayed, rating, 0, 0)) - # Create the reference in emby table - emby_db.addReference( + # Create the reference in plex table + plex_db.addReference( itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, @@ -1752,17 +1574,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. + # Artist is missing from plex database, add it. artistXml = 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 +1620,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. + # Artist is missing from plex database, add it. artistXml = 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 = ( @@ -1868,21 +1690,21 @@ 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 ##### @@ -1891,15 +1713,15 @@ class Music(Items): 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") + childs = plex_db.getItem_byParentId(item_kid, "song") if not childs: # Delete album self.removeAlbum(item_kid) @@ -1908,12 +1730,12 @@ class Music(Items): elif mediatype == "album": # Delete songs, album - album_songs = emby_db.getItem_byParentId(kodiid, "song") + album_songs = plex_db.getItem_byParentId(kodiid, "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, "song") # Remove the album self.removeAlbum(kodiid) @@ -1922,22 +1744,22 @@ class Music(Items): elif mediatype == "artist": # Delete songs, album, artist - albums = emby_db.getItem_byParentId(kodiid, "album") + albums = plex_db.getItem_byParentId(kodiid, "album") for album in albums: albumid = album[1] - album_songs = emby_db.getItem_byParentId(albumid, "song") + album_songs = plex_db.getItem_byParentId(albumid, "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, "song") + # Remove plex artist + plex_db.removeItems_byParentId(albumid, "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, "album") # Remove artist self.removeArtist(kodiid) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 066613da..4aeafa13 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -9,7 +9,7 @@ import xbmc import xbmcgui import downloadutils -import embydb_functions as embydb +import plexdb_functions as plexdb import playbackutils as pbutils from utils import window, settings, CatchExceptions, tryDecode, tryEncode from PlexFunctions import scrobble, REMAP_TYPE_FROM_PLEXTYPE @@ -93,12 +93,12 @@ 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. @@ -113,40 +113,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('plex') - 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 @@ -219,10 +186,10 @@ class KodiMonitor(xbmc.Monitor): 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) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 749990cd..fdf96ade 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']) @@ -530,25 +530,18 @@ 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('plex') - 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 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)""") + plex_db.plexcursor.execute( + """CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, view_name TEXT, media_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() @@ -634,7 +627,7 @@ 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'] @@ -652,8 +645,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] @@ -676,8 +669,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, @@ -699,10 +692,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. @@ -739,7 +732,7 @@ 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( @@ -806,15 +799,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 @@ -833,19 +826,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 @@ -1052,11 +1043,11 @@ class LibrarySync(Thread): 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('Movie')) except ValueError: self.allKodiElementsId = {} @@ -1139,11 +1130,11 @@ 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'): try: - elements = dict(emby_db.getChecksum(kind)) + elements = dict(plex.getChecksum(kind)) self.allKodiElementsId.update(elements) # Yet empty/not yet synched except ValueError: @@ -1309,10 +1300,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: @@ -1560,14 +1551,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 @@ -1669,9 +1660,9 @@ class LibrarySync(Thread): 'Movie': 'Movies', 'Series': '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: diff --git a/resources/lib/player.py b/resources/lib/player.py index 06b6b0f8..3c983315 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -11,7 +11,7 @@ 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 ############################################################################### @@ -88,13 +88,13 @@ 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 = "emby_%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") @@ -134,7 +134,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 +154,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 +190,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 +229,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 diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 8e355eda..cd94e01f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -1,9 +1,9 @@ import logging from urllib import quote -import embydb_functions as embydb +import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU -from utils import JSONRPC, tryEncode, tryDecode +from utils import JSONRPC, tryEncode from PlexAPI import API ############################################################################### @@ -12,7 +12,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -# kodi_item: +# kodi_item dict: # {u'type': u'movie', u'id': 3, 'file': path-to-file} @@ -94,12 +94,12 @@ def playlist_item_from_kodi(kodi_item): item = Playlist_Item() item.kodi_id = kodi_item.get('id') if item.kodi_id: - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byKodiId(kodi_item['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 = emby_dbitem[0] - item.plex_UUID = emby_dbitem[0] # we dont need the uuid yet :-) + 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') @@ -121,11 +121,11 @@ def playlist_item_from_plex(plex_id): """ item = Playlist_Item() item.plex_id = plex_id - with embydb.GetEmbyDB() as emby_db: - emby_dbitem = emby_db.getItem_byId(plex_id) + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(plex_id) try: - item.kodi_id = emby_dbitem[0] - item.kodi_type = emby_dbitem[4] + 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 @@ -141,8 +141,8 @@ def playlist_item_from_xml(playlist, xml_video_element): item.ID = xml_video_element.attrib['%sItemID' % playlist.kind] item.guid = xml_video_element.attrib.get('guid') if item.plex_id: - with embydb.GetEmbyDB() as emby_db: - db_element = emby_db.getItem_byId(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: @@ -517,59 +517,3 @@ def remove_from_Kodi_playlist(playlist, pos): 'position': pos })) del playlist.items[pos] - - -# NOT YET UPDATED!! - -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 = PF.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) - -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) diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index 443b1c8f..d9d19f85 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 diff --git a/resources/lib/embydb_functions.py b/resources/lib/plexdb_functions.py similarity index 74% rename from resources/lib/embydb_functions.py rename to resources/lib/plexdb_functions.py index d502a4b7..fa8d0eca 100644 --- a/resources/lib/embydb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -12,29 +12,29 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -class GetEmbyDB(): +class Get_Plex_DB(): """ - Usage: with GetEmbyDB() as emby_db: - do stuff with emby_db + Usage: with Get_Plex_DB() as plexcursor: + plexcursor.do_something() On exiting "with" (no matter what), commits get automatically committed and the db gets closed """ def __enter__(self): - self.embyconn = kodiSQL('plex') - self.emby_db = Embydb_Functions(self.embyconn.cursor()) - return self.emby_db + self.plexconn = kodiSQL('plex') + self.plexcursor = Plex_DB_Functions(self.plexconn.cursor()) + return self.plexcursor def __exit__(self, type, value, traceback): - self.embyconn.commit() - self.embyconn.close() + self.plexconn.commit() + self.plexconn.close() -class Embydb_Functions(): +class Plex_DB_Functions(): - def __init__(self, embycursor): + def __init__(self, plexcursor): - self.embycursor = embycursor + self.plexcursor = plexcursor def getViews(self): @@ -45,15 +45,15 @@ class Embydb_Functions(): "SELECT view_id", "FROM view" )) - self.embycursor.execute(query) - rows = self.embycursor.fetchall() + self.plexcursor.execute(query) + rows = self.plexcursor.fetchall() for row in rows: views.append(row[0]) return views def getAllViewInfo(self): - embycursor = self.embycursor + plexcursor = self.plexcursor views = [] query = ' '.join(( @@ -61,8 +61,8 @@ class Embydb_Functions(): "SELECT view_id, view_name, media_type", "FROM view" )) - embycursor.execute(query) - rows = embycursor.fetchall() + plexcursor.execute(query) + rows = plexcursor.fetchall() for row in rows: views.append({'id': row[0], 'name': row[1], @@ -78,8 +78,8 @@ class Embydb_Functions(): "FROM view", "WHERE view_id = ?" )) - self.embycursor.execute(query, (viewid,)) - view = self.embycursor.fetchone() + self.plexcursor.execute(query, (viewid,)) + view = self.plexcursor.fetchone() return view @@ -93,8 +93,8 @@ class Embydb_Functions(): "FROM view", "WHERE media_type = ?" )) - self.embycursor.execute(query, (mediatype,)) - rows = self.embycursor.fetchall() + self.plexcursor.execute(query, (mediatype,)) + rows = self.plexcursor.fetchall() for row in rows: views.append({ @@ -113,9 +113,9 @@ class Embydb_Functions(): "FROM view", "WHERE view_name = ?" )) - self.embycursor.execute(query, (tagname,)) + self.plexcursor.execute(query, (tagname,)) try: - view = self.embycursor.fetchone()[0] + view = self.plexcursor.fetchone()[0] except TypeError: view = None @@ -132,7 +132,7 @@ class Embydb_Functions(): VALUES (?, ?, ?, ?) ''' ) - self.embycursor.execute(query, (plexid, name, mediatype, tagid)) + self.plexcursor.execute(query, (plexid, name, mediatype, tagid)) def updateView(self, name, tagid, mediafolderid): @@ -142,7 +142,7 @@ class Embydb_Functions(): "SET view_name = ?, kodi_tagid = ?", "WHERE view_id = ?" )) - self.embycursor.execute(query, (name, tagid, mediafolderid)) + self.plexcursor.execute(query, (name, tagid, mediafolderid)) def removeView(self, viewid): @@ -151,7 +151,7 @@ class Embydb_Functions(): "DELETE FROM view", "WHERE view_id = ?" )) - self.embycursor.execute(query, (viewid,)) + self.plexcursor.execute(query, (viewid,)) def getItem_byFileId(self, fileId, kodiType): """ @@ -165,8 +165,8 @@ class Embydb_Functions(): "WHERE kodi_fileid = ? AND media_type = ?" )) try: - self.embycursor.execute(query, (fileId, kodiType)) - item = self.embycursor.fetchone()[0] + self.plexcursor.execute(query, (fileId, kodiType)) + item = self.plexcursor.fetchone()[0] return item except: return None @@ -183,8 +183,8 @@ class Embydb_Functions(): "WHERE kodi_id = ? AND media_type = ?" )) try: - self.embycursor.execute(query, (fileId, kodiType)) - item = self.embycursor.fetchone()[0] + self.plexcursor.execute(query, (fileId, kodiType)) + item = self.plexcursor.fetchone()[0] return item except: return None @@ -198,8 +198,8 @@ class Embydb_Functions(): "WHERE emby_id = ?" )) try: - self.embycursor.execute(query, (plexid,)) - item = self.embycursor.fetchone() + self.plexcursor.execute(query, (plexid,)) + item = self.plexcursor.fetchone() return item except: return None @@ -211,8 +211,8 @@ class Embydb_Functions(): "FROM emby", "WHERE emby_id LIKE ?" )) - self.embycursor.execute(query, (plexid+"%",)) - return self.embycursor.fetchall() + self.plexcursor.execute(query, (plexid+"%",)) + return self.plexcursor.fetchall() def getItem_byView(self, mediafolderid): @@ -222,8 +222,8 @@ class Embydb_Functions(): "FROM emby", "WHERE media_folder = ?" )) - self.embycursor.execute(query, (mediafolderid,)) - return self.embycursor.fetchall() + self.plexcursor.execute(query, (mediafolderid,)) + return self.plexcursor.fetchall() def getPlexId(self, kodiid, mediatype): """ @@ -236,8 +236,8 @@ class Embydb_Functions(): "WHERE kodi_id = ? AND media_type = ?" )) try: - self.embycursor.execute(query, (kodiid, mediatype)) - item = self.embycursor.fetchone() + self.plexcursor.execute(query, (kodiid, mediatype)) + item = self.plexcursor.fetchone() return item except: return None @@ -251,8 +251,8 @@ class Embydb_Functions(): "WHERE kodi_id = ?", "AND media_type = ?" )) - self.embycursor.execute(query, (kodiid, mediatype,)) - return self.embycursor.fetchone() + self.plexcursor.execute(query, (kodiid, mediatype,)) + return self.plexcursor.fetchone() def getItem_byParentId(self, parentid, mediatype): @@ -263,8 +263,8 @@ class Embydb_Functions(): "WHERE parent_id = ?", "AND media_type = ?" )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() + self.plexcursor.execute(query, (parentid, mediatype,)) + return self.plexcursor.fetchall() def getItemId_byParentId(self, parentid, mediatype): @@ -275,8 +275,8 @@ class Embydb_Functions(): "WHERE parent_id = ?", "AND media_type = ?" )) - self.embycursor.execute(query, (parentid, mediatype,)) - return self.embycursor.fetchall() + self.plexcursor.execute(query, (parentid, mediatype,)) + return self.plexcursor.fetchall() def getChecksum(self, mediatype): @@ -286,8 +286,8 @@ class Embydb_Functions(): "FROM emby", "WHERE emby_type = ?" )) - self.embycursor.execute(query, (mediatype,)) - return self.embycursor.fetchall() + self.plexcursor.execute(query, (mediatype,)) + return self.plexcursor.fetchall() def getMediaType_byId(self, plexid): @@ -297,9 +297,9 @@ class Embydb_Functions(): "FROM emby", "WHERE emby_id = ?" )) - self.embycursor.execute(query, (plexid,)) + self.plexcursor.execute(query, (plexid,)) try: - itemtype = self.embycursor.fetchone()[0] + itemtype = self.plexcursor.fetchone()[0] except TypeError: itemtype = None @@ -330,18 +330,18 @@ class Embydb_Functions(): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''' ) - self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype, + self.plexcursor.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)) + self.plexcursor.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)) + self.plexcursor.execute(query, (parent_kodiid, plexid)) def removeItems_byParentId(self, parent_kodiid, mediatype): @@ -351,7 +351,7 @@ class Embydb_Functions(): "WHERE parent_id = ?", "AND media_type = ?" )) - self.embycursor.execute(query, (parent_kodiid, mediatype,)) + self.plexcursor.execute(query, (parent_kodiid, mediatype,)) def removeItem_byKodiId(self, kodiid, mediatype): @@ -361,17 +361,17 @@ class Embydb_Functions(): "WHERE kodi_id = ?", "AND media_type = ?" )) - self.embycursor.execute(query, (kodiid, mediatype,)) + self.plexcursor.execute(query, (kodiid, mediatype,)) def removeItem(self, plexid): query = "DELETE FROM emby WHERE emby_id = ?" - self.embycursor.execute(query, (plexid,)) + self.plexcursor.execute(query, (plexid,)) def removeWildItem(self, plexid): query = "DELETE FROM emby WHERE emby_id LIKE ?" - self.embycursor.execute(query, (plexid+"%",)) + self.plexcursor.execute(query, (plexid+"%",)) def itemsByType(self, plextype): """ @@ -390,9 +390,9 @@ class Embydb_Functions(): "FROM emby", "WHERE emby_type = ?", )) - self.embycursor.execute(query, (plextype, )) + self.plexcursor.execute(query, (plextype, )) result = [] - for row in self.embycursor.fetchall(): + for row in self.plexcursor.fetchall(): result.append({ 'plexId': row[0], 'kodiId': row[1], From e90f48cc8ec0302d42442bbdb0d222cbe6e57f3b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 7 Jan 2017 20:11:48 +0100 Subject: [PATCH 26/50] Rename plex database fields; abstract types --- resources/lib/PlexAPI.py | 8 +- resources/lib/PlexFunctions.py | 126 +++++--- resources/lib/itemtypes.py | 193 ++++++----- resources/lib/kodidb_functions.py | 9 +- resources/lib/librarysync.py | 83 +++-- resources/lib/playback_starter.py | 4 +- resources/lib/playbackutils.py | 11 +- resources/lib/player.py | 3 +- resources/lib/plexdb_functions.py | 519 +++++++++++++++--------------- 9 files changed, 527 insertions(+), 429 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index a437512c..3337a2a9 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -50,7 +50,7 @@ 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 + REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW import plexdb_functions as plexdb ############################################################################### @@ -1915,9 +1915,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 +1927,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... diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index bdc36ecd..97d8d611 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -20,71 +20,105 @@ 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 = 'audio' +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', } 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 = { - 'video': 'video', - 'movie': 'video', - 'episode': 'video', - 'season': 'video', - 'tvshow': 'video', - 'clip': 'video', - 'artist': 'audio', - 'album': 'audio', - 'track': 'audio', - 'song': 'audio' + 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 } REMAP_TYPE_FROM_PLEXTYPE = { - 'movie': 'movie', - 'show': 'tv', - 'season': 'tv', - 'episode': 'tv', - 'artist': 'music', - 'album': 'music', - 'song': 'music', - 'track': 'music', - 'clip': 'clip', - 'photo': 'photo' + 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' } diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f31f1941..40cf7e70 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -7,8 +7,6 @@ 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 @@ -16,7 +14,7 @@ import plexdb_functions as plexdb import kodidb_functions as kodidb import PlexAPI -from PlexFunctions import GetPlexMetadata +import PlexFunctions as PF ############################################################################### @@ -75,13 +73,13 @@ 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) @@ -334,8 +332,15 @@ class Movies(Items): # 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 - plex_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(( @@ -533,12 +538,12 @@ class TVShows(Items): # Add reference is idempotent; the call here updates also fileid # and pathid when item is moved or renamed 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: @@ -569,8 +574,13 @@ class TVShows(Items): kodicursor.execute(query, (showid, pathid)) # Create the reference in plex table - plex_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) + 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(( @@ -605,9 +615,9 @@ class TVShows(Items): @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 plex_db = self.plex_db @@ -621,14 +631,14 @@ class TVShows(Items): 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 - plex_dbitem = plex_db.getItem_byId(itemid) + plex_dbitem = plex_db.getItem_byId(plex_id) try: plexdbItemId = plex_dbitem[0] except TypeError: @@ -640,14 +650,15 @@ class TVShows(Items): if update_item: # Update a reference: checksum in plex table - plex_db.updateReference(itemid, checksum) + plex_db.updateReference(plex_id, checksum) else: # Create the reference in plex table - plex_db.addReference(itemid, + 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) @@ -869,8 +880,15 @@ class TVShows(Items): # 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 - plex_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(( @@ -956,24 +974,25 @@ class TVShows(Items): ##### 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 = plex_db.getItem_byKodiId(parentid, "season") + season = plex_db.getItem_byKodiId(parentid, PF.KODI_TYPE_SEASON) try: showid = season[1] except TypeError: return - season_episodes = plex_db.getItem_byParentId(parentid, "episode") + season_episodes = plex_db.getItem_byParentId(parentid, + PF.KODI_TYPE_EPISODE) if not season_episodes: self.removeSeason(parentid) plex_db.removeItem(season[0]) # Show verification - show = plex_db.getItem_byKodiId(showid, "tvshow") + show = plex_db.getItem_byKodiId(showid, PF.KODI_TYPE_SHOW) query = ' '.join(( "SELECT totalCount", @@ -984,12 +1003,14 @@ 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 = plex_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 plex season entries - plex_db.removeItems_byParentId(showid, "season") + plex_db.removeItems_byParentId(showid, + PF.KODI_TYPE_SEASON) self.removeShow(showid) plex_db.removeItem(show[0]) @@ -997,42 +1018,47 @@ class TVShows(Items): elif mediatype == "tvshow": # Remove episodes, seasons, tvshow - seasons = plex_db.getItem_byParentId(kodiid, "season") + seasons = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_SEASON) for season in seasons: seasonid = season[1] - season_episodes = plex_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 plex episodes - plex_db.removeItems_byParentId(seasonid, "episode") + plex_db.removeItems_byParentId(seasonid, + PF.KODI_TYPE_EPISODE) else: # Remove plex seasons - plex_db.removeItems_byParentId(kodiid, "season") + 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 = plex_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 plex episodes - plex_db.removeItems_byParentId(kodiid, "episode") + plex_db.removeItems_byParentId(kodiid, PF.KODI_TYPE_EPISODE) # Remove season self.removeSeason(kodiid) # Show verification - seasons = plex_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) - plex_db.removeItem_byKodiId(parentid, "tvshow") + plex_db.removeItem_byKodiId(parentid, PF.KODI_TYPE_SHOW) log.debug("Deleted %s: %s from kodi database" % (mediatype, itemid)) @@ -1134,8 +1160,11 @@ class Music(Items): # artist entries. artistid = self.kodi_db.addArtist(name, musicBrainzId) # Create the reference in plex table - plex_db.addReference( - itemid, artistid, artisttype, "artist", checksum=checksum) + plex_db.addReference(itemid, + PF.PLEX_TYPE_ARTIST, + artistid, + PF.KODI_TYPE_ARTIST, + checksum=checksum) # Process the artist if KODIVERSION in (16, 17): @@ -1227,8 +1256,11 @@ class Music(Items): # artist entries. albumid = self.kodi_db.addAlbum(name, musicBrainzId) # Create the reference in plex table - plex_db.addReference( - itemid, albumid, "MusicAlbum", "album", checksum=checksum) + plex_db.addReference(itemid, + PF.PLEX_TYPE_ALBUM, + albumid, + PF.KODI_TYPE_ALBUM, + checksum=checksum) # Process the album info if KODIVERSION == 17: @@ -1293,13 +1325,15 @@ class Music(Items): except TypeError: 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 - plex_db.addReference( - parentId, parentId, "MusicArtist", "artist") + plex_db.addReference(parentId, + PF.PLEX_TYPE_ARTIST, + parentId, + PF.KODI_TYPE_ARTIST) plex_db.updateParentId(itemid, parentId) else: # Update plex reference with the artistid @@ -1314,7 +1348,7 @@ class Music(Items): except TypeError: # 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") plex_dbartist = plex_db.getItem_byId(artistId) @@ -1474,7 +1508,10 @@ class Music(Items): log.info("Creating virtual music album for song: %s." % itemid) albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) - plex_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." @@ -1485,7 +1522,7 @@ class Music(Items): # No album found. Let's create it log.info("Album database entry missing.") plex_albumId = item.attrib.get('parentRatingKey') - album = GetPlexMetadata(plex_albumId) + album = PF.GetPlexMetadata(plex_albumId) if album is None or album == 401: log.error('Could not download album, abort') return @@ -1547,11 +1584,13 @@ class Music(Items): dateplayed, rating, 0, 0)) # Create the reference in plex table - plex_db.addReference( - itemid, songid, "Audio", "song", - pathid=pathid, - parentid=albumid, - checksum=checksum) + 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 = ( @@ -1579,7 +1618,7 @@ class Music(Items): artistid = artist_edb[0] except TypeError: # Artist is missing from plex database, add it. - artistXml = GetPlexMetadata(artist_eid) + artistXml = PF.GetPlexMetadata(artist_eid) if artistXml is None or artistXml == 401: log.error('Error getting artist, abort') return @@ -1625,7 +1664,7 @@ class Music(Items): artistid = artist_edb[0] except TypeError: # Artist is missing from plex database, add it. - artistXml = GetPlexMetadata(artist_eid) + artistXml = PF.GetPlexMetadata(artist_eid) if artistXml is None or artistXml == 401: log.error('Error getting artist, abort') return @@ -1708,7 +1747,7 @@ class Music(Items): ##### 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 @@ -1720,46 +1759,54 @@ class Music(Items): item_kid = item[0] item_mediatype = item[1] - if item_mediatype == "album": - childs = plex_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 = plex_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 plex songs - plex_db.removeItems_byParentId(kodiid, "song") + 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 = plex_db.getItem_byParentId(kodiid, "album") + albums = plex_db.getItem_byParentId(kodiid, + PF.KODI_TYPE_ALBUM) for album in albums: albumid = album[1] - album_songs = plex_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 plex song - plex_db.removeItems_byParentId(albumid, "song") + plex_db.removeItems_byParentId(albumid, + PF.KODI_TYPE_SONG) # Remove plex artist - plex_db.removeItems_byParentId(albumid, "artist") + plex_db.removeItems_byParentId(albumid, + PF.KODI_TYPE_ARTIST) # Remove kodi album self.removeAlbum(albumid) else: # Remove plex albums - plex_db.removeItems_byParentId(kodiid, "album") + plex_db.removeItems_byParentId(kodiid, + PF.KODI_TYPE_ALBUM) # Remove artist self.removeArtist(kodiid) @@ -1767,16 +1814,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 fd682a07..d02d2fb6 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 ############################################################################### @@ -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", diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index fdf96ade..fd47acb3 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -446,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: @@ -534,14 +536,28 @@ class LibrarySync(Thread): """ with plexdb.Get_Plex_DB() as plex_db: # Create the tables for the plex database - plex_db.plexcursor.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)""") - plex_db.plexcursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - plex_db.plexcursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") + 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() @@ -632,7 +648,8 @@ class LibrarySync(Thread): 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 @@ -656,12 +673,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, @@ -715,7 +732,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, @@ -739,9 +756,9 @@ class LibrarySync(Thread): 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, @@ -776,22 +793,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) @@ -1020,9 +1037,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, @@ -1038,7 +1056,7 @@ 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 = {} @@ -1047,7 +1065,8 @@ class LibrarySync(Thread): # Get movies from Plex server # Pull the list of movies and boxsets in Kodi try: - self.allKodiElementsId = dict(plex_db.getChecksum('Movie')) + self.allKodiElementsId = dict( + plex_db.getChecksum(PF.PLEX_TYPE_MOVIE)) except ValueError: self.allKodiElementsId = {} @@ -1132,7 +1151,9 @@ class LibrarySync(Thread): if self.compare: 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(plex.getChecksum(kind)) self.allKodiElementsId.update(elements) @@ -1657,8 +1678,8 @@ class LibrarySync(Thread): """ items = [] typus = { - 'Movie': 'Movies', - 'Series': 'TVShows' + PF.PLEX_TYPE_MOVIE: 'Movies', + PF.PLEX_TYPE_SHOW: 'TVShows' } with plexdb.Get_Plex_DB() as plex_db: for plextype in typus: diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index ddc10afe..c4b78b3f 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -8,7 +8,7 @@ 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 +from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO from PlexAPI import API from playqueue import lock @@ -38,7 +38,7 @@ class Playback_Starter(Thread): # Todo: Warn user with dialog return xml = GetPlexMetadata(plex_id) - if xml[0].attrib.get('type') == 'photo': + if xml[0].attrib.get('type') == PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() listitem = PKC_ListItem() diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index aff5af40..b5f65a3f 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -15,7 +15,7 @@ import downloadutils from PlexAPI import API from PlexFunctions import GetPlexPlaylist, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, \ - KODITYPE_FROM_PLEXTYPE + KODITYPE_FROM_PLEXTYPE, 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, \ @@ -37,7 +37,8 @@ class PlaybackUtils(): def __init__(self, item, callback=None, playlist_type=None): self.item = item self.api = API(item) - playlist_type = playlist_type if playlist_type else KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] + playlist_type = playlist_type if playlist_type else \ + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] if callback: self.mgr = callback self.playqueue = self.mgr.playqueue.get_playqueue_from_type( @@ -132,8 +133,10 @@ class PlaybackUtils(): # Where will the player need to start? # Do we need to get trailers? trailers = False - if (api.getType() == 'movie' and not seektime and sizePlaylist < 2 - and settings('enableCinema') == "true"): + 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, diff --git a/resources/lib/player.py b/resources/lib/player.py index 3c983315..6c79d76c 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -13,6 +13,7 @@ import clientinfo import downloadutils import plexdb_functions as plexdb import kodidb_functions as kodidb +from PlexFunctions import KODI_TYPE_MOVIE, KODI_TYPE_EPISODE ############################################################################### @@ -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'], diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index fa8d0eca..3859fab4 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -14,16 +14,15 @@ log = logging.getLogger("PLEX."+__name__) class Get_Plex_DB(): """ - Usage: with Get_Plex_DB() as plexcursor: - plexcursor.do_something() + 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') - self.plexcursor = Plex_DB_Functions(self.plexconn.cursor()) - return self.plexcursor + return Plex_DB_Functions(self.plexconn.cursor()) def __exit__(self, type, value, traceback): self.plexconn.commit() @@ -33,18 +32,17 @@ class Get_Plex_DB(): class Plex_DB_Functions(): def __init__(self, plexcursor): - self.plexcursor = plexcursor def getViews(self): - + """ + Returns a list of view_id + """ views = [] - - query = ' '.join(( - - "SELECT view_id", - "FROM view" - )) + query = ''' + SELECT view_id + FROM view + ''' self.plexcursor.execute(query) rows = self.plexcursor.fetchall() for row in rows: @@ -52,15 +50,16 @@ class Plex_DB_Functions(): return views def getAllViewInfo(self): - + """ + Returns a list of dicts: + {'id': view_id, 'name': view_name, 'itemtype': kodi_type} + """ plexcursor = self.plexcursor views = [] - - query = ' '.join(( - - "SELECT view_id, view_name, media_type", - "FROM view" - )) + query = ''' + SELECT view_id, view_name, kodi_type + FROM view + ''' plexcursor.execute(query) rows = plexcursor.fetchall() for row in rows: @@ -69,334 +68,324 @@ class Plex_DB_Functions(): '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.plexcursor.execute(query, (viewid,)) + 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, mediatype): - + 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 = ' '.join(( - - "SELECT view_id, view_name, media_type", - "FROM view", - "WHERE media_type = ?" - )) - self.plexcursor.execute(query, (mediatype,)) + 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, tagname): - - query = ' '.join(( - - "SELECT view_id", - "FROM view", - "WHERE view_name = ?" - )) - self.plexcursor.execute(query, (tagname,)) + 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, plexid, name, mediatype, tagid): - - query = ( - ''' + 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, media_type, kodi_tagid) - + view_id, view_name, kodi_type, kodi_tagid) VALUES (?, ?, ?, ?) ''' - ) - self.plexcursor.execute(query, (plexid, name, mediatype, tagid)) + self.plexcursor.execute(query, + (view_id, view_name, kodi_type, kodi_tagid)) - def updateView(self, name, tagid, mediafolderid): - - query = ' '.join(( - - "UPDATE view", - "SET view_name = ?, kodi_tagid = ?", - "WHERE view_id = ?" - )) - self.plexcursor.execute(query, (name, tagid, mediafolderid)) - - def removeView(self, viewid): - - query = ' '.join(( - - "DELETE FROM view", - "WHERE view_id = ?" - )) - self.plexcursor.execute(query, (viewid,)) - - def getItem_byFileId(self, fileId, kodiType): + def updateView(self, view_name, kodi_tagid, view_id): """ - Returns the Plex itemId by using the Kodi fileId. VIDEO ONLY - - kodiType: 'movie', 'episode', ... + Updates the view_id with view_name and kodi_tagid """ - query = ' '.join(( - "SELECT emby_id", - "FROM emby", - "WHERE kodi_fileid = ? AND media_type = ?" - )) + 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, (fileId, kodiType)) + self.plexcursor.execute(query, (kodi_fileid, kodi_type)) item = self.plexcursor.fetchone()[0] return item except: return None - def getMusicItem_byFileId(self, fileId, kodiType): + def getMusicItem_byFileId(self, kodi_id, kodi_type): """ - Returns the Plex itemId by using the Kodi fileId. MUSIC ONLY + Returns the plex_id for kodi_id and kodi_type - kodiType: 'song' + None if not found """ - query = ' '.join(( - "SELECT emby_id", - "FROM emby", - "WHERE kodi_id = ? AND media_type = ?" - )) + query = ''' + SELECT plex_id + FROM plex + WHERE kodi_id = ? AND kodi_type = ? + ''' try: - self.plexcursor.execute(query, (fileId, kodiType)) + self.plexcursor.execute(query, (kodi_id, kodi_type)) item = self.plexcursor.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.plexcursor.execute(query, (plexid,)) - item = self.plexcursor.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.plexcursor.execute(query, (plexid+"%",)) - return self.plexcursor.fetchall() - - def getItem_byView(self, mediafolderid): - - query = ' '.join(( - - "SELECT kodi_id", - "FROM emby", - "WHERE media_folder = ?" - )) - self.plexcursor.execute(query, (mediafolderid,)) - return self.plexcursor.fetchall() - - def getPlexId(self, kodiid, mediatype): + def getItem_byId(self, plex_id): """ - Returns the Plex ID usind the Kodiid. Result: - (Plex Id, Parent's 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 = ' '.join(( - "SELECT emby_id, parent_id", - "FROM emby", - "WHERE kodi_id = ? AND media_type = ?" - )) + query = ''' + SELECT kodi_id, kodi_fileid, kodi_pathid, + parent_id, kodi_type, plex_type + FROM plex + WHERE plex_id = ? + ''' try: - self.plexcursor.execute(query, (kodiid, mediatype)) + self.plexcursor.execute(query, (plex_id,)) item = self.plexcursor.fetchone() return item except: return None - def getItem_byKodiId(self, kodiid, mediatype): + 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() - query = ' '.join(( + 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() - "SELECT emby_id, parent_id", - "FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.plexcursor.execute(query, (kodiid, mediatype,)) + 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, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id, kodi_fileid", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.plexcursor.execute(query, (parentid, mediatype,)) + 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, parentid, mediatype): - - query = ' '.join(( - - "SELECT emby_id, kodi_id", - "FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.plexcursor.execute(query, (parentid, mediatype,)) + 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, mediatype): - - query = ' '.join(( - - "SELECT emby_id, checksum", - "FROM emby", - "WHERE emby_type = ?" - )) - self.plexcursor.execute(query, (mediatype,)) + 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, plexid): + def getMediaType_byId(self, plex_id): + """ + Returns plex_type for plex_id - query = ' '.join(( - - "SELECT emby_type", - "FROM emby", - "WHERE emby_id = ?" - )) - self.plexcursor.execute(query, (plexid,)) + 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 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) - + 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, (plexid, kodiid, fileid, pathid, embytype, mediatype, - parentid, checksum, mediafolderid)) + self.plexcursor.execute(query, (plex_id, kodi_id, kodi_fileid, + kodi_pathid, plex_type, kodi_type, + parent_id, checksum, view_id)) - def updateReference(self, plexid, checksum): - - query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" - self.plexcursor.execute(query, (checksum, plexid)) + 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): - - query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?" + """ + 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_kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE parent_id = ?", - "AND media_type = ?" - )) - self.plexcursor.execute(query, (parent_kodiid, mediatype,)) - - def removeItem_byKodiId(self, kodiid, mediatype): - - query = ' '.join(( - - "DELETE FROM emby", - "WHERE kodi_id = ?", - "AND media_type = ?" - )) - self.plexcursor.execute(query, (kodiid, mediatype,)) - - def removeItem(self, plexid): - - query = "DELETE FROM emby WHERE emby_id = ?" - self.plexcursor.execute(query, (plexid,)) - - def removeWildItem(self, plexid): - - query = "DELETE FROM emby WHERE emby_id LIKE ?" - self.plexcursor.execute(query, (plexid+"%",)) - - def itemsByType(self, plextype): + def removeItems_byParentId(self, parent_id, kodi_type): """ - Returns a list of dictionaries for all Kodi DB items present for - plextype. One dict is of the 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': the Plex id - 'kodiId': the Kodi id - 'kodi_type': e.g. 'movie', 'tvshow' - 'plex_type': e.g. 'Movie', 'Series', the input plextype + 'plexId': plex_id + 'kodiId': kodi_id + 'kodi_type': kodi_type + 'plex_type': plex_type } """ - query = ' '.join(( - "SELECT emby_id, kodi_id, media_type", - "FROM emby", - "WHERE emby_type = ?", - )) - self.plexcursor.execute(query, (plextype, )) + 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': plextype + 'plex_type': plex_type }) return result From 0a2f1bc01bb3d13265d3f4e7c70a3304d8a11619 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 7 Jan 2017 20:20:18 +0100 Subject: [PATCH 27/50] Fix KeyError for librarysync --- resources/lib/PlexFunctions.py | 10 ++++++++++ resources/lib/librarysync.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 97d8d611..c998740a 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -83,6 +83,16 @@ ITEMTYPE_FROM_PLEXTYPE = { 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 = { PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE, PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE, diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index fd47acb3..1d6af002 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1666,7 +1666,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) From 234f5042a34f0c24076dc1324a412fef06dc5ee1 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 7 Jan 2017 20:35:10 +0100 Subject: [PATCH 28/50] Fix Plex Companion music playQueue startup --- resources/lib/PlexCompanion.py | 4 ++-- resources/lib/PlexFunctions.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 27e9e82f..92dd5f1d 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -9,7 +9,7 @@ from xbmc import sleep from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings -from PlexFunctions import ParseContainerKey +from PlexFunctions import ParseContainerKey, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE import player from entrypoint import Plex_Node @@ -95,7 +95,7 @@ class PlexCompanion(Thread): log.error("Traceback:\n%s" % traceback.format_exc()) return playqueue = self.mgr.playqueue.get_playqueue_from_type( - data['type']) + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) self.mgr.playqueue.update_playqueue_from_PMS( playqueue, ID, diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index c998740a..a6595932 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -30,7 +30,7 @@ PLEX_TYPE_EPISODE = 'episode' PLEX_TYPE_SEASON = 'season' PLEX_TYPE_SHOW = 'show' -PLEX_TYPE_AUDIO = 'audio' +PLEX_TYPE_AUDIO = 'music' PLEX_TYPE_SONG = 'track' PLEX_TYPE_ALBUM = 'album' PLEX_TYPE_ARTIST = 'artist' @@ -115,7 +115,8 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { 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_SONG: KODI_TYPE_AUDIO, + PLEX_TYPE_AUDIO: KODI_TYPE_AUDIO } From 8e6d94129b69c0018d6310adde9972476a7ee8b3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 11:38:37 +0100 Subject: [PATCH 29/50] Fix keyError for url-encoding --- resources/lib/artwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ef8d36c0727cc06d55a357c76aa0e2655c2bd014 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 11:46:19 +0100 Subject: [PATCH 30/50] Fix for Plex Companion not showing up - Should fix #173 --- resources/lib/clientinfo.py | 2 +- resources/lib/plexbmchelper/functions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index d9d19f85..5885253d 100644 --- a/resources/lib/plexbmchelper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -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", From 4a28a8ebc97615937a372aaf6ddc1e183d229242 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 12:07:03 +0100 Subject: [PATCH 31/50] Don't support Plex Companion mirror - Should fix #131 --- resources/lib/plexbmchelper/plexgdm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'], From 4aab4813a9fcbd44987679b454bd394c19a255d3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 12:20:51 +0100 Subject: [PATCH 32/50] Less logging --- resources/lib/playbackutils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index b5f65a3f..355afc1f 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -149,9 +149,7 @@ class PlaybackUtils(): plex_lib_UUID, mediatype=api.getType(), trailers=trailers) - log.debug('xml: ID: %s' % xml.attrib['playQueueID']) get_playlist_details_from_xml(playqueue, xml=xml) - log.debug('finished ') if (not homeScreen and not seektime and sizePlaylist < 2 and window('plex_customplaylist') != "true" and From a53203c828a9b6d2f0bf727b4db3b669289fb8f9 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 12:35:58 +0100 Subject: [PATCH 33/50] Check xml playQueue --- resources/lib/playlist_func.py | 10 +++++----- resources/lib/playqueue.py | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index cd94e01f..30479f74 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -209,11 +209,11 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): """ if xml is None: xml = get_PMS_playlist(playlist, playlist_id) - try: - xml.attrib['%sVersion' % playlist.kind] - except: - log.error('Could not download Plex playlist.') - return + try: + xml.attrib['%sVersion' % playlist.kind] + except: + log.error('Could not download Plex playlist.') + return # Clear our existing playlist and the associated Kodi playlist playlist.clear() # Set new values diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 43076f37..c58f127a 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -101,14 +101,18 @@ class Playqueue(Thread): startpos = None # Start playback. Player does not return in time if startpos: + log.debug('Start position Plex Companion playback: %s' + % startpos) thread = Thread(target=Player().play, args=(playqueue.kodi_pl, None, False, startpos)) else: + log.debug('Start Plex Companion playback from beginning') thread = Thread(target=Player().play, args=(playqueue.kodi_pl,)) + log.debug('Playqueues are: %s' % self.playqueues) thread.setDaemon(True) thread.start() From 1b03486ecfb9821ad8f54492e86c1b8ff4478e55 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 12:48:57 +0100 Subject: [PATCH 34/50] Fix unnecessary syncs for music --- resources/lib/librarysync.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 1d6af002..a6ae9b8e 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1273,22 +1273,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) From 1b9ae0c9245ce0734669feebad3ccd16a4fdfd25 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 12:56:40 +0100 Subject: [PATCH 35/50] Increase logging --- resources/lib/playbackutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 355afc1f..65043071 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -289,7 +289,7 @@ class PlaybackUtils(): contextmenu_play): # Playlist was created just now, play it. # Contextmenu plays always need this - log.info("Play playlist") + 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)) From 0dfafbbe3ee40d43f28cc17aea51f73de0c3765a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:03:41 +0100 Subject: [PATCH 36/50] Move playbackutils methods to API --- resources/lib/PlexAPI.py | 70 +++++++++++++++++++- resources/lib/entrypoint.py | 8 +-- resources/lib/kodidb_functions.py | 4 +- resources/lib/kodimonitor.py | 42 ++++++------ resources/lib/playback_starter.py | 2 +- resources/lib/playbackutils.py | 104 ++++++------------------------ resources/lib/player.py | 24 +++---- resources/lib/playlist_func.py | 2 +- resources/lib/playqueue.py | 3 +- resources/lib/playutils.py | 6 +- 10 files changed, 135 insertions(+), 130 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 3337a2a9..71ca8bef 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -50,7 +50,8 @@ 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, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW + REMAP_TYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE, PLEX_TYPE_SHOW, \ + PLEX_TYPE_EPISODE import plexdb_functions as plexdb ############################################################################### @@ -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 @@ -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/entrypoint.py b/resources/lib/entrypoint.py index 251ae0b5..2e6a934e 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -1035,14 +1035,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"), @@ -1099,7 +1099,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: @@ -1252,7 +1252,7 @@ def watchlater(): 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') diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index d02d2fb6..db83ea1b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -32,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() diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4aeafa13..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 plexdb_functions as plexdb -import playbackutils as pbutils 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,14 +21,14 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -class KodiMonitor(xbmc.Monitor): +class KodiMonitor(Monitor): def __init__(self, callback): self.mgr = callback self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.xbmcplayer = xbmc.Player() + self.xbmcplayer = Player() self.playqueue = self.mgr.playqueue - xbmc.Monitor.__init__(self) + Monitor.__init__(self) log.info("Kodi monitor started.") def onScanStarted(self, library): @@ -71,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": @@ -102,9 +101,9 @@ class KodiMonitor(xbmc.Monitor): "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: @@ -122,13 +121,13 @@ 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") def PlayBackStart(self, data): @@ -142,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: @@ -166,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 @@ -210,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/playback_starter.py b/resources/lib/playback_starter.py index c4b78b3f..cf910c63 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -45,7 +45,7 @@ class Playback_Starter(Thread): api = API(xml[0]) listitem = api.CreateListItemFromPlexItem(listitem) api.AddStreamInfo(listitem) - listitem = PlaybackUtils(xml[0], self.mgr).setArtwork(listitem) + api.set_listitem_artwork(listitem) result.listitem = listitem else: # Video and Music diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 65043071..5eba8272 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -68,10 +68,10 @@ class PlaybackUtils(): # 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) + 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].attrib.get('key')) try: @@ -81,16 +81,16 @@ class PlaybackUtils(): % 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) + api.set_playback_win_props(playurl, listitem) result.listitem = listitem return result @@ -197,16 +197,16 @@ class PlaybackUtils(): # 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") api.CreateListItemFromPlexItem(listitem) - self.setProperties(playurl, listitem) - self.setArtwork(listitem) + api.set_playback_win_props(playurl, listitem) + api.set_listitem_artwork(listitem) kodiPl.add(playurl, listitem, index=self.currentPosition+1) else: # Full metadata @@ -236,8 +236,9 @@ class PlaybackUtils(): log.debug("Adding additional part: %s, url: %s" % (counter, additionalPlayurl)) api.CreateListItemFromPlexItem(additionalListItem) - self.setProperties(additionalPlayurl, additionalListItem) - self.setArtwork(additionalListItem) + api.set_playback_win_props(additionalPlayurl, + additionalListItem) + api.set_listitem_artwork(additionalListItem) add_listitem_to_playlist( playqueue, self.currentPosition, @@ -266,15 +267,16 @@ class PlaybackUtils(): # 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 ################ if (homeScreen and seektime and window('plex_customplaylist') != "true" @@ -325,7 +327,7 @@ class PlaybackUtils(): params['id'] = introAPI.getRatingKey() params['filename'] = introAPI.getKey() introPlayurl = path + '?' + urlencode(params) - self.setArtwork(listitem, introAPI) + introAPI.set_listitem_artwork(listitem) # Overwrite the Plex url listitem.setPath(introPlayurl) log.info("Adding Intro: %s" % introPlayurl) @@ -337,67 +339,3 @@ class PlaybackUtils(): intro) 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, api=None): - if api is None: - api = self.api - allartwork = 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}) diff --git a/resources/lib/player.py b/resources/lib/player.py index 6c79d76c..14e4a3af 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -77,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 @@ -89,7 +89,7 @@ class Player(xbmc.Player): log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) - plexitem = "emby_%s" % currentFile + plexitem = "plex_%s" % currentFile runtime = window("%s.runtime" % plexitem) refresh_id = window("%s.refreshid" % plexitem) playMethod = window("%s.playmethod" % plexitem) @@ -98,7 +98,7 @@ class Player(xbmc.Player): 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) @@ -339,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: @@ -392,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_func.py b/resources/lib/playlist_func.py index 30479f74..ee2794f5 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -212,7 +212,7 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): try: xml.attrib['%sVersion' % playlist.kind] except: - log.error('Could not download Plex playlist.') + log.error('Could not process Plex playlist') return # Clear our existing playlist and the associated Kodi playlist playlist.clear() diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index c58f127a..096c2c96 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -85,7 +85,8 @@ class Playqueue(Thread): with lock: if playqueue_id != playqueue.ID: log.debug('Need to fetch new playQueue from the PMS') - PL.update_playlist_from_PMS(playqueue, playqueue_id) + xml = PL.get_PMS_playlist(playqueue, playqueue_id) + PL.update_playlist_from_PMS(playqueue, playqueue_id, xml=xml) else: log.debug('Restarting existing playQueue') PL.refresh_playlist_from_PMS(playqueue) 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 From 6df9bcd6b7c7f8547856669165f9cd9de4f8e355 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:36:36 +0100 Subject: [PATCH 37/50] Remove obsolete function --- resources/lib/entrypoint.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 2e6a934e..6d9e60fe 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -96,30 +96,6 @@ 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 Plex_Node(url, viewOffset, plex_type, playdirectly=False): """ Called only for a SINGLE element for Plex.tv watch later From dd40192a5136e0c04c544dc0336d3a2f89ff179c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:40:05 +0100 Subject: [PATCH 38/50] Abort if no xml received from PMS --- resources/lib/playback_starter.py | 5 +++++ resources/lib/playbackutils.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index cf910c63..bbac8231 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -38,6 +38,11 @@ class Playback_Starter(Thread): # 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 if xml[0].attrib.get('type') == PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 5eba8272..fd00d50c 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -34,9 +34,8 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item, callback=None, playlist_type=None): - self.item = item - self.api = API(item) + def __init__(self, xml, callback=None, playlist_type=None): + self.xml = xml playlist_type = playlist_type if playlist_type else \ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] if callback: From 20cf0cece5a03a7beaa094b74866305849abcab1 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:40:19 +0100 Subject: [PATCH 39/50] Revert "Abort if no xml received from PMS" This reverts commit dd40192a5136e0c04c544dc0336d3a2f89ff179c. --- resources/lib/playback_starter.py | 5 ----- resources/lib/playbackutils.py | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index bbac8231..cf910c63 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -38,11 +38,6 @@ class Playback_Starter(Thread): # 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 if xml[0].attrib.get('type') == PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index fd00d50c..5eba8272 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -34,8 +34,9 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, xml, callback=None, playlist_type=None): - self.xml = xml + def __init__(self, item, callback=None, playlist_type=None): + self.item = item + self.api = API(item) playlist_type = playlist_type if playlist_type else \ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] if callback: From 18cdfa192f592f69f6687bd1c000adeabe4d837c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:40:47 +0100 Subject: [PATCH 40/50] Abort if no xml received from PMS --- resources/lib/playback_starter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index cf910c63..bbac8231 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -38,6 +38,11 @@ class Playback_Starter(Thread): # 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 if xml[0].attrib.get('type') == PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() From 5230f736562686fe3a7df6a33462ef106236ff64 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 8 Jan 2017 15:43:30 +0100 Subject: [PATCH 41/50] Code optimization --- resources/lib/playback_starter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index bbac8231..0b97724c 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -43,11 +43,11 @@ class Playback_Starter(Thread): except (TypeError, AttributeError): log.error('Could not get a PMS xml for plex id %s' % plex_id) return - if xml[0].attrib.get('type') == PLEX_TYPE_PHOTO: + api = API(xml[0]) + if api.getType() == PLEX_TYPE_PHOTO: # Photo result = Playback_Successful() listitem = PKC_ListItem() - api = API(xml[0]) listitem = api.CreateListItemFromPlexItem(listitem) api.AddStreamInfo(listitem) api.set_listitem_artwork(listitem) From aac892fed80c2e9f3c9b4acad10f837eec58823e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 19:56:57 +0100 Subject: [PATCH 42/50] Rewire Plex Companion startup --- resources/lib/entrypoint.py | 97 +----------------- resources/lib/playback_starter.py | 7 +- resources/lib/playbackutils.py | 160 +++++++++++++++++------------- resources/lib/playqueue.py | 32 +++--- 4 files changed, 116 insertions(+), 180 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 6d9e60fe..eca0ac3c 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -23,6 +23,7 @@ import playbackutils as pbutils import PlexFunctions import PlexAPI from PKC_listitem import convert_PKC_to_listitem +from playqueue import Playqueue ############################################################################### @@ -121,7 +122,8 @@ def Plex_Node(url, viewOffset, plex_type, playdirectly=False): window('plex_customplaylist.seektime', value=str(viewOffset)) log.info('Set resume point to %s' % str(viewOffset)) typus = PlexFunctions.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type] - result = pbutils.PlaybackUtils(xml[0],playlist_type=typus).play( + playqueue = Playqueue().get_playqueue_from_type(typus) + result = pbutils.PlaybackUtils(xml, playqueue).play( None, kodi_id='plexnode', plex_lib_UUID=xml.attrib.get('librarySectionUUID')) @@ -383,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.setArt({'icon': '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.setArt({'icon': '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): diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 0b97724c..e22feba7 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -8,7 +8,8 @@ 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 +from PlexFunctions import GetPlexMetadata, PLEX_TYPE_PHOTO, \ + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE from PlexAPI import API from playqueue import lock @@ -54,8 +55,10 @@ class Playback_Starter(Thread): 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[0], self.mgr).play( + result = PlaybackUtils(xml, playqueue).play( plex_id, kodi_id, xml.attrib.get('librarySectionUUID')) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 5eba8272..d7195ad2 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -14,14 +14,14 @@ from utils import window, settings, tryEncode, tryDecode import downloadutils from PlexAPI import API -from PlexFunctions import GetPlexPlaylist, KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, \ - KODITYPE_FROM_PLEXTYPE, PLEX_TYPE_MOVIE +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 playqueue import lock, Playqueue from pickler import Playback_Successful +from plexdb_functions import Get_Plex_DB ############################################################################### @@ -34,17 +34,9 @@ addonName = "PlexKodiConnect" class PlaybackUtils(): - def __init__(self, item, callback=None, playlist_type=None): - self.item = item - self.api = API(item) - playlist_type = playlist_type if playlist_type else \ - KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.api.getType()] - if callback: - self.mgr = callback - self.playqueue = self.mgr.playqueue.get_playqueue_from_type( - playlist_type) - else: - self.playqueue = Playqueue().get_playqueue_from_type(playlist_type) + def __init__(self, xml, playqueue): + self.xml = xml + self.playqueue = playqueue def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): """ @@ -52,8 +44,8 @@ class PlaybackUtils(): to the PMS """ log.info("Playbackutils called") - item = self.item - api = self.api + item = self.xml[0] + api = API(item) playqueue = self.playqueue xml = None result = Playback_Successful() @@ -179,7 +171,12 @@ class PlaybackUtils(): # -- ADD TRAILERS ################ if trailers: - introsPlaylist = self.AddTrailers(xml) + 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: @@ -223,40 +220,14 @@ class PlaybackUtils(): # -- CHECK FOR ADDITIONAL PARTS ################ if len(item[0]) > 1: - # 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() - 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( - playqueue, - self.currentPosition, - additionalListItem, - kodi_id=kodi_id, - kodi_type=kodi_type, - plex_id=plex_id, - file=additionalPlayurl) - self.currentPosition += 1 - api.setPartNumber(0) + 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.") # Delete the item that's gonna fail! - with lock: - del playqueue.items[startPos] + del playqueue.items[startPos] # Don't attach listitem return result @@ -304,38 +275,89 @@ class PlaybackUtils(): 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 getting trailers, e.g. when no plex pass - if xml.attrib.get('size') == '1': - return False + log.info("Playbackutils play_all called") + window('plex_playbackProps', value="true") + self.currentPosition = 0 + for item in self.xml: + log.debug('item.attrib: %s' % item.attrib) + 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/" 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 - introAPI = API(intro) - 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 Intro: %s" % introPlayurl) - add_listitem_to_Kodi_playlist( + 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 + + 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, - listitem, - introPlayurl, - intro) + additionalListItem, + kodi_id=kodi_id, + kodi_type=kodi_type, + plex_id=api.getRatingKey(), + file=additionalPlayurl) self.currentPosition += 1 - return True + api.setPartNumber(0) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 096c2c96..3b79023e 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -8,6 +8,7 @@ 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__) @@ -86,7 +87,12 @@ class Playqueue(Thread): if playqueue_id != playqueue.ID: log.debug('Need to fetch new playQueue from the PMS') xml = PL.get_PMS_playlist(playqueue, playqueue_id) - PL.update_playlist_from_PMS(playqueue, playqueue_id, xml=xml) + 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() else: log.debug('Restarting existing playQueue') PL.refresh_playlist_from_PMS(playqueue) @@ -99,21 +105,17 @@ class Playqueue(Thread): if item.ID == playqueue.selectedItemID: break else: - startpos = None + startpos = 0 # Start playback. Player does not return in time - if startpos: - log.debug('Start position Plex Companion playback: %s' - % startpos) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, - None, - False, - startpos)) - else: - log.debug('Start Plex Companion playback from beginning') - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl,)) - log.debug('Playqueues are: %s' % self.playqueues) + log.debug('Playqueues after Plex Companion update are now: %s' + % self.playqueues) + log.debug('Start position Plex Companion playback: %s' + % startpos) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, + None, + False, + startpos)) thread.setDaemon(True) thread.start() From 4b1c2e944727be2302e3f7661155b925a596ccd5 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 19:57:23 +0100 Subject: [PATCH 43/50] Less logging --- resources/lib/playbackutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index d7195ad2..4a17cde1 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -283,7 +283,6 @@ class PlaybackUtils(): window('plex_playbackProps', value="true") self.currentPosition = 0 for item in self.xml: - log.debug('item.attrib: %s' % item.attrib) api = API(item) if api.getType() == PLEX_TYPE_CLIP: self.add_trailer(item) From a45af3d25363037675cfdd50c6b1d85b03e57242 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:00:37 +0100 Subject: [PATCH 44/50] Always re-fetch playQueues from PMS --- resources/lib/playqueue.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 3b79023e..2e1085b6 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -84,18 +84,13 @@ class Playqueue(Thread): log.info('New playqueue %s received from Plex companion with offset ' '%s, repeat %s' % (playqueue_id, offset, repeat)) with lock: - if playqueue_id != playqueue.ID: - log.debug('Need to fetch new playQueue from the PMS') - 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() - else: - log.debug('Restarting existing playQueue') - PL.refresh_playlist_from_PMS(playqueue) + 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"): @@ -109,8 +104,6 @@ class Playqueue(Thread): # Start playback. Player does not return in time log.debug('Playqueues after Plex Companion update are now: %s' % self.playqueues) - log.debug('Start position Plex Companion playback: %s' - % startpos) thread = Thread(target=Player().play, args=(playqueue.kodi_pl, None, From 8acf07c607068c352f87c8953885f05422943ac6 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:33:52 +0100 Subject: [PATCH 45/50] Krypton: add ratings and IMDB id for movies --- resources/lib/itemtypes.py | 32 ++++++++++++++++ resources/lib/kodidb_functions.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 40cf7e70..3935afb7 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -268,6 +268,22 @@ class Movies(Items): # Update the movie entry if KODIVERSION > 16: + # 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 = ?," @@ -299,6 +315,22 @@ class Movies(Items): else: log.info("ADD movie itemid: %s - Title: %s" % (itemid, title)) if KODIVERSION > 16: + # 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, diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index db83ea1b..e1c7ff6b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1399,6 +1399,69 @@ 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): """ From 087a6ff465b07a7474482e993fb0744124c2ec23 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:37:24 +0100 Subject: [PATCH 46/50] Ensure Jarvis compatibility --- resources/lib/itemtypes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 3935afb7..523d84d6 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -267,7 +267,7 @@ 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, @@ -314,7 +314,7 @@ 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, @@ -848,7 +848,7 @@ class TVShows(Items): log.info("UPDATE episode itemid: %s" % (itemid)) # Update the movie entry - if KODIVERSION in (16, 17): + if KODIVERSION in >= 16: # Kodi Jarvis, Krypton query = ' '.join(( "UPDATE episode", @@ -881,7 +881,7 @@ class TVShows(Items): 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 = ( ''' @@ -1199,7 +1199,7 @@ class Music(Items): checksum=checksum) # Process the artist - if KODIVERSION in (16, 17): + if KODIVERSION >= 16: query = ' '.join(( "UPDATE artist", @@ -1295,7 +1295,7 @@ class Music(Items): checksum=checksum) # Process the album info - if KODIVERSION == 17: + if KODIVERSION >= 17: # Kodi Krypton query = ' '.join(( @@ -1568,7 +1568,7 @@ class Music(Items): 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 = ( ''' @@ -1733,7 +1733,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)) From 6ab2ab4b262042d137b3632600b611789a03e3e9 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:39:41 +0100 Subject: [PATCH 47/50] Fix SyntaxError --- resources/lib/itemtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 523d84d6..cbc91413 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -848,7 +848,7 @@ class TVShows(Items): log.info("UPDATE episode itemid: %s" % (itemid)) # Update the movie entry - if KODIVERSION in >= 16: + if KODIVERSION >= 16: # Kodi Jarvis, Krypton query = ' '.join(( "UPDATE episode", From 5c836f7911c13aae3ff28f884d056d2e07fc737e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:47:32 +0100 Subject: [PATCH 48/50] Krypton: add ratings and IMDB id for TV shows --- resources/lib/itemtypes.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index cbc91413..5fbbd6ca 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -555,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(( @@ -580,7 +595,22 @@ class TVShows(Items): ##### 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", From 0b304f563c7f2b2f337f13f8b28d3b1fd27f8b19 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 20:54:48 +0100 Subject: [PATCH 49/50] Version bump --- addon.xml | 2 +- changelog.txt | 11 +++++++++++ service.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 192c6930..c7fdb0ca 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ 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/service.py b/service.py index fc6053cd..de3ca487 100644 --- a/service.py +++ b/service.py @@ -122,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: From 03438b1c7611ba52621f10f001215702daf6c442 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 9 Jan 2017 21:25:52 +0100 Subject: [PATCH 50/50] Fix Plex Companion showing last item being played --- resources/lib/playqueue.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 2e1085b6..cf87218e 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -141,7 +141,8 @@ class Playqueue(Thread): del old[j], index[j] break else: - log.debug('Detected new Kodi element: %s' % new_item) + 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) @@ -149,6 +150,9 @@ class Playqueue(Thread): 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)