From ad8b7c7d9061227fdcf7ff7bb1d1c3da320ad35c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 20 Dec 2016 16:13:19 +0100 Subject: [PATCH] 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