From 37c5215c116fd89462d4be544e09a5bbbe60dbe8 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 25 Jul 2017 21:50:47 +0200 Subject: [PATCH 001/509] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbae4777..331b0a29 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.6-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.6-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) From 5a978bd98a65a59aa40a48fc0e9b7b4f2038280e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 2 Aug 2017 20:06:18 +0200 Subject: [PATCH 002/509] Update German --- resources/language/resource.language.de_DE/strings.po | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/language/resource.language.de_DE/strings.po b/resources/language/resource.language.de_DE/strings.po index 82c207ff..356af051 100644 --- a/resources/language/resource.language.de_DE/strings.po +++ b/resources/language/resource.language.de_DE/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Dieser Plex Media Server gehört mir" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Informationen" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Anzahl anzuzeigender zuletzt hinzugefügter Alben:" From 630b848f6e0960911f9dd8d04e42e7beb79844e3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 2 Aug 2017 20:09:02 +0200 Subject: [PATCH 003/509] Version bump --- README.md | 2 +- addon.xml | 8 +++++++- changelog.txt | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbae4777..a5d41149 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.7-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 04d6d22e..9499dd5f 100644 --- a/addon.xml +++ b/addon.xml @@ -59,7 +59,13 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.6: + version 1.8.7: +- Some fixes to playstate reporting, thanks @RickDB +- Add Kodi info screen for episodes in context menu +- Fix PKC asking for trailers not working +- Fix PKC not automatically updating + +version 1.8.6: - Portuguese translation, thanks @goncalo532 - Updated other translations diff --git a/changelog.txt b/changelog.txt index d59f84f0..6213c72e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +version 1.8.7: +- Some fixes to playstate reporting, thanks @RickDB +- Add Kodi info screen for episodes in context menu +- Fix PKC asking for trailers not working +- Fix PKC not automatically updating + version 1.8.6: - Portuguese translation, thanks @goncalo532 - Updated other translations From f9037dcbd836d23f38f26846904ed666ef246adb Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 8 Aug 2017 20:27:37 +0200 Subject: [PATCH 004/509] Fix playback not starting in some cirrcumstances - Should fix #330 --- resources/lib/PlexAPI.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 65db0967..b58ff222 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2395,9 +2395,11 @@ class API(): log.error('Could not temporarily download subtitle %s' % url) return else: + log.debug('Writing temp subtitle to %s' % path) r.encoding = 'utf-8' with open(path, 'wb') as f: - f.write(r.content) + # r.content does not always seem to be encoded! + f.write(tryEncode(r.content)) return path def GetKodiPremierDate(self): From 83b18faac17c60a0c4334a97cfa5a8c4a0ed847f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 8 Aug 2017 20:44:36 +0200 Subject: [PATCH 005/509] Fix first artist [missing tag] (Reset your DB!) - Thanks @angelblue05 - Fixes #308 --- resources/lib/kodidb_functions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 503dfcc2..5f289dac 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1280,7 +1280,14 @@ class Kodidb_Functions(): try: artistid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idArtist),0) from artist") + # Krypton has a dummy first entry idArtist: 1 strArtist: + # [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing + if v.KODIVERSION >= 17: + self.cursor.execute( + "select coalesce(max(idArtist),1) from artist") + else: + self.cursor.execute( + "select coalesce(max(idArtist),0) from artist") artistid = self.cursor.fetchone()[0] + 1 query = ( ''' From 5f79214148a89c9b1a209d196adc472d3d86a9aa Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 8 Aug 2017 21:05:31 +0200 Subject: [PATCH 006/509] Update Czech translation --- .../language/resource.language.cs_CZ/strings.po | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/resources/language/resource.language.cs_CZ/strings.po b/resources/language/resource.language.cs_CZ/strings.po index 782a4887..974cfa33 100644 --- a/resources/language/resource.language.cs_CZ/strings.po +++ b/resources/language/resource.language.cs_CZ/strings.po @@ -105,6 +105,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Tento Plex Media Server je můj" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Informace" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Zobrazit počet naposledy přidaných hudebních alb:" @@ -1808,18 +1813,18 @@ msgstr "Pouze chybějící" # Message in the PKC settings if user has not logged in to plex.tv msgctxt "#39226" msgid "Not logged in to plex.tv" -msgstr "" +msgstr "Nepřihlášeno k plex.tv" # Message in the PKC settings if user is logged in to plex.tv msgctxt "#39227" msgid "Logged in to plex.tv" -msgstr "" +msgstr "Přihlášeno k plex.tv" # Message in the PKC settings to display the plex.tv username. Leave the colon # : msgctxt "#39228" msgid "Plex user:" -msgstr "" +msgstr "Uživatel plexu:" # Plex Artwork.py msgctxt "#39250" @@ -2103,6 +2108,7 @@ msgstr "" msgctxt "#39717" msgid "PKC uses free additional artwork from www.themoviedb.org. Many thanks!" msgstr "" +"PKC používá dodatečné obrázky z www.themoviedb.org. Děkujeme mnohokrát!" # Shown during very first PKC setup only msgctxt "#39718" @@ -2110,8 +2116,10 @@ msgid "" "Do you want to replace your custom user ratings with an indicator of how " "many versions of a media item you posses?" msgstr "" +"Chcete nahradit uživatelské hodnocení indikátorem počtu verzí médií které " +"jsou k dispozici?" # In PKC Settings under Sync msgctxt "#39719" msgid "Replace user ratings with number of media versions" -msgstr "" +msgstr "Nahradit uživatelské hodnocení počtem verzí média" From 368c9024583fc8fdffb9b403356ec8fb45aff179 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 8 Aug 2017 21:30:07 +0200 Subject: [PATCH 007/509] Version bump --- README.md | 4 ++-- addon.xml | 9 +++++++-- changelog.txt | 7 ++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a5d41149..1b98a04d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.7-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.8-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.8-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 9499dd5f..0f6064ec 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,12 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.7: + version 1.8.8 +- Fix playback not starting in some circumstances +- Fix first artist "missing" tag (Reset your DB!) +- Update Czech translation + +version 1.8.7 (beta only): - Some fixes to playstate reporting, thanks @RickDB - Add Kodi info screen for episodes in context menu - Fix PKC asking for trailers not working diff --git a/changelog.txt b/changelog.txt index 6213c72e..e3c5f2df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ -version 1.8.7: +version 1.8.8 +- Fix playback not starting in some circumstances +- Fix first artist "missing" tag (Reset your DB!) +- Update Czech translation + +version 1.8.7 (beta only): - Some fixes to playstate reporting, thanks @RickDB - Add Kodi info screen for episodes in context menu - Fix PKC asking for trailers not working From 3d58b931071ad12d858646da1914e60874e27649 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 10 Aug 2017 19:34:23 +0200 Subject: [PATCH 008/509] Revert "Fix playback not starting in some cirrcumstances" This reverts commit f9037dcbd836d23f38f26846904ed666ef246adb. --- resources/lib/PlexAPI.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b58ff222..65db0967 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2395,11 +2395,9 @@ class API(): log.error('Could not temporarily download subtitle %s' % url) return else: - log.debug('Writing temp subtitle to %s' % path) r.encoding = 'utf-8' with open(path, 'wb') as f: - # r.content does not always seem to be encoded! - f.write(tryEncode(r.content)) + f.write(r.content) return path def GetKodiPremierDate(self): From 73d6bfde890fc3033bcf5dad28f5f2d44adb82b7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 10 Aug 2017 21:05:46 +0200 Subject: [PATCH 009/509] Fix playback not starting in some circumstances - Fixes #330 --- resources/lib/PlexAPI.py | 13 +++++++++---- resources/lib/utils.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 65db0967..f03b0d80 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -49,7 +49,7 @@ from xbmcvfs import exists import clientinfo as client from downloadutils import DownloadUtils from utils import window, settings, language as lang, tryDecode, tryEncode, \ - DateToKodi, exists_dir + DateToKodi, exists_dir, slugify from PlexFunctions import PMSHttpsEnabled import plexdb_functions as plexdb import variables as v @@ -2395,9 +2395,14 @@ class API(): log.error('Could not temporarily download subtitle %s' % url) return else: - r.encoding = 'utf-8' - with open(path, 'wb') as f: - f.write(r.content) + log.debug('Writing temp subtitle to %s' % path) + try: + with open(path, 'wb') as f: + f.write(r.content) + except UnicodeEncodeError: + log.debug('Need to slugify the filename %s' % path) + with open(slugify(path), 'wb') as f: + f.write(r.content) return path def GetKodiPremierDate(self): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 27354384..ee1631d1 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -221,6 +221,16 @@ def tryDecode(string, encoding='utf-8'): return string +def slugify(text): + """ + Normalizes text (in unicode or string) to e.g. enable safe filenames. + Returns unicode + """ + if not isinstance(text, unicode): + text = unicode(text) + return unicode(normalize('NFKD', text).encode('ascii', 'ignore')) + + def escape_html(string): """ Escapes the following: From 9e275b23d4cfcc105f324394a86f0d1d4f0d3477 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 10 Aug 2017 21:08:37 +0200 Subject: [PATCH 010/509] Deactivate some annoying popups on install --- resources/lib/initialsetup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index f29afa72..730517e9 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -496,10 +496,10 @@ class InitialSetup(): # 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=lang(29999), line1=lang(39076)) + # dialog.ok(heading=lang(29999), line1=lang(39076)) # Need to tell about our image source for collections: themoviedb.org - dialog.ok(heading=lang(29999), line1=lang(39717)) + # dialog.ok(heading=lang(29999), line1=lang(39717)) # Make sure that we only ask these questions upon first installation settings('InstallQuestionsAnswered', value='true') From e4ca63b42c81b89826bc6c971404205b018b93ea Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 10 Aug 2017 21:10:19 +0200 Subject: [PATCH 011/509] Version bump --- README.md | 4 ++-- addon.xml | 8 ++++++-- changelog.txt | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1b98a04d..7a88e755 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.8-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.8-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.9-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.9-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 0f6064ec..83707bb9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,11 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.8 + version 1.8.9 +- Fix playback not starting in some circumstances +- Deactivate some annoying popups on install + +version 1.8.8 - Fix playback not starting in some circumstances - Fix first artist "missing" tag (Reset your DB!) - Update Czech translation diff --git a/changelog.txt b/changelog.txt index e3c5f2df..65d57151 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +version 1.8.9 +- Fix playback not starting in some circumstances +- Deactivate some annoying popups on install + version 1.8.8 - Fix playback not starting in some circumstances - Fix first artist "missing" tag (Reset your DB!) From 8af180968b41f0600a440cceb5967b1ce6d7e4e2 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 11 Aug 2017 12:21:44 +0200 Subject: [PATCH 012/509] More descriptive downloadable subtitles --- resources/lib/PlexAPI.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f03b0d80..8ab094b5 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2357,11 +2357,11 @@ class API(): # ext = stream.attrib.get('format') if key: # We do know the language - temporarily download - if stream.attrib.get('languageCode') is not None: + if stream.attrib.get('language') is not None: path = self.download_external_subtitles( "{server}%s" % key, "subtitle%02d.%s.%s" % (fileindex, - stream.attrib['languageCode'], + stream.attrib['language'], stream.attrib['codec'])) fileindex += 1 # We don't know the language - no need to download From b103309ceb9da398a8432487d0d3202126937400 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 18 Aug 2017 09:53:10 +0200 Subject: [PATCH 013/509] Library sync dialog code optimization --- resources/lib/librarysync.py | 50 +++++++++++++++++------------------- resources/lib/utils.py | 9 +++++++ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 40078e45..29a891e0 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -63,7 +63,6 @@ class LibrarySync(Thread): self.user = userclient.UserClient() self.vnodes = videonodes.VideoNodes() - self.dialog = xbmcgui.Dialog() self.syncThreadNumber = int(settings('syncThreadNumber')) self.installSyncDone = settings('SyncInstallRunDone') == 'true' @@ -98,19 +97,16 @@ class LibrarySync(Thread): if not forced: return if icon == "plex": - self.dialog.notification( - lang(29999), - message, - "special://home/addons/plugin.video.plexkodiconnect/icon.png", - 5000, - False) + dialog('notification', + heading='{plex}', + message=message, + icon='{plex}', + sound=False) elif icon == "error": - self.dialog.notification( - lang(29999), - message, - xbmcgui.NOTIFICATION_ERROR, - 7000, - True) + dialog('notification', + heading='{plex}', + message=message, + type='{error}') def syncPMStime(self): """ @@ -317,13 +313,13 @@ class LibrarySync(Thread): setScreensaver(value=screensaver) if window('plex_scancrashed') == 'true': # Show warning if itemtypes.py crashed at some point - self.dialog.ok(lang(29999), lang(39408)) + dialog('ok', heading='{plex}', line1=lang(39408)) window('plex_scancrashed', clear=True) elif window('plex_scancrashed') == '401': window('plex_scancrashed', clear=True) if state.PMS_STATUS not in ('401', 'Auth'): # Plex server had too much and returned ERROR - self.dialog.ok(lang(29999), lang(39409)) + dialog('ok', heading='{plex}', line1=lang(39409)) # Path hack, so Kodis Information screen works with kodidb.GetKodiDB('video') as kodi_db: @@ -477,7 +473,7 @@ class LibrarySync(Thread): log.info('Detected new Music library - restarting now') # 'New Plex music library detected. Sorry, but we need to # restart Kodi now due to the changes made.' - dialog('ok', lang(29999), lang(39711)) + dialog('ok', heading='{plex}', line1=lang(39711)) from xbmc import executebuiltin executebuiltin('RestartApp') return False @@ -1404,7 +1400,7 @@ class LibrarySync(Thread): import traceback log.error("Traceback:\n%s" % traceback.format_exc()) # Library sync thread has crashed - self.dialog.ok(lang(29999), lang(39400)) + dialog('ok', heading='{plex}', line1=lang(39400)) raise def run_internal(self): @@ -1459,13 +1455,15 @@ class LibrarySync(Thread): log.warn("Db version out of date: %s minimum version " "required: %s" % (currentVersion, minVersion)) # DB out of date. Proceed to recreate? - resp = self.dialog.yesno(heading=lang(29999), - line1=lang(39401)) + resp = dialog('yesno', + heading=lang(29999), + line1=lang(39401)) if not resp: log.warn("Db version out of date! USER IGNORED!") # PKC may not work correctly until reset - self.dialog.ok(heading=lang(29999), - line1=(lang(29999) + lang(39402))) + dialog('ok', + heading='{plex}', + line1=lang(29999) + lang(39402)) else: reset() break @@ -1483,7 +1481,7 @@ class LibrarySync(Thread): log.error('Current Kodi version: %s' % tryDecode( xbmc.getInfoLabel('System.BuildVersion'))) # "Current Kodi version is unsupported, cancel lib sync" - self.dialog.ok(heading=lang(29999), line1=lang(39403)) + dialog('ok', heading='{plex}', line1=lang(39403)) break # Run start up sync state.DB_SCAN = True @@ -1525,8 +1523,7 @@ class LibrarySync(Thread): log.error("Startup full sync failed. Stopping sync") # "Startup syncing process failed repeatedly" # "Please restart" - self.dialog.ok(heading=lang(29999), - line1=lang(39404)) + dialog('ok', heading='{plex}', line1=lang(39404)) break # Currently no db scan, so we can start a new scan @@ -1575,8 +1572,9 @@ class LibrarySync(Thread): window('plex_runLibScan', clear=True) # Only look for missing fanart (No) # or refresh all fanart (Yes) - self.fanartSync(refresh=self.dialog.yesno( - heading=lang(29999), + self.fanartSync(refresh=dialog( + 'yesno', + heading='{plex}', line1=lang(39223), nolabel=lang(39224), yeslabel=lang(39225))) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index ee1631d1..962fa79a 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -140,6 +140,15 @@ def dialog(typus, *args, **kwargs): Displays xbmcgui Dialog. Pass a string as typus: 'yesno', 'ok', 'notification', 'input', 'select', 'numeric' + kwargs: + heading='{plex}' title bar (here PlexKodiConnect) + message=lang(30128), Actual dialog content. Don't use with OK + line1=str(), For 'OK' and 'yesno' dialogs use line1...line3! + time=5000, + sound=True, + nolabel=str(), For 'yesno' dialogs + yeslabel=str(), For 'yesno' dialogs + Icons: icon='{plex}' Display Plex standard icon icon='{info}' xbmcgui.NOTIFICATION_INFO From bc36750d529ac4067d55745f962af9a5b8edb8b6 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 18 Aug 2017 10:37:30 +0200 Subject: [PATCH 014/509] Move dialog instance --- resources/lib/library_sync/sync_info.py | 10 +++++----- resources/lib/librarysync.py | 7 +------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index b2dd98d8..3380bf19 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -3,6 +3,7 @@ from logging import getLogger from threading import Thread, Lock from xbmc import sleep +from xbmcgui import DialogProgressBG from utils import thread_methods, language as lang @@ -24,12 +25,11 @@ class Threaded_Show_Sync_Info(Thread): Threaded class to show the Kodi statusbar of the metadata download. Input: - dialog xbmcgui.DialogProgressBG() object to show progress total: Total number of items to get + item_type: """ - def __init__(self, dialog, total, item_type): + def __init__(self, total, item_type): self.total = total - self.dialog = dialog self.item_type = item_type Thread.__init__(self) @@ -50,8 +50,8 @@ class Threaded_Show_Sync_Info(Thread): """ log.debug('Show sync info thread started') # cache local variables because it's faster - total = self.total - dialog = self.dialog + total = self.totaltal + dialog = DialogProgressBG('dialoglogProgressBG') thread_stopped = self.thread_stopped dialog.create("%s %s: %s %s" % (lang(39714), self.item_type, str(total), lang(39715))) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 29a891e0..389127bc 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -6,7 +6,6 @@ import Queue from random import shuffle import xbmc -import xbmcgui from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ @@ -736,11 +735,7 @@ class LibrarySync(Thread): threads.append(thread) # 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 = sync_info.Threaded_Show_Sync_Info( - dialog, - itemNumber, - itemType) + thread = sync_info.Threaded_Show_Sync_Info(itemNumber, itemType) thread.setDaemon(True) thread.start() threads.append(thread) From 1aee66a5652fbb29b8818d49d6304311061bce4a Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 18 Aug 2017 10:38:03 +0200 Subject: [PATCH 015/509] Clarify import --- resources/lib/librarysync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 389127bc..add0c36e 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from threading import Thread import Queue from random import shuffle @@ -32,7 +32,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### From b544ad93f3c0c783df1ef61c0b6bad2f55aa2ed4 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 18 Aug 2017 10:56:45 +0200 Subject: [PATCH 016/509] Never show library sync dialog if media is playing --- resources/lib/librarysync.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index add0c36e..f49257e1 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -62,6 +62,7 @@ class LibrarySync(Thread): self.user = userclient.UserClient() self.vnodes = videonodes.VideoNodes() + self.xbmcplayer = xbmc.Player() self.syncThreadNumber = int(settings('syncThreadNumber')) self.installSyncDone = settings('SyncInstallRunDone') == 'true' @@ -92,6 +93,9 @@ class LibrarySync(Thread): forced: always show popup, even if user setting to off """ + if self.xbmcplayer.isPlaying(): + # Don't show any dialog if media is playing + return if settings('dbSyncIndicator') != 'true': if not forced: return @@ -1413,8 +1417,6 @@ class LibrarySync(Thread): lastProcessing = 0 oneDay = 60*60*24 - xbmcplayer = xbmc.Player() - # Link to Websocket queue queue = self.mgr.ws.queue @@ -1584,7 +1586,7 @@ class LibrarySync(Thread): else: now = getUnixTimestamp() if (now - lastSync > fullSyncInterval and - not xbmcplayer.isPlaying()): + not self.xbmcplayer.isPlaying()): lastSync = now log.info('Doing scheduled full library scan') state.DB_SCAN = True From 334bbf418c31c2c0ef4f4b096688caec59f2a6b8 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 12:51:58 +0200 Subject: [PATCH 017/509] Fix typo --- resources/lib/library_sync/sync_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 3380bf19..86c21a75 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -50,7 +50,7 @@ class Threaded_Show_Sync_Info(Thread): """ log.debug('Show sync info thread started') # cache local variables because it's faster - total = self.totaltal + total = self.total dialog = DialogProgressBG('dialoglogProgressBG') thread_stopped = self.thread_stopped dialog.create("%s %s: %s %s" From d636271525484a4cd3180a3eca4d7320de1b333a Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 13:42:15 +0200 Subject: [PATCH 018/509] Don't show sync progress if media is playing --- resources/lib/library_sync/sync_info.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 86c21a75..37bbaa21 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -2,7 +2,7 @@ from logging import getLogger from threading import Thread, Lock -from xbmc import sleep +from xbmc import sleep, Player from xbmcgui import DialogProgressBG from utils import thread_methods, language as lang @@ -55,10 +55,11 @@ class Threaded_Show_Sync_Info(Thread): thread_stopped = self.thread_stopped dialog.create("%s %s: %s %s" % (lang(39714), self.item_type, str(total), lang(39715))) + player = Player() total = 2 * total totalProgress = 0 - while thread_stopped() is False: + while thread_stopped() is False and not player.isPlaying(): with LOCK: get_progress = GET_METADATA_COUNT process_progress = PROCESS_METADATA_COUNT From 12db99203f8f9ae5bea8f65292bdc116efc5037e Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 13:43:50 +0200 Subject: [PATCH 019/509] Improvements to sync dialog --- resources/lib/librarysync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index f49257e1..67301a11 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -96,7 +96,7 @@ class LibrarySync(Thread): if self.xbmcplayer.isPlaying(): # Don't show any dialog if media is playing return - if settings('dbSyncIndicator') != 'true': + if window('dbSyncIndicator') != 'true': if not forced: return if icon == "plex": @@ -1538,7 +1538,7 @@ class LibrarySync(Thread): window('plex_dbScan', clear=True) state.DB_SCAN = False # Full library sync finished - self.showKodiNote(lang(39407), forced=False) + self.showKodiNote(lang(39407), forced=True) # Reset views was requested from somewhere else elif window('plex_runLibScan') == "views": log.info('Refresh playlist and nodes requested, starting') From 86b4f02e09bc946e3510665aaac028f4fb385b37 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 13:48:56 +0200 Subject: [PATCH 020/509] Remove obsolete import --- service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service.py b/service.py index 601a2a59..c17fb082 100644 --- a/service.py +++ b/service.py @@ -30,8 +30,7 @@ sys_path.append(_base_resource) ############################################################################### -from utils import settings, window, language as lang, dialog, tryEncode, \ - tryDecode +from utils import settings, window, language as lang, dialog, tryDecode from userclient import UserClient import initialsetup from kodimonitor import KodiMonitor From cda68d14b46ec4f58406236a8bfcd6c57a56d079 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 14:39:00 +0200 Subject: [PATCH 021/509] Fix stop synching if path not found - Fixes #333 --- resources/lib/library_sync/get_metadata.py | 2 +- resources/lib/library_sync/process_metadata.py | 2 +- resources/lib/library_sync/sync_info.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index ed3e187e..2100f1f0 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -16,7 +16,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index c4c599a4..cdefa952 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -15,7 +15,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) class Threaded_Process_Metadata(Thread): """ Not yet implemented for more than 1 thread - if ever. Only to be called by diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 37bbaa21..494b499a 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -19,7 +19,7 @@ LOCK = Lock() ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. From 27d356e3c5c429e173da98fc0d92b0aa12c4052b Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 14:50:28 +0200 Subject: [PATCH 022/509] Don't quit sync threads if path wasn't found - Partially fixes #333 --- resources/lib/artwork.py | 5 +++-- resources/lib/library_sync/fanart.py | 5 +++-- resources/lib/librarysync.py | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 1a310921..ce2edc34 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -126,8 +126,9 @@ def double_urldecode(text): return unquote(unquote(text)) -@thread_methods(add_stops=['STOP_SYNC'], - add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN']) +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', + 'DB_SCAN', + 'STOP_SYNC']) class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 1fdcb4e7..620f341d 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -17,8 +17,9 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'], - add_stops=['STOP_SYNC']) +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', + 'DB_SCAN', + 'STOP_SYNC']) class Process_Fanart_Thread(Thread): """ Threaded download of additional fanart in the background diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 67301a11..62062b03 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -37,8 +37,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@thread_methods(add_stops=['STOP_SYNC'], - add_suspends=['SUSPEND_LIBRARY_THREAD']) +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) class LibrarySync(Thread): """ """ From a41e6ce8214411f4c7cb7496d9a9a3389bf53a68 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 15:02:23 +0200 Subject: [PATCH 023/509] Resume aborted sync on PKC settings change --- resources/lib/kodimonitor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index fde49a13..457077ab 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,6 +47,9 @@ class KodiMonitor(Monitor): """ Monitor the PKC settings for changes made by the user """ + # Assume that the user changed the settings so that we can now find the + # path to all media files + state.STOP_SYNC = False # settings: window-variable items = { 'logLevel': 'plex_logLevel', From d5c92f89d967debc54ee49dbef5185cc4109a251 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 15:03:19 +0200 Subject: [PATCH 024/509] Move path-checked flag to state.py --- resources/lib/PlexAPI.py | 8 ++++---- resources/lib/kodimonitor.py | 1 + resources/lib/state.py | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 8ab094b5..83098360 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2589,7 +2589,7 @@ class API(): elif window('replaceSMB') == 'true': if path.startswith('\\\\'): path = 'smb:' + path.replace('\\', '/') - if ((window('plex_pathverified') == 'true' and forceCheck is False) or + if ((state.PATH_VERIFIED and forceCheck is False) or omitCheck is True): return path @@ -2617,12 +2617,12 @@ class API(): if self.askToValidate(path): state.STOP_SYNC = True path = None - window('plex_pathverified', value='true') + state.PATH_VERIFIED = True else: path = None elif forceCheck is False: - if window('plex_pathverified') != 'true': - window('plex_pathverified', value='true') + # Only set the flag if we were not force-checking the path + state.PATH_VERIFIED = True return path def askToValidate(self, url): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 457077ab..14520476 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -50,6 +50,7 @@ class KodiMonitor(Monitor): # Assume that the user changed the settings so that we can now find the # path to all media files state.STOP_SYNC = False + state.PATH_VERIFIED = False # settings: window-variable items = { 'logLevel': 'plex_logLevel', diff --git a/resources/lib/state.py b/resources/lib/state.py index b364f749..ecae0964 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -10,6 +10,8 @@ STOP_PKC = False SUSPEND_LIBRARY_THREAD = False # Set if user decided to cancel sync STOP_SYNC = False +# Could we access the paths? +PATH_VERIFIED = False # Set if a Plex-Kodi DB sync is being done - along with # window('plex_dbScan') set to 'true' DB_SCAN = False From 8267fb48326c4c6e883315268462f71024f87409 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 15:05:56 +0200 Subject: [PATCH 025/509] Don't quit library sync if failed repeatedly --- resources/lib/librarysync.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 62062b03..75b8faa2 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1421,7 +1421,6 @@ class LibrarySync(Thread): startupComplete = False self.views = [] - errorcount = 0 log.info("---===### Starting LibrarySync ###===---") @@ -1514,13 +1513,6 @@ class LibrarySync(Thread): installSyncDone = True else: log.error("Initial start-up full sync unsuccessful") - errorcount += 1 - if errorcount > 2: - log.error("Startup full sync failed. Stopping sync") - # "Startup syncing process failed repeatedly" - # "Please restart" - dialog('ok', heading='{plex}', line1=lang(39404)) - break # Currently no db scan, so we can start a new scan elif state.DB_SCAN is False: From 4494add29870663940bd97e51579f90bcddaac55 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 15:13:22 +0200 Subject: [PATCH 026/509] Verify path for every Plex library on install sync --- resources/lib/librarysync.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 75b8faa2..06ffb2be 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -797,6 +797,8 @@ class LibrarySync(Thread): # PROCESS MOVIES ##### self.updatelist = [] for view in views: + if self.installSyncDone is not True: + state.PATH_VERIFIED = False if self.thread_stopped(): return False # Get items per view @@ -890,6 +892,8 @@ class LibrarySync(Thread): # PROCESS TV Shows ##### self.updatelist = [] for view in views: + if self.installSyncDone is not True: + state.PATH_VERIFIED = False if self.thread_stopped(): return False # Get items per view @@ -1058,6 +1062,8 @@ class LibrarySync(Thread): except ValueError: pass for view in views: + if self.installSyncDone is not True: + state.PATH_VERIFIED = False if self.thread_stopped(): return False # Get items per view From 40fc88c8f61867bc23f7711a770247624a402f0b Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 15:23:57 +0200 Subject: [PATCH 027/509] Increase logging --- resources/lib/kodimonitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 14520476..79574057 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,6 +47,7 @@ class KodiMonitor(Monitor): """ Monitor the PKC settings for changes made by the user """ + log.debug('PKC settings change detected') # Assume that the user changed the settings so that we can now find the # path to all media files state.STOP_SYNC = False From 0d108577ab638e83aadc5cd2bf954a1c342f3e06 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 16:10:44 +0200 Subject: [PATCH 028/509] Fix TypeError --- 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 f244844b..96597681 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1729,7 +1729,7 @@ class Music(Items): if album is None or album == 401: log.error('Could not download album, abort') return - self.add_updateAlbum(album[0]) + self.add_updateAlbum(album[0], children=[item]) plex_dbalbum = plex_db.getItem_byId(plex_albumId) try: albumid = plex_dbalbum[0] From 3daf82ef3dc79dfc7ddf3b036e349c839d493cd6 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 16:14:14 +0200 Subject: [PATCH 029/509] Code optimization --- resources/lib/librarysync.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 06ffb2be..d14356b2 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -296,11 +296,7 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: - if self.thread_stopped(): - xbmc.executebuiltin('InhibitIdleShutdown(false)') - setScreensaver(value=screensaver) - return False - if not process[itemtype](): + if self.thread_stopped() or not process[itemtype](): xbmc.executebuiltin('InhibitIdleShutdown(false)') setScreensaver(value=screensaver) return False From c0bef37dd58384d8f3041c772f111295f95959b3 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 16:26:51 +0200 Subject: [PATCH 030/509] Cancels syncs if lib sync thread gets suspended - Partially solves #333 --- resources/lib/librarysync.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index d14356b2..c60a866c 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -296,7 +296,9 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: - if self.thread_stopped() or not process[itemtype](): + if (self.thread_stopped() or + self.thread_suspended() or + not process[itemtype]()): xbmc.executebuiltin('InhibitIdleShutdown(false)') setScreensaver(value=screensaver) return False @@ -795,7 +797,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False # Get items per view viewId = view['id'] @@ -813,10 +815,9 @@ class LibrarySync(Thread): viewName, viewId) self.GetAndProcessXMLs(itemType) - log.info("Processed view") # Update viewstate for EVERY item for view in views: - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -890,7 +891,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False # Get items per view viewId = view['id'] @@ -919,7 +920,7 @@ class LibrarySync(Thread): # PROCESS TV Seasons ##### # Cycle through tv shows for tvShowId in allPlexTvShowsId: - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False # Grab all seasons to tvshow from PMS seasons = GetAllPlexChildren(tvShowId) @@ -944,7 +945,7 @@ class LibrarySync(Thread): # PROCESS TV Episodes ##### # Cycle through tv shows for view in views: - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False # Grab all episodes to tvshow from PMS episodes = GetAllPlexLeaves(view['id']) @@ -979,7 +980,7 @@ class LibrarySync(Thread): # Update viewstate: for view in views: - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1016,7 +1017,7 @@ class LibrarySync(Thread): for kind in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_SONG): - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False log.debug("Start processing music %s" % kind) self.allKodiElementsId = {} @@ -1033,7 +1034,7 @@ class LibrarySync(Thread): # Update viewstate for EVERY item for view in views: - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1060,7 +1061,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): return False # Get items per view itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) @@ -1135,7 +1136,7 @@ class LibrarySync(Thread): now = getUnixTimestamp() deleteListe = [] for i, item in enumerate(self.itemsToProcess): - if self.thread_stopped(): + if self.thread_stopped() or self.thread_suspended(): # Chances are that Kodi gets shut down break if item['state'] == 9: From 7f74dd93f4145de4c06940e79348bf8afaab8162 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 16:49:29 +0200 Subject: [PATCH 031/509] Vastly improve sync speed for music --- resources/lib/library_sync/get_metadata.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index 2100f1f0..afe47b5b 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -115,17 +115,9 @@ class Threaded_Get_Metadata(Thread): except (TypeError, IndexError, AttributeError): log.error('Could not get children for Plex id %s' % item['itemId']) - else: item['children'] = [] - for child in children_xml: - child_xml = GetPlexMetadata(child.attrib['ratingKey']) - try: - child_xml[0].attrib - except (TypeError, IndexError, AttributeError): - log.error('Could not get child for Plex id %s' - % child.attrib['ratingKey']) - else: - item['children'].append(child_xml[0]) + else: + item['children'] = children_xml # place item into out queue out_queue.put(item) From e5a77a1839a77b037719d457b76672a8b1d081b7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 19 Aug 2017 18:05:22 +0200 Subject: [PATCH 032/509] Version bump --- README.md | 2 +- addon.xml | 15 +++++++++++++-- changelog.txt | 11 +++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a88e755..e508c3ca 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.9-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.9-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.10-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 83707bb9..8562b5b7 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,18 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.9 + version 1.8.10 (beta only): +- Vastly improve sync speed for music +- Never show library sync dialog if media is playing +- Improvements to sync dialog +- Fix stop synching if path not found +- Resume aborted sync on PKC settings change +- Don't quit library sync if failed repeatedly +- Verify path for every Plex library on install sync +- More descriptive downloadable subtitles +- More code fixes and optimization + +version 1.8.9 - Fix playback not starting in some circumstances - Deactivate some annoying popups on install diff --git a/changelog.txt b/changelog.txt index 65d57151..b7c8181a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +version 1.8.10 (beta only): +- Vastly improve sync speed for music +- Never show library sync dialog if media is playing +- Improvements to sync dialog +- Fix stop synching if path not found +- Resume aborted sync on PKC settings change +- Don't quit library sync if failed repeatedly +- Verify path for every Plex library on install sync +- More descriptive downloadable subtitles +- More code fixes and optimization + version 1.8.9 - Fix playback not starting in some circumstances - Deactivate some annoying popups on install From 743d8dbb2f5f0408672b2df338645860d7bd4da5 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 07:42:11 +0200 Subject: [PATCH 033/509] Move sync indication setting to state.py --- resources/lib/kodimonitor.py | 49 +++++++++++++++++++++++------------- resources/lib/librarysync.py | 6 ++--- resources/lib/state.py | 5 ++++ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 79574057..3c60a944 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -20,6 +20,26 @@ import state log = logging.getLogger("PLEX."+__name__) +# settings: window-variable +WINDOW_SETTINGS = { + 'logLevel': 'plex_logLevel', + 'enableContext': 'plex_context', + 'plex_restricteduser': 'plex_restricteduser', + 'remapSMB': 'remapSMB', + 'replaceSMB': 'replaceSMB', + 'force_transcode_pix': 'plex_force_transcode_pix', + 'fetch_pms_item_number': 'fetch_pms_item_number' +} +# Path replacement +for typus in REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + WINDOW_SETTINGS[key] = key + +# settings: state-variable (state.py) +STATE_SETTINGS = { + 'dbSyncIndicator': state.SYNC_DIALOG +} ############################################################################### @@ -52,24 +72,8 @@ class KodiMonitor(Monitor): # path to all media files state.STOP_SYNC = False state.PATH_VERIFIED = False - # settings: window-variable - items = { - 'logLevel': 'plex_logLevel', - 'enableContext': 'plex_context', - 'plex_restricteduser': 'plex_restricteduser', - 'dbSyncIndicator': 'dbSyncIndicator', - 'remapSMB': 'remapSMB', - 'replaceSMB': 'replaceSMB', - 'force_transcode_pix': 'plex_force_transcode_pix', - 'fetch_pms_item_number': 'fetch_pms_item_number' - } - # 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(): + for settings_value, window_value in WINDOW_SETTINGS.iteritems(): if window(window_value) != settings(settings_value): log.debug('PKC settings changed: %s is now %s' % (settings_value, settings(settings_value))) @@ -77,6 +81,17 @@ class KodiMonitor(Monitor): if settings_value == 'fetch_pms_item_number': log.info('Requesting playlist/nodes refresh') window('plex_runLibScan', value="views") + # Reset the state variables in state.py + for settings_value, state_value in STATE_SETTINGS.iteritems(): + new = settings(settings_value) + if new == 'true': + new = True + elif new == 'false': + new = False + if state_value != new: + log.debug('PKC settings changed: %s is now %s' + % (settings_value, new)) + state_value = new @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index c60a866c..128a495a 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -65,7 +65,7 @@ class LibrarySync(Thread): self.syncThreadNumber = int(settings('syncThreadNumber')) self.installSyncDone = settings('SyncInstallRunDone') == 'true' - window('dbSyncIndicator', value=settings('dbSyncIndicator')) + state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' self.enableMusic = settings('enableMusic') == "true" self.enableBackgroundSync = settings( 'enableBackgroundSync') == "true" @@ -95,7 +95,7 @@ class LibrarySync(Thread): if self.xbmcplayer.isPlaying(): # Don't show any dialog if media is playing return - if window('dbSyncIndicator') != 'true': + if state.SYNC_DIALOG is not True: if not forced: return if icon == "plex": @@ -735,7 +735,7 @@ class LibrarySync(Thread): thread.start() threads.append(thread) # Start one thread to show sync progress ONLY for new PMS items - if self.new_items_only is True and window('dbSyncIndicator') == 'true': + if self.new_items_only is True and state.SYNC_DIALOG is True: thread = sync_info.Threaded_Show_Sync_Info(itemNumber, itemType) thread.setDaemon(True) thread.start() diff --git a/resources/lib/state.py b/resources/lib/state.py index ecae0964..91c9d913 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -27,6 +27,11 @@ DIRECT_PATHS = False # Shall we replace custom user ratings with the number of versions available? INDICATE_MEDIA_VERSIONS = False +# Stemming from the PKC settings.xml +# Shall we show Kodi dialogs when synching? +SYNC_DIALOG = True + + # Along with window('plex_authenticated') AUTHENTICATED = False # plex.tv username From a2b145e4ec665d4ea1cf8b21ee5a716f023ad56a Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 08:01:48 +0200 Subject: [PATCH 034/509] Force show sync if user manually initiated --- resources/lib/librarysync.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 128a495a..85e6697f 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -70,6 +70,8 @@ class LibrarySync(Thread): self.enableBackgroundSync = settings( 'enableBackgroundSync') == "true" + # Show sync dialog even if user deactivated? + self.force_dialog = True # Init for replacing paths window('remapSMB', value=settings('remapSMB')) window('replaceSMB', value=settings('replaceSMB')) @@ -82,22 +84,19 @@ class LibrarySync(Thread): window('kodiplextimeoffset', value=str(self.timeoffset)) Thread.__init__(self) - def showKodiNote(self, message, forced=False, icon="plex"): + def showKodiNote(self, message, icon="plex"): """ Shows a Kodi popup, if user selected to do so. Pass message in unicode or string icon: "plex": shows Plex icon "error": shows Kodi error icon - - forced: always show popup, even if user setting to off """ if self.xbmcplayer.isPlaying(): # Don't show any dialog if media is playing return - if state.SYNC_DIALOG is not True: - if not forced: - return + if state.SYNC_DIALOG is not True and self.force_dialog is not True: + return if icon == "plex": dialog('notification', heading='{plex}', @@ -735,7 +734,8 @@ class LibrarySync(Thread): thread.start() threads.append(thread) # Start one thread to show sync progress ONLY for new PMS items - if self.new_items_only is True and state.SYNC_DIALOG is True: + if self.new_items_only is True and (state.SYNC_DIALOG is True or + self.force_dialog is True): thread = sync_info.Threaded_Show_Sync_Info(itemNumber, itemType) thread.setDaemon(True) thread.start() @@ -1445,6 +1445,8 @@ class LibrarySync(Thread): xbmc.sleep(1000) if (window('plex_dbCheck') != "true" and installSyncDone): + # Install sync was already done, don't force-show dialogs + self.force_dialog = False # Verify the validity of the database currentVersion = settings('dbCreatedWithVersion') minVersion = window('plex_minDBVersion') @@ -1514,11 +1516,14 @@ class LibrarySync(Thread): settings('SyncInstallRunDone', value="true") settings("dbCreatedWithVersion", v.ADDON_VERSION) installSyncDone = True + self.force_dialog = False else: log.error("Initial start-up full sync unsuccessful") # Currently no db scan, so we can start a new scan elif state.DB_SCAN is False: + # Force-show dialogs since they are user-initiated + self.force_dialog = True # Full scan was requested from somewhere else, e.g. userclient if window('plex_runLibScan') in ("full", "repair"): log.info('Full library scan requested, starting') @@ -1532,7 +1537,7 @@ class LibrarySync(Thread): window('plex_dbScan', clear=True) state.DB_SCAN = False # Full library sync finished - self.showKodiNote(lang(39407), forced=True) + self.showKodiNote(lang(39407)) # Reset views was requested from somewhere else elif window('plex_runLibScan') == "views": log.info('Refresh playlist and nodes requested, starting') @@ -1549,13 +1554,12 @@ class LibrarySync(Thread): # Ran successfully log.info("Refresh playlists/nodes completed") # "Plex playlists/nodes refreshed" - self.showKodiNote(lang(39405), forced=True) + self.showKodiNote(lang(39405)) else: # Failed log.error("Refresh playlists/nodes failed") # "Plex playlists/nodes refresh failed" self.showKodiNote(lang(39406), - forced=True, icon="error") window('plex_dbScan', clear=True) state.DB_SCAN = False @@ -1579,6 +1583,8 @@ class LibrarySync(Thread): state.DB_SCAN = False else: now = getUnixTimestamp() + # Standard syncs - don't force-show dialogs + self.force_dialog = False if (now - lastSync > fullSyncInterval and not self.xbmcplayer.isPlaying()): lastSync = now @@ -1587,13 +1593,14 @@ class LibrarySync(Thread): window('plex_dbScan', value="true") if fullSync() is False and not thread_stopped(): log.error('Could not finish scheduled full sync') + self.force_dialog = True self.showKodiNote(lang(39410), - forced=True, icon='error') + self.force_dialog = False window('plex_dbScan', clear=True) state.DB_SCAN = False # Full library sync finished - self.showKodiNote(lang(39407), forced=False) + self.showKodiNote(lang(39407)) elif now - lastTimeSync > oneDay: lastTimeSync = now log.info('Starting daily time sync') From b045c49ad0414069556cfa2b80e0b2993da1214d Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 08:02:44 +0200 Subject: [PATCH 035/509] Sleep longer --- resources/lib/librarysync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 85e6697f..3dbd12b0 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1620,7 +1620,7 @@ class LibrarySync(Thread): try: message = queue.get(block=False) except Queue.Empty: - xbmc.sleep(100) + xbmc.sleep(300) continue # Got a message from PMS; process it else: @@ -1630,9 +1630,9 @@ class LibrarySync(Thread): continue else: # Still sleep if backgroundsync disabled - xbmc.sleep(100) + xbmc.sleep(300) - xbmc.sleep(100) + xbmc.sleep(300) # doUtils could still have a session open due to interrupted sync try: From 5585f8a4e0e8ac04e88bf1553e52dba6e863c273 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 08:03:08 +0200 Subject: [PATCH 036/509] Revert "Sleep longer" This reverts commit b045c49ad0414069556cfa2b80e0b2993da1214d. --- resources/lib/librarysync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 3dbd12b0..85e6697f 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1620,7 +1620,7 @@ class LibrarySync(Thread): try: message = queue.get(block=False) except Queue.Empty: - xbmc.sleep(300) + xbmc.sleep(100) continue # Got a message from PMS; process it else: @@ -1630,9 +1630,9 @@ class LibrarySync(Thread): continue else: # Still sleep if backgroundsync disabled - xbmc.sleep(300) + xbmc.sleep(100) - xbmc.sleep(300) + xbmc.sleep(100) # doUtils could still have a session open due to interrupted sync try: From 31be5f30f3cf1340aab607a271d28e904eec51d9 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 18:53:38 +0200 Subject: [PATCH 037/509] Move init of syncs to state.py --- default.py | 20 +-- resources/lib/command_pipeline.py | 2 + resources/lib/entrypoint.py | 2 +- resources/lib/kodimonitor.py | 7 +- resources/lib/librarysync.py | 206 ++++++++++++++++-------------- resources/lib/state.py | 2 + service.py | 2 +- 7 files changed, 130 insertions(+), 111 deletions(-) diff --git a/default.py b/default.py index 96983316..164db4c4 100644 --- a/default.py +++ b/default.py @@ -33,7 +33,7 @@ sys_path.append(_base_resource) import entrypoint from utils import window, pickl_window, reset, passwordsXML, language as lang,\ - dialog + dialog, plex_command from pickler import unpickle_me from PKC_listitem import convert_PKC_to_listitem import variables as v @@ -127,28 +127,29 @@ class Main(): log.error('Not connected to a PMS.') else: if mode == 'repair': - window('plex_runLibScan', value='repair') log.info('Requesting repair lib sync') + plex_command('RUN_LIB_SCAN', 'repair') elif mode == 'manualsync': log.info('Requesting full library scan') - window('plex_runLibScan', value='full') + plex_command('RUN_LIB_SCAN', 'full') elif mode == 'texturecache': - window('plex_runLibScan', value='del_textures') + log.info('Requesting texture caching of all textures') + plex_command('RUN_LIB_SCAN', 'textures') elif mode == 'chooseServer': entrypoint.chooseServer() elif mode == 'refreshplaylist': log.info('Requesting playlist/nodes refresh') - window('plex_runLibScan', value='views') + plex_command('RUN_LIB_SCAN', 'views') elif mode == 'deviceid': self.deviceid() elif mode == 'fanart': log.info('User requested fanarttv refresh') - window('plex_runLibScan', value='fanart') + plex_command('RUN_LIB_SCAN', 'fanart') elif '/extrafanart' in argv[0]: plexpath = argv[2][1:] @@ -165,7 +166,8 @@ class Main(): else: entrypoint.doMainListing(content_type=params.get('content_type')) - def play(self): + @staticmethod + def play(): """ Start up playback_starter in main Python thread """ @@ -190,7 +192,8 @@ class Main(): listitem = convert_PKC_to_listitem(result.listitem) setResolvedUrl(HANDLE, True, listitem) - def deviceid(self): + @staticmethod + def deviceid(): deviceId_old = window('plex_client_Id') from clientinfo import getDeviceId try: @@ -205,6 +208,7 @@ class Main(): dialog('ok', lang(29999), lang(33033)) executebuiltin('RestartApp') + if __name__ == '__main__': log.info('%s started' % v.ADDON_ID) Main() diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 64fc799c..a50edd8a 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -64,6 +64,8 @@ class Monitor_Window(Thread): elif value.startswith('PLEX_USERNAME-'): state.PLEX_USERNAME = \ value.replace('PLEX_USERNAME-', '') or None + elif value.startswith('RUN_LIB_SCAN-'): + state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '') else: raise NotImplementedError('%s not implemented' % value) else: diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 5dfc93d2..3fc8f0a8 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -975,7 +975,7 @@ def __LogIn(): SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed out! """ - window('plex_runLibScan', value='full') + plex_command('RUN_LIB_SCAN', 'full') # Restart user client plex_command('SUSPEND_USER_CLIENT', 'False') diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 3c60a944..a46c509a 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -9,7 +9,8 @@ from xbmc import Monitor, Player, sleep import downloadutils import plexdb_functions as plexdb -from utils import window, settings, CatchExceptions, tryDecode, tryEncode +from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ + plex_command from PlexFunctions import scrobble from kodidb_functions import get_kodiid_from_filename from PlexAPI import API @@ -80,7 +81,7 @@ class KodiMonitor(Monitor): window(window_value, value=settings(settings_value)) if settings_value == 'fetch_pms_item_number': log.info('Requesting playlist/nodes refresh') - window('plex_runLibScan', value="views") + plex_command('RUN_LIB_SCAN', 'views') # Reset the state variables in state.py for settings_value, state_value in STATE_SETTINGS.iteritems(): new = settings(settings_value) @@ -157,7 +158,7 @@ class KodiMonitor(Monitor): elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": sleep(5000) - window('plex_runLibScan', value="full") + plex_command('RUN_LIB_SCAN', 'full') elif method == "System.OnQuit": log.info('Kodi OnQuit detected - shutting down') diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 85e6697f..a555e601 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1391,6 +1391,69 @@ class LibrarySync(Thread): 'refresh': refresh }) + def triage_lib_scans(self): + """ + """ + if state.RUN_LIB_SCAN in ("full", "repair"): + log.info('Full library scan requested, starting') + window('plex_dbScan', value="true") + state.DB_SCAN = True + if state.RUN_LIB_SCAN == "full": + self.fullSync() + elif state.RUN_LIB_SCAN == "repair": + self.fullSync(repair=True) + state.RUN_LIB_SCAN = None + window('plex_dbScan', clear=True) + state.DB_SCAN = False + # Full library sync finished + self.showKodiNote(lang(39407)) + # Reset views was requested from somewhere else + elif state.RUN_LIB_SCAN == "views": + log.info('Refresh playlist and nodes requested, starting') + window('plex_dbScan', value="true") + state.DB_SCAN = True + state.RUN_LIB_SCAN = None + + # First remove playlists + deletePlaylists() + # Remove video nodes + deleteNodes() + # Kick off refresh + if self.maintainViews() is True: + # Ran successfully + log.info("Refresh playlists/nodes completed") + # "Plex playlists/nodes refreshed" + self.showKodiNote(lang(39405)) + else: + # Failed + log.error("Refresh playlists/nodes failed") + # "Plex playlists/nodes refresh failed" + self.showKodiNote(lang(39406), + icon="error") + window('plex_dbScan', clear=True) + state.DB_SCAN = False + elif state.RUN_LIB_SCAN == 'fanart': + state.RUN_LIB_SCAN = None + # Only look for missing fanart (No) + # or refresh all fanart (Yes) + self.fanartSync(refresh=dialog( + 'yesno', + heading='{plex}', + line1=lang(39223), + nolabel=lang(39224), + yeslabel=lang(39225))) + elif state.RUN_LIB_SCAN == 'textures': + state.RUN_LIB_SCAN = None + state.DB_SCAN = True + window('plex_dbScan', value="true") + import artwork + artwork.Artwork().fullTextureCacheSync() + window('plex_dbScan', clear=True) + state.DB_SCAN = False + else: + raise NotImplementedError('Library scan not defined: %s' + % state.RUN_LIB_SCAN) + def run(self): try: self.run_internal() @@ -1522,115 +1585,62 @@ class LibrarySync(Thread): # Currently no db scan, so we can start a new scan elif state.DB_SCAN is False: - # Force-show dialogs since they are user-initiated - self.force_dialog = True # Full scan was requested from somewhere else, e.g. userclient - if window('plex_runLibScan') in ("full", "repair"): - log.info('Full library scan requested, starting') - window('plex_dbScan', value="true") + if state.RUN_LIB_SCAN is not None: + # Force-show dialogs since they are user-initiated + self.force_dialog = True + self.triage_lib_scans() + self.force_dialog = False + continue + now = getUnixTimestamp() + # Standard syncs - don't force-show dialogs + self.force_dialog = False + if (now - lastSync > fullSyncInterval and + not self.xbmcplayer.isPlaying()): + lastSync = now + log.info('Doing scheduled full library scan') state.DB_SCAN = True - if window('plex_runLibScan') == "full": - fullSync() - elif window('plex_runLibScan') == "repair": - fullSync(repair=True) - window('plex_runLibScan', clear=True) + window('plex_dbScan', value="true") + if fullSync() is False and not thread_stopped(): + log.error('Could not finish scheduled full sync') + self.force_dialog = True + self.showKodiNote(lang(39410), + icon='error') + self.force_dialog = False window('plex_dbScan', clear=True) state.DB_SCAN = False # Full library sync finished self.showKodiNote(lang(39407)) - # Reset views was requested from somewhere else - elif window('plex_runLibScan') == "views": - log.info('Refresh playlist and nodes requested, starting') - window('plex_dbScan', value="true") - state.DB_SCAN = True - window('plex_runLibScan', clear=True) - - # First remove playlists - deletePlaylists() - # Remove video nodes - deleteNodes() - # Kick off refresh - if self.maintainViews() is True: - # Ran successfully - log.info("Refresh playlists/nodes completed") - # "Plex playlists/nodes refreshed" - self.showKodiNote(lang(39405)) - else: - # Failed - log.error("Refresh playlists/nodes failed") - # "Plex playlists/nodes refresh failed" - self.showKodiNote(lang(39406), - icon="error") - window('plex_dbScan', clear=True) - state.DB_SCAN = False - elif window('plex_runLibScan') == 'fanart': - window('plex_runLibScan', clear=True) - # Only look for missing fanart (No) - # or refresh all fanart (Yes) - self.fanartSync(refresh=dialog( - 'yesno', - heading='{plex}', - line1=lang(39223), - nolabel=lang(39224), - yeslabel=lang(39225))) - elif window('plex_runLibScan') == 'del_textures': - window('plex_runLibScan', clear=True) + elif now - lastTimeSync > oneDay: + lastTimeSync = now + log.info('Starting daily time sync') state.DB_SCAN = True window('plex_dbScan', value="true") - import artwork - artwork.Artwork().fullTextureCacheSync() + self.syncPMStime() window('plex_dbScan', clear=True) state.DB_SCAN = False - else: - now = getUnixTimestamp() - # Standard syncs - don't force-show dialogs - self.force_dialog = False - if (now - lastSync > fullSyncInterval and - not self.xbmcplayer.isPlaying()): - lastSync = now - log.info('Doing scheduled full library scan') - state.DB_SCAN = True - window('plex_dbScan', value="true") - if fullSync() is False and not thread_stopped(): - log.error('Could not finish scheduled full sync') - self.force_dialog = True - self.showKodiNote(lang(39410), - icon='error') - self.force_dialog = False - window('plex_dbScan', clear=True) - state.DB_SCAN = False - # Full library sync finished - self.showKodiNote(lang(39407)) - elif now - lastTimeSync > oneDay: - lastTimeSync = now - log.info('Starting daily time sync') - state.DB_SCAN = True - window('plex_dbScan', value="true") - self.syncPMStime() - window('plex_dbScan', clear=True) - state.DB_SCAN = False - elif enableBackgroundSync: - # Check back whether we should process something - # Only do this once every while (otherwise, potentially - # many screen refreshes lead to flickering) - if now - lastProcessing > 5: - lastProcessing = now - processItems() - # See if there is a PMS message we need to handle - try: - message = queue.get(block=False) - except Queue.Empty: - xbmc.sleep(100) - continue - # Got a message from PMS; process it - else: - processMessage(message) - queue.task_done() - # NO sleep! - continue - else: - # Still sleep if backgroundsync disabled + elif enableBackgroundSync: + # Check back whether we should process something + # Only do this once every while (otherwise, potentially + # many screen refreshes lead to flickering) + if now - lastProcessing > 5: + lastProcessing = now + processItems() + # See if there is a PMS message we need to handle + try: + message = queue.get(block=False) + except Queue.Empty: xbmc.sleep(100) + continue + # Got a message from PMS; process it + else: + processMessage(message) + queue.task_done() + # NO sleep! + continue + else: + # Still sleep if backgroundsync disabled + xbmc.sleep(100) xbmc.sleep(100) diff --git a/resources/lib/state.py b/resources/lib/state.py index 91c9d913..7a9cd8ef 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -26,6 +26,8 @@ RESTRICTED_USER = False DIRECT_PATHS = False # Shall we replace custom user ratings with the number of versions available? INDICATE_MEDIA_VERSIONS = False +# Do we need to run a special library scan? +RUN_LIB_SCAN = None # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? diff --git a/service.py b/service.py index c17fb082..e0990f92 100644 --- a/service.py +++ b/service.py @@ -110,7 +110,7 @@ class Service(): "plex_dbCheck", "plex_kodiScan", "plex_shouldStop", "plex_dbScan", "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", - "plex_runLibScan", "pms_token", "plex_token", + "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", "kodiplextimeoffset", "countError", "countUnauthorized", From 5f45cc1c9b5411c1797ceda8f5583d03570c317e Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 18:53:52 +0200 Subject: [PATCH 038/509] Remove obsolete function --- resources/lib/entrypoint.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 3fc8f0a8..de4b9f83 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -575,14 +575,6 @@ def getExtraFanArt(plexid, plexPath): xbmcplugin.endOfDirectory(HANDLE) -def RunLibScan(mode): - if window('plex_online') != "true": - # Server is not online, do not run the sync - dialog('ok', lang(29999), lang(39205)) - else: - window('plex_runLibScan', value='full') - - def getOnDeck(viewid, mediatype, tagname, limit): """ Retrieves Plex On Deck items, currently only for TV shows From 7b6834b32699ef3f4006eb42ee3cb66e513f5819 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 18:59:47 +0200 Subject: [PATCH 039/509] Code optimization --- default.py | 5 +---- resources/lib/command_pipeline.py | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/default.py b/default.py index 164db4c4..43b7bfc6 100644 --- a/default.py +++ b/default.py @@ -172,10 +172,7 @@ class Main(): Start up playback_starter in main Python thread """ # Put the request into the 'queue' - while window('plex_command'): - sleep(50) - window('plex_command', - value='play_%s' % argv[2]) + plex_command('PLAY', argv[2]) # Wait for the result while not pickl_window('plex_result'): sleep(50) diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index a50edd8a..13ec7be9 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -21,9 +21,6 @@ class Monitor_Window(Thread): Monitors window('plex_command') for new entries that we need to take care of, e.g. for new plays initiated on the Kodi side with addon paths. - Possible values of window('plex_command'): - 'play_....': to start playback using playback_starter - Adjusts state.py accordingly """ # Borg - multiple instances, shared state @@ -40,9 +37,8 @@ class Monitor_Window(Thread): if window('plex_command'): value = window('plex_command') window('plex_command', clear=True) - if value.startswith('play_'): - queue.put(value) - + if value.startswith('PLAY-'): + queue.put(value.replace('PLAY-', '')) elif value == 'SUSPEND_LIBRARY_THREAD-True': state.SUSPEND_LIBRARY_THREAD = True elif value == 'SUSPEND_LIBRARY_THREAD-False': From 66eb599a1411534c501770efa096bf2b94b70bf7 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 19:38:41 +0200 Subject: [PATCH 040/509] Code optimization --- resources/lib/librarysync.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index a555e601..c56cd2b3 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1393,6 +1393,8 @@ class LibrarySync(Thread): def triage_lib_scans(self): """ + Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually + triggered full or repair syncs """ if state.RUN_LIB_SCAN in ("full", "repair"): log.info('Full library scan requested, starting') @@ -1400,9 +1402,8 @@ class LibrarySync(Thread): state.DB_SCAN = True if state.RUN_LIB_SCAN == "full": self.fullSync() - elif state.RUN_LIB_SCAN == "repair": + else: self.fullSync(repair=True) - state.RUN_LIB_SCAN = None window('plex_dbScan', clear=True) state.DB_SCAN = False # Full library sync finished @@ -1412,8 +1413,6 @@ class LibrarySync(Thread): log.info('Refresh playlist and nodes requested, starting') window('plex_dbScan', value="true") state.DB_SCAN = True - state.RUN_LIB_SCAN = None - # First remove playlists deletePlaylists() # Remove video nodes @@ -1433,7 +1432,6 @@ class LibrarySync(Thread): window('plex_dbScan', clear=True) state.DB_SCAN = False elif state.RUN_LIB_SCAN == 'fanart': - state.RUN_LIB_SCAN = None # Only look for missing fanart (No) # or refresh all fanart (Yes) self.fanartSync(refresh=dialog( @@ -1443,7 +1441,6 @@ class LibrarySync(Thread): nolabel=lang(39224), yeslabel=lang(39225))) elif state.RUN_LIB_SCAN == 'textures': - state.RUN_LIB_SCAN = None state.DB_SCAN = True window('plex_dbScan', value="true") import artwork @@ -1453,6 +1450,8 @@ class LibrarySync(Thread): else: raise NotImplementedError('Library scan not defined: %s' % state.RUN_LIB_SCAN) + # Reset + state.RUN_LIB_SCAN = None def run(self): try: From ce508257a3ca925290334ed8d309949682de9187 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 21 Aug 2017 19:42:41 +0200 Subject: [PATCH 041/509] Move Kodi DB check flag to state.py --- resources/lib/librarysync.py | 5 ++--- resources/lib/state.py | 2 ++ service.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index c56cd2b3..9b2f0ad2 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1506,7 +1506,7 @@ class LibrarySync(Thread): return xbmc.sleep(1000) - if (window('plex_dbCheck') != "true" and installSyncDone): + if state.KODI_DB_CHECKED is False and installSyncDone: # Install sync was already done, don't force-show dialogs self.force_dialog = False # Verify the validity of the database @@ -1529,8 +1529,7 @@ class LibrarySync(Thread): else: reset() break - - window('plex_dbCheck', value="true") + state.KODI_DB_CHECKED = True if not startupComplete: # Also runs when first installed diff --git a/resources/lib/state.py b/resources/lib/state.py index 7a9cd8ef..f744fa0e 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -32,6 +32,8 @@ RUN_LIB_SCAN = None # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? SYNC_DIALOG = True +# Have we already checked the Kodi DB on consistency? +KODI_DB_CHECKED = False # Along with window('plex_authenticated') diff --git a/service.py b/service.py index e0990f92..0d2daf3a 100644 --- a/service.py +++ b/service.py @@ -107,7 +107,7 @@ class Service(): # Reset window props for profile switch properties = [ "plex_online", "plex_serverStatus", "plex_onWake", - "plex_dbCheck", "plex_kodiScan", + "plex_kodiScan", "plex_shouldStop", "plex_dbScan", "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", "pms_token", "plex_token", From 261a0aad4cc2c5d73df9a261f4d8baa6297e282c Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 22 Aug 2017 07:18:19 +0200 Subject: [PATCH 042/509] Allow replace path settings changes without reboot --- resources/lib/PlexAPI.py | 8 ++++---- resources/lib/kodimonitor.py | 12 ++++++++++-- resources/lib/librarysync.py | 7 ++++--- resources/lib/state.py | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 83098360..53375015 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2580,13 +2580,13 @@ class API(): if path is None: return None typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus] - if window('remapSMB') == 'true': - path = path.replace(window('remapSMB%sOrg' % typus), - window('remapSMB%sNew' % typus), + if state.REMAP_PATH is True: + path = path.replace(getattr(state, 'remapSMB%sOrg' % typus), + getattr(state, 'remapSMB%sNew' % typus), 1) # There might be backslashes left over: path = path.replace('\\', '/') - elif window('replaceSMB') == 'true': + elif state.REPLACE_SMB_PATH is True: if path.startswith('\\\\'): path = 'smb:' + path.replace('\\', '/') if ((state.PATH_VERIFIED and forceCheck is False) or diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index a46c509a..26f585b0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -26,7 +26,6 @@ WINDOW_SETTINGS = { 'logLevel': 'plex_logLevel', 'enableContext': 'plex_context', 'plex_restricteduser': 'plex_restricteduser', - 'remapSMB': 'remapSMB', 'replaceSMB': 'replaceSMB', 'force_transcode_pix': 'plex_force_transcode_pix', 'fetch_pms_item_number': 'fetch_pms_item_number' @@ -39,7 +38,16 @@ for typus in REMAP_TYPE_FROM_PLEXTYPE.values(): # settings: state-variable (state.py) STATE_SETTINGS = { - 'dbSyncIndicator': state.SYNC_DIALOG + 'dbSyncIndicator': state.SYNC_DIALOG, + 'remapSMB': state.REMAP_PATH, + 'remapSMBmovieOrg': state.remapSMBmovieOrg, + 'remapSMBmovieNew': state.remapSMBmovieNew, + 'remapSMBtvOrg': state.remapSMBtvOrg, + 'remapSMBtvNew': state.remapSMBtvNew, + 'remapSMBmusicOrg': state.remapSMBmusicOrg, + 'remapSMBmusicNew': state.remapSMBmusicNew, + 'remapSMBphotoOrg': state.remapSMBphotoOrg, + 'remapSMBphotoNew': state.remapSMBphotoNew } ############################################################################### diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 9b2f0ad2..de661200 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -73,12 +73,13 @@ class LibrarySync(Thread): # Show sync dialog even if user deactivated? self.force_dialog = True # Init for replacing paths - window('remapSMB', value=settings('remapSMB')) - window('replaceSMB', value=settings('replaceSMB')) + state.REPLACE_SMB_PATH = True if settings('replaceSMB') == 'true' \ + else False + state.REMAP_PATH = True if settings('remapSMB') == 'true' else False for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): for arg in ('Org', 'New'): key = 'remapSMB%s%s' % (typus, arg) - window(key, value=settings(key)) + setattr(state, key, settings(key)) # Just in case a time sync goes wrong self.timeoffset = int(settings('kodiplextimeoffset')) window('kodiplextimeoffset', value=str(self.timeoffset)) diff --git a/resources/lib/state.py b/resources/lib/state.py index f744fa0e..8434b86b 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -35,6 +35,20 @@ SYNC_DIALOG = True # Have we already checked the Kodi DB on consistency? KODI_DB_CHECKED = False +# Path remapping mechanism (e.g. smb paths) +# Do we replace \\myserver\path to smb://myserver/path? +REPLACE_SMB_PATH = False +# Do we generally remap? +REMAP_PATH = False +# Mappings for REMAP_PATH: +remapSMBmovieOrg = None +remapSMBmovieNew = None +remapSMBtvOrg = None +remapSMBtvNew = None +remapSMBmusicOrg = None +remapSMBmusicNew = None +remapSMBphotoOrg = None +remapSMBphotoNew = None # Along with window('plex_authenticated') AUTHENTICATED = False From cb459f2fd58348a29a2c7ab463a420f35ccc527c Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 22 Aug 2017 08:16:21 +0200 Subject: [PATCH 043/509] Enable many setting changes without Kodi restart --- resources/lib/kodimonitor.py | 11 ++++++++- resources/lib/librarysync.py | 48 +++++++++++++++++------------------- resources/lib/state.py | 13 ++++++++++ resources/lib/utils.py | 2 +- service.py | 2 +- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 26f585b0..eea0ae9c 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,7 +47,9 @@ STATE_SETTINGS = { 'remapSMBmusicOrg': state.remapSMBmusicOrg, 'remapSMBmusicNew': state.remapSMBmusicNew, 'remapSMBphotoOrg': state.remapSMBphotoOrg, - 'remapSMBphotoNew': state.remapSMBphotoNew + 'remapSMBphotoNew': state.remapSMBphotoNew, + 'enableMusic': state.ENABLE_MUSIC, + 'enableBackgroundSync': state.BACKGROUND_SYNC } ############################################################################### @@ -101,6 +103,13 @@ class KodiMonitor(Monitor): log.debug('PKC settings changed: %s is now %s' % (settings_value, new)) state_value = new + # Special cases, overwrite all internal settings + state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60 + state.BACKGROUNDSYNC_SAFTYMARGIN = int( + settings('backgroundsync_saftyMargin')) + state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) + # Never set through the user + # state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index de661200..bc4b34bb 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -55,34 +55,33 @@ class LibrarySync(Thread): if settings('FanartTV') == 'true': self.fanartthread = Process_Fanart_Thread(self.fanartqueue) # How long should we wait at least to process new/changed PMS items? - self.saftyMargin = int(settings('backgroundsync_saftyMargin')) - - self.fullSyncInterval = int(settings('fullSyncInterval')) * 60 self.user = userclient.UserClient() self.vnodes = videonodes.VideoNodes() self.xbmcplayer = xbmc.Player() - self.syncThreadNumber = int(settings('syncThreadNumber')) self.installSyncDone = settings('SyncInstallRunDone') == 'true' + + state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 + state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' - self.enableMusic = settings('enableMusic') == "true" - self.enableBackgroundSync = settings( - 'enableBackgroundSync') == "true" + state.ENABLE_MUSIC = settings('enableMusic') == 'true' + state.BACKGROUND_SYNC = settings( + 'enableBackgroundSync') == 'true' + state.BACKGROUNDSYNC_SAFTYMARGIN = int( + settings('backgroundsync_saftyMargin')) # Show sync dialog even if user deactivated? self.force_dialog = True # Init for replacing paths - state.REPLACE_SMB_PATH = True if settings('replaceSMB') == 'true' \ - else False - state.REMAP_PATH = True if settings('remapSMB') == 'true' else False + state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' + state.REMAP_PATH = settings('remapSMB') == 'true' for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): for arg in ('Org', 'New'): key = 'remapSMB%s%s' % (typus, arg) setattr(state, key, settings(key)) # Just in case a time sync goes wrong - self.timeoffset = int(settings('kodiplextimeoffset')) - window('kodiplextimeoffset', value=str(self.timeoffset)) + state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) Thread.__init__(self) def showKodiNote(self, message, icon="plex"): @@ -206,11 +205,10 @@ class LibrarySync(Thread): return False # Calculate time offset Kodi-PMS - self.timeoffset = int(koditime) - int(plextime) - window('kodiplextimeoffset', value=str(self.timeoffset)) - settings('kodiplextimeoffset', value=str(self.timeoffset)) + state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime) + settings('kodiplextimeoffset', value=str(state.KODI_PLEX_TIME_OFFSET)) log.info("Time offset Koditime - Plextime in seconds: %s" - % str(self.timeoffset)) + % str(state.KODI_PLEX_TIME_OFFSET)) return True def initializeDBs(self): @@ -291,7 +289,7 @@ class LibrarySync(Thread): 'movies': self.PlexMovies, 'tvshows': self.PlexTVShows, } - if self.enableMusic: + if state.ENABLE_MUSIC: process['music'] = self.PlexMusic # Do the processing @@ -305,7 +303,7 @@ class LibrarySync(Thread): # Let kodi update the views in any case, since we're doing a full sync xbmc.executebuiltin('UpdateLibrary(video)') - if self.enableMusic: + if state.ENABLE_MUSIC: xbmc.executebuiltin('UpdateLibrary(music)') window('plex_initialScan', clear=True) @@ -468,7 +466,7 @@ class LibrarySync(Thread): """ Compare the views to Plex """ - if state.DIRECT_PATHS is True and self.enableMusic is True: + if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True: if music.set_excludefromscan_music_folders() is True: log.info('Detected new Music library - restarting now') # 'New Plex music library detected. Sorry, but we need to @@ -721,7 +719,7 @@ class LibrarySync(Thread): getMetadataQueue.put(updateItem) # Spawn GetMetadata threads for downloading threads = [] - for i in range(min(self.syncThreadNumber, itemNumber)): + for i in range(min(state.SYNC_THREAD_NUMBER, itemNumber)): thread = Threaded_Get_Metadata(getMetadataQueue, processMetadataQueue) thread.setDaemon(True) @@ -1142,7 +1140,7 @@ class LibrarySync(Thread): break if item['state'] == 9: successful = self.process_deleteditems(item) - elif now - item['timestamp'] < self.saftyMargin: + elif now - item['timestamp'] < state.BACKGROUNDSYNC_SAFTYMARGIN: # We haven't waited long enough for the PMS to finish # processing the item. Do it later (excepting deletions) continue @@ -1472,11 +1470,11 @@ class LibrarySync(Thread): thread_stopped = self.thread_stopped thread_suspended = self.thread_suspended installSyncDone = self.installSyncDone - enableBackgroundSync = self.enableBackgroundSync + background_sync = state.BACKGROUND_SYNC fullSync = self.fullSync processMessage = self.processMessage processItems = self.processItems - fullSyncInterval = self.fullSyncInterval + FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL lastSync = 0 lastTimeSync = 0 lastProcessing = 0 @@ -1594,7 +1592,7 @@ class LibrarySync(Thread): now = getUnixTimestamp() # Standard syncs - don't force-show dialogs self.force_dialog = False - if (now - lastSync > fullSyncInterval and + if (now - lastSync > FULL_SYNC_INTERVALL and not self.xbmcplayer.isPlaying()): lastSync = now log.info('Doing scheduled full library scan') @@ -1618,7 +1616,7 @@ class LibrarySync(Thread): self.syncPMStime() window('plex_dbScan', clear=True) state.DB_SCAN = False - elif enableBackgroundSync: + elif background_sync: # Check back whether we should process something # Only do this once every while (otherwise, potentially # many screen refreshes lead to flickering) diff --git a/resources/lib/state.py b/resources/lib/state.py index 8434b86b..e7378f19 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -34,6 +34,19 @@ RUN_LIB_SCAN = None SYNC_DIALOG = True # Have we already checked the Kodi DB on consistency? KODI_DB_CHECKED = False +# Is synching of Plex music enabled? +ENABLE_MUSIC = False +# How often shall we sync? +FULL_SYNC_INTERVALL = 0 +# Background Sync enabled at all? +BACKGROUND_SYNC = True +# How long shall we wait with synching a new item to make sure Plex got all +# metadata? +BACKGROUNDSYNC_SAFTYMARGIN = 0 +# How many threads to download Plex metadata on sync? +SYNC_THREAD_NUMBER = 0 +# What's the time offset between the PMS and Kodi? +KODI_PLEX_TIME_OFFSET = 0.0 # Path remapping mechanism (e.g. smb paths) # Do we replace \\myserver\path to smb://myserver/path? diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 962fa79a..4325eb78 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -267,7 +267,7 @@ def DateToKodi(stamp): None if an error was encountered """ try: - stamp = float(stamp) + float(window('kodiplextimeoffset')) + stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET date_time = localtime(stamp) localdate = strftime('%Y-%m-%d %H:%M:%S', date_time) except: diff --git a/service.py b/service.py index 0d2daf3a..dd845e81 100644 --- a/service.py +++ b/service.py @@ -113,7 +113,7 @@ class Service(): "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", - "kodiplextimeoffset", "countError", "countUnauthorized", + "countError", "countUnauthorized", "plex_restricteduser", "plex_allows_mediaDeletion", "plex_command", "plex_result", "plex_force_transcode_pix" ] From ee02d5c9f46c57769f97eafd6f052851b090fbfe Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 1 Sep 2017 12:19:27 +0200 Subject: [PATCH 044/509] Increase logging --- resources/lib/kodimonitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index eea0ae9c..71a333f7 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -86,7 +86,7 @@ class KodiMonitor(Monitor): # Reset the window variables from the settings variables for settings_value, window_value in WINDOW_SETTINGS.iteritems(): if window(window_value) != settings(settings_value): - log.debug('PKC settings changed: %s is now %s' + log.debug('PKC window settings changed: %s is now %s' % (settings_value, settings(settings_value))) window(window_value, value=settings(settings_value)) if settings_value == 'fetch_pms_item_number': @@ -100,7 +100,7 @@ class KodiMonitor(Monitor): elif new == 'false': new = False if state_value != new: - log.debug('PKC settings changed: %s is now %s' + log.debug('PKC state settings changed: %s is now %s' % (settings_value, new)) state_value = new # Special cases, overwrite all internal settings From e7de0f921825ea723f139013aa5b67f08f4a57a5 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 1 Sep 2017 12:28:29 +0200 Subject: [PATCH 045/509] Adjust initial states --- resources/lib/state.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/state.py b/resources/lib/state.py index e7378f19..404fbd17 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -54,14 +54,14 @@ REPLACE_SMB_PATH = False # Do we generally remap? REMAP_PATH = False # Mappings for REMAP_PATH: -remapSMBmovieOrg = None -remapSMBmovieNew = None -remapSMBtvOrg = None -remapSMBtvNew = None -remapSMBmusicOrg = None -remapSMBmusicNew = None -remapSMBphotoOrg = None -remapSMBphotoNew = None +remapSMBmovieOrg = '' +remapSMBmovieNew = 'smb://' +remapSMBtvOrg = '' +remapSMBtvNew = 'smb://' +remapSMBmusicOrg = '' +remapSMBmusicNew = 'smb://' +remapSMBphotoOrg = '' +remapSMBphotoNew = 'smb://' # Along with window('plex_authenticated') AUTHENTICATED = False From 430b10ec1ca8abd9a5dee6bcc781649613fab46d Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 1 Sep 2017 12:31:58 +0200 Subject: [PATCH 046/509] Increase logging --- resources/lib/kodimonitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 71a333f7..cbec8087 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -100,8 +100,8 @@ class KodiMonitor(Monitor): elif new == 'false': new = False if state_value != new: - log.debug('PKC state settings changed: %s is now %s' - % (settings_value, new)) + log.debug('PKC state settings %s changed from %s to %s' + % (settings_value, state_value, new)) state_value = new # Special cases, overwrite all internal settings state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60 From 6268768a4b378b0fe0ed508f255eadb660d06d81 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 12:03:04 +0200 Subject: [PATCH 047/509] Version bump --- README.md | 4 ++-- addon.xml | 7 +++++-- changelog.txt | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e508c3ca..e84a35bd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.9-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.10-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.11-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.11-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 8562b5b7..3555ea9e 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,10 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.10 (beta only): + version 1.8.11: +- version 1.8.10 for everybody + +version 1.8.10 (beta only): - Vastly improve sync speed for music - Never show library sync dialog if media is playing - Improvements to sync dialog diff --git a/changelog.txt b/changelog.txt index b7c8181a..f19679ed 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 1.8.11: +- version 1.8.10 for everybody + version 1.8.10 (beta only): - Vastly improve sync speed for music - Never show library sync dialog if media is playing From ff1eb674b30309b608d0fadf7818aeb5298133f4 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 12:44:03 +0200 Subject: [PATCH 048/509] Revert "Adjust initial states" This reverts commit e7de0f921825ea723f139013aa5b67f08f4a57a5. --- resources/lib/state.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/state.py b/resources/lib/state.py index 404fbd17..e7378f19 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -54,14 +54,14 @@ REPLACE_SMB_PATH = False # Do we generally remap? REMAP_PATH = False # Mappings for REMAP_PATH: -remapSMBmovieOrg = '' -remapSMBmovieNew = 'smb://' -remapSMBtvOrg = '' -remapSMBtvNew = 'smb://' -remapSMBmusicOrg = '' -remapSMBmusicNew = 'smb://' -remapSMBphotoOrg = '' -remapSMBphotoNew = 'smb://' +remapSMBmovieOrg = None +remapSMBmovieNew = None +remapSMBtvOrg = None +remapSMBtvNew = None +remapSMBmusicOrg = None +remapSMBmusicNew = None +remapSMBphotoOrg = None +remapSMBphotoNew = None # Along with window('plex_authenticated') AUTHENTICATED = False From 1a91149b5f2e8af075c0ba0a956c6917a8fce006 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 12:46:41 +0200 Subject: [PATCH 049/509] Optimize code --- resources/lib/kodimonitor.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index cbec8087..c594d065 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -2,24 +2,23 @@ ############################################################################### -import logging +from logging import getLogger from json import loads from xbmc import Monitor, Player, sleep -import downloadutils +from downloadutils import DownloadUtils import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ plex_command from PlexFunctions import scrobble from kodidb_functions import get_kodiid_from_filename from PlexAPI import API -from variables import REMAP_TYPE_FROM_PLEXTYPE import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) # settings: window-variable WINDOW_SETTINGS = { @@ -30,11 +29,6 @@ WINDOW_SETTINGS = { 'force_transcode_pix': 'plex_force_transcode_pix', 'fetch_pms_item_number': 'fetch_pms_item_number' } -# Path replacement -for typus in REMAP_TYPE_FROM_PLEXTYPE.values(): - for arg in ('Org', 'New'): - key = 'remapSMB%s%s' % (typus, arg) - WINDOW_SETTINGS[key] = key # settings: state-variable (state.py) STATE_SETTINGS = { @@ -58,7 +52,7 @@ class KodiMonitor(Monitor): def __init__(self, callback): self.mgr = callback - self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.doUtils = DownloadUtils().downloadUrl self.xbmcplayer = Player() self.playqueue = self.mgr.playqueue Monitor.__init__(self) From 882c592e4535aa93f17858a96fa85d06b4589290 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 13:23:18 +0200 Subject: [PATCH 050/509] Fix detecting changes to PKC settings --- resources/lib/kodimonitor.py | 34 +++++++++++++++++----------------- resources/lib/state.py | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c594d065..c86d1ff8 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -25,25 +25,25 @@ WINDOW_SETTINGS = { 'logLevel': 'plex_logLevel', 'enableContext': 'plex_context', 'plex_restricteduser': 'plex_restricteduser', - 'replaceSMB': 'replaceSMB', 'force_transcode_pix': 'plex_force_transcode_pix', 'fetch_pms_item_number': 'fetch_pms_item_number' } # settings: state-variable (state.py) +# Need to use getattr and setattr! STATE_SETTINGS = { - 'dbSyncIndicator': state.SYNC_DIALOG, - 'remapSMB': state.REMAP_PATH, - 'remapSMBmovieOrg': state.remapSMBmovieOrg, - 'remapSMBmovieNew': state.remapSMBmovieNew, - 'remapSMBtvOrg': state.remapSMBtvOrg, - 'remapSMBtvNew': state.remapSMBtvNew, - 'remapSMBmusicOrg': state.remapSMBmusicOrg, - 'remapSMBmusicNew': state.remapSMBmusicNew, - 'remapSMBphotoOrg': state.remapSMBphotoOrg, - 'remapSMBphotoNew': state.remapSMBphotoNew, - 'enableMusic': state.ENABLE_MUSIC, - 'enableBackgroundSync': state.BACKGROUND_SYNC + 'dbSyncIndicator': 'SYNC_DIALOG', + 'remapSMB': 'REMAP_PATH', + 'remapSMBmovieOrg': 'remapSMBmovieOrg', + 'remapSMBmovieNew': 'remapSMBmovieNew', + 'remapSMBtvOrg': 'remapSMBtvOrg', + 'remapSMBtvNew': 'remapSMBtvNew', + 'remapSMBmusicOrg': 'remapSMBmusicOrg', + 'remapSMBmusicNew': 'remapSMBmusicNew', + 'remapSMBphotoOrg': 'remapSMBphotoOrg', + 'remapSMBphotoNew': 'remapSMBphotoNew', + 'enableMusic': 'ENABLE_MUSIC', + 'enableBackgroundSync': 'BACKGROUND_SYNC' } ############################################################################### @@ -87,16 +87,16 @@ class KodiMonitor(Monitor): log.info('Requesting playlist/nodes refresh') plex_command('RUN_LIB_SCAN', 'views') # Reset the state variables in state.py - for settings_value, state_value in STATE_SETTINGS.iteritems(): + for settings_value, state_name in STATE_SETTINGS.iteritems(): new = settings(settings_value) if new == 'true': new = True elif new == 'false': new = False - if state_value != new: + if getattr(state, state_name) != new: log.debug('PKC state settings %s changed from %s to %s' - % (settings_value, state_value, new)) - state_value = new + % (settings_value, getattr(state, state_name), new)) + setattr(state, state_name, new) # Special cases, overwrite all internal settings state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( diff --git a/resources/lib/state.py b/resources/lib/state.py index e7378f19..97da71c9 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -35,7 +35,7 @@ SYNC_DIALOG = True # Have we already checked the Kodi DB on consistency? KODI_DB_CHECKED = False # Is synching of Plex music enabled? -ENABLE_MUSIC = False +ENABLE_MUSIC = True # How often shall we sync? FULL_SYNC_INTERVALL = 0 # Background Sync enabled at all? From d4bb8eed840f1f0eda318c519c0115974d61e6b4 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 13:28:40 +0200 Subject: [PATCH 051/509] Fix resuming interrupted sync --- resources/lib/kodimonitor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c86d1ff8..6e6aa9ca 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -73,13 +73,11 @@ class KodiMonitor(Monitor): Monitor the PKC settings for changes made by the user """ log.debug('PKC settings change detected') - # Assume that the user changed the settings so that we can now find the - # path to all media files - state.STOP_SYNC = False - state.PATH_VERIFIED = False + changed = False # Reset the window variables from the settings variables for settings_value, window_value in WINDOW_SETTINGS.iteritems(): if window(window_value) != settings(settings_value): + changed = True log.debug('PKC window settings changed: %s is now %s' % (settings_value, settings(settings_value))) window(window_value, value=settings(settings_value)) @@ -94,6 +92,7 @@ class KodiMonitor(Monitor): elif new == 'false': new = False if getattr(state, state_name) != new: + changed = True log.debug('PKC state settings %s changed from %s to %s' % (settings_value, getattr(state, state_name), new)) setattr(state, state_name, new) @@ -104,6 +103,11 @@ class KodiMonitor(Monitor): state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) # Never set through the user # state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) + if changed is True: + # Assume that the user changed the settings so that we can now find + # the path to all media files + state.STOP_SYNC = False + state.PATH_VERIFIED = False @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): From 41b44930726452d007ef8cf0251f45ad51d35083 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 3 Sep 2017 13:30:50 +0200 Subject: [PATCH 052/509] Sleep longer --- resources/lib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 4325eb78..3c24a36f 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -86,7 +86,7 @@ def plex_command(key, value): value: either 'True' or 'False' """ while window('plex_command'): - xbmc.sleep(5) + xbmc.sleep(20) window('plex_command', value='%s-%s' % (key, value)) From 32c43855f7fd49081986863493142ae83d4cc8e8 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 13:31:15 +0200 Subject: [PATCH 053/509] PKC logging now uses Kodi log levels --- resources/lib/loghandler.py | 71 ++++++++----------------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/resources/lib/loghandler.py b/resources/lib/loghandler.py index c4e34188..9aebb3e9 100644 --- a/resources/lib/loghandler.py +++ b/resources/lib/loghandler.py @@ -1,74 +1,33 @@ # -*- coding: utf-8 -*- - -################################################################################################## - +############################################################################### import logging import xbmc -from utils import window, tryEncode - -################################################################################################## +from utils import tryEncode +############################################################################### +LEVELS = { + logging.ERROR: xbmc.LOGERROR, + logging.WARNING: xbmc.LOGWARNING, + logging.INFO: xbmc.LOGNOTICE, + logging.DEBUG: xbmc.LOGDEBUG +} +############################################################################### def config(): - logger = logging.getLogger('PLEX') logger.addHandler(LogHandler()) logger.setLevel(logging.DEBUG) class LogHandler(logging.StreamHandler): - def __init__(self): - logging.StreamHandler.__init__(self) - self.setFormatter(MyFormatter()) + self.setFormatter(logging.Formatter(fmt="%(name)s: %(message)s")) def emit(self, record): - - if self._get_log_level(record.levelno): - try: - xbmc.log(self.format(record), level=xbmc.LOGNOTICE) - except UnicodeEncodeError: - xbmc.log(tryEncode(self.format(record)), level=xbmc.LOGNOTICE) - - @classmethod - def _get_log_level(cls, level): - - levels = { - logging.ERROR: 0, - logging.WARNING: 0, - logging.INFO: 1, - logging.DEBUG: 2 - } try: - log_level = int(window('plex_logLevel')) - except ValueError: - log_level = 0 - - return log_level >= levels[level] - - -class MyFormatter(logging.Formatter): - - def __init__(self, fmt="%(name)s -> %(message)s"): - - logging.Formatter.__init__(self, fmt) - - def format(self, record): - - # Save the original format configured by the user - # when the logger formatter was instantiated - format_orig = self._fmt - - # Replace the original format with one customized by logging level - if record.levelno in (logging.DEBUG, logging.ERROR): - self._fmt = '%(name)s -> %(levelname)s: %(message)s' - - # Call the original formatter class to do the grunt work - result = logging.Formatter.format(self, record) - - # Restore the original format configured by the user - self._fmt = format_orig - - return result + xbmc.log(self.format(record), level=LEVELS[record.levelno]) + except UnicodeEncodeError: + xbmc.log(tryEncode(self.format(record)), + level=LEVELS[record.levelno]) From b0c62be75f83b2c4ab71e1a364bf55eca6d9bca0 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 13:39:44 +0200 Subject: [PATCH 054/509] Adjust log levels --- resources/lib/clientinfo.py | 6 +++--- resources/lib/downloadutils.py | 26 +++++++++++++------------- service.py | 24 ++++++++++++------------ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 91cb6b91..dfddae5f 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -68,13 +68,13 @@ def getDeviceId(reset=False): # Because Kodi appears to cache file settings!! if clientId != "" and reset is False: window('plex_client_Id', value=clientId) - log.warn("Unique device Id plex_client_Id loaded: %s" % clientId) + log.info("Unique device Id plex_client_Id loaded: %s" % clientId) return clientId - log.warn("Generating a new deviceid.") + log.info("Generating a new deviceid.") from uuid import uuid4 clientId = str(uuid4()) settings('plex_client_Id', value=clientId) window('plex_client_Id', value=clientId) - log.warn("Unique device Id plex_client_Id loaded: %s" % clientId) + log.info("Unique device Id plex_client_Id loaded: %s" % clientId) return clientId diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index a30ab4d9..53eb8d84 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -198,12 +198,12 @@ class DownloadUtils(): # THE EXCEPTIONS except requests.exceptions.ConnectionError as e: # Connection error - log.debug("Server unreachable at: %s" % url) - log.debug(e) + log.warn("Server unreachable at: %s" % url) + log.warn(e) except requests.exceptions.Timeout as e: - log.debug("Server timeout at: %s" % url) - log.debug(e) + log.warn("Server timeout at: %s" % url) + log.warn(e) except requests.exceptions.HTTPError as e: log.warn('HTTP Error at %s' % url) @@ -300,21 +300,21 @@ class DownloadUtils(): # update pass else: - log.error("Unable to convert the response for: " - "%s" % url) - log.info("Received headers were: %s" % r.headers) - log.info('Received text:') - log.info(r.text) + log.warn("Unable to convert the response for: " + "%s" % url) + log.warn("Received headers were: %s" % r.headers) + log.warn('Received text:') + log.warn(r.text) return True elif r.status_code == 403: # E.g. deleting a PMS item - log.error('PMS sent 403: Forbidden error for url %s' % url) + log.warn('PMS sent 403: Forbidden error for url %s' % url) return None else: - log.error('Unknown answer from PMS %s with status code %s. ' - 'Message:' % (url, r.status_code)) + log.warn('Unknown answer from PMS %s with status code %s. ' + 'Message:' % (url, r.status_code)) r.encoding = 'utf-8' - log.info(r.text) + log.warn(r.text) return True # And now deal with the consequences of the exceptions diff --git a/service.py b/service.py index dd845e81..b2b89407 100644 --- a/service.py +++ b/service.py @@ -93,16 +93,16 @@ class Service(): value=settings('fetch_pms_item_number')) # Initial logging - log.warn("======== START %s ========" % v.ADDON_NAME) - log.warn("Platform: %s" % v.PLATFORM) - log.warn("KODI Version: %s" % v.KODILONGVERSION) - log.warn("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION)) - log.warn("Using plugin paths: %s" + log.info("======== START %s ========" % v.ADDON_NAME) + log.info("Platform: %s" % v.PLATFORM) + log.info("KODI Version: %s" % v.KODILONGVERSION) + log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION)) + log.info("Using plugin paths: %s" % (settings('useDirectPaths') != "true")) - log.warn("Number of sync threads: %s" + log.info("Number of sync threads: %s" % settings('syncThreadNumber')) - log.warn("Log Level: %s" % logLevel) - log.warn("Full sys.argv received: %s" % argv) + log.info("Log Level: %s" % logLevel) + log.info("Full sys.argv received: %s" % argv) # Reset window props for profile switch properties = [ @@ -172,7 +172,7 @@ class Service(): if window('plex_kodiProfile') != kodiProfile: # Profile change happened, terminate this thread and others - log.warn("Kodi profile was: %s and changed to: %s. " + log.info("Kodi profile was: %s and changed to: %s. " "Terminating old PlexKodiConnect thread." % (kodiProfile, window('plex_kodiProfile'))) @@ -331,7 +331,7 @@ class Service(): except: pass window('plex_service_started', clear=True) - log.warn("======== STOP %s ========" % v.ADDON_NAME) + log.info("======== STOP %s ========" % v.ADDON_NAME) # Safety net - Kody starts PKC twice upon first installation! @@ -344,11 +344,11 @@ else: # Delay option delay = int(settings('startupDelay')) -log.warn("Delaying Plex startup by: %s sec..." % delay) +log.info("Delaying Plex startup by: %s sec..." % delay) if exit: log.error('PKC service.py already started - exiting this instance') elif delay and Monitor().waitForAbort(delay): # Start the service - log.warn("Abort requested while waiting. PKC not started.") + log.info("Abort requested while waiting. PKC not started.") else: Service().ServiceEntryPoint() From b555df1061636791f262127ceee53db4f65bed22 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 13:43:52 +0200 Subject: [PATCH 055/509] Remove obsolete log level code --- resources/lib/kodimonitor.py | 1 - resources/settings.xml | 1 - service.py | 10 ---------- 3 files changed, 12 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6e6aa9ca..27db9a73 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -22,7 +22,6 @@ log = getLogger("PLEX."+__name__) # settings: window-variable WINDOW_SETTINGS = { - 'logLevel': 'plex_logLevel', 'enableContext': 'plex_context', 'plex_restricteduser': 'plex_restricteduser', 'force_transcode_pix': 'plex_force_transcode_pix', diff --git a/resources/settings.xml b/resources/settings.xml index 52865be1..bfe2d62a 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -146,7 +146,6 @@ - diff --git a/service.py b/service.py index b2b89407..6b8f464f 100644 --- a/service.py +++ b/service.py @@ -81,10 +81,8 @@ class Service(): def __init__(self): - logLevel = self.getLogLevel() self.monitor = Monitor() - window('plex_logLevel', value=str(logLevel)) window('plex_kodiProfile', value=tryDecode(translatePath("special://profile"))) window('plex_context', @@ -101,7 +99,6 @@ class Service(): % (settings('useDirectPaths') != "true")) log.info("Number of sync threads: %s" % settings('syncThreadNumber')) - log.info("Log Level: %s" % logLevel) log.info("Full sys.argv received: %s" % argv) # Reset window props for profile switch @@ -126,13 +123,6 @@ class Service(): # Set the minimum database version window('plex_minDBVersion', value="1.5.10") - def getLogLevel(self): - try: - logLevel = int(settings('logLevel')) - except ValueError: - logLevel = 0 - return logLevel - def __stop_PKC(self): """ Kodi's abortRequested is really unreliable :-( From 6dfed36dbe8b32c2c6686cec28b33dfd170e6099 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 13:52:55 +0200 Subject: [PATCH 056/509] Fix logging NameError --- contextmenu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index df6de3fd..304032ce 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -46,7 +46,7 @@ if __name__ == "__main__": # Start the context menu ContextMenu() except Exception as error: - log.exception(error) + log.error(error) import traceback - log.exception("Traceback:\n%s" % traceback.format_exc()) + log.error("Traceback:\n%s" % traceback.format_exc()) raise From 81084ea47954422984222a48340174964fb90359 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 14:14:42 +0200 Subject: [PATCH 057/509] Increase logging for websockets --- resources/lib/websocket_client.py | 63 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 0ae40da8..c7d929c4 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -84,7 +84,8 @@ class WebSocket(Thread): # No worries if read timed out pass except websocket.WebSocketConnectionClosedException: - log.info("Connection closed, (re)connecting") + log.info("%s: connection closed, (re)connecting" + % self.__class__.__name__) uri, sslopt = self.getUri() try: # Low timeout - let's us shut this thread down! @@ -95,7 +96,7 @@ class WebSocket(Thread): enable_multithread=True) except IOError: # Server is probably offline - log.info("Error connecting") + log.info("%s: Error connecting" % self.__class__.__name__) self.ws = None counter += 1 if counter > 3: @@ -103,33 +104,41 @@ class WebSocket(Thread): self.IOError_response() sleep(1000) except websocket.WebSocketTimeoutException: - log.info("timeout while connecting, trying again") + log.info("%s: Timeout while connecting, trying again" + % self.__class__.__name__) self.ws = None sleep(1000) except websocket.WebSocketException as e: - log.info('WebSocketException: %s' % e) + log.info('%s: WebSocketException: %s' + % (self.__class__.__name__, e)) if 'Handshake Status 401' in e.args: handshake_counter += 1 if handshake_counter >= 5: - log.info('Error in handshake detected. Stopping ' - '%s now' % self.__class__.__name__) + log.info('%s: Error in handshake detected. ' + 'Stopping now' + % self.__class__.__name__) break self.ws = None sleep(1000) except Exception as e: - log.error("Unknown exception encountered in connecting: %s" - % e) + log.error('%s: Unknown exception encountered when ' + 'connecting: %s' % (self.__class__.__name__, e)) import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + log.error("%s: Traceback:\n%s" + % (self.__class__.__name__, + traceback.format_exc())) self.ws = None sleep(1000) else: counter = 0 handshake_counter = 0 except Exception as e: - log.error("Unknown exception encountered: %s" % e) + log.error("%s: Unknown exception encountered: %s" + % (self.__class__.__name__, e)) import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + log.error("%s: Traceback:\n%s" + % (self.__class__.__name__, + traceback.format_exc())) try: self.ws.shutdown() except: @@ -171,7 +180,8 @@ class PMS_Websocket(WebSocket): sslopt = {} if settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE - log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) + log.debug("%s: Uri: %s, sslopt: %s" + % (self.__class__.__name__, uri, sslopt)) return uri, sslopt def process(self, opcode, message): @@ -181,20 +191,24 @@ class PMS_Websocket(WebSocket): try: message = loads(message) except Exception as ex: - log.error('Error decoding message from websocket: %s' % ex) + log.error('%s: Error decoding message from websocket: %s' + % (self.__class__.__name__, ex)) log.error(message) return False try: message = message['NotificationContainer'] except KeyError: - log.error('Could not parse PMS message: %s' % message) + log.error('%s: Could not parse PMS message: %s' + % (self.__class__.__name__, message)) return False # Triage typus = message.get('type') if typus is None: - log.error('No message type, dropping message: %s' % message) + log.error('%s: No message type, dropping message: %s' + % (self.__class__.__name__, message)) return False - log.debug('Received message from PMS server: %s' % message) + log.debug('%s: Received message from PMS server: %s' + % (self.__class__.__name__, message)) # Drop everything we're not interested in if typus not in ('playing', 'timeline'): return True @@ -224,27 +238,32 @@ class Alexa_Websocket(WebSocket): % (state.PLEX_USER_ID, self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} - log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) + log.debug("%s: Uri: %s, sslopt: %s" + % (self.__class__.__name__, uri, sslopt)) return uri, sslopt def process(self, opcode, message): if opcode not in self.opcode_data: return False - log.debug('Received the following message from Alexa:') - log.debug(message) + log.debug('%s: Received the following message from Alexa:' + % self.__class__.__name__) + log.debug('%s: %s' % (self.__class__.__name__, message)) try: message = etree.fromstring(message) except Exception as ex: - log.error('Error decoding message from Alexa: %s' % ex) + log.error('%s: Error decoding message from Alexa: %s' + % (self.__class__.__name__, ex)) return False try: if message.attrib['command'] == 'processRemoteControlCommand': message = message[0] else: - log.error('Unknown Alexa message received') + log.error('%s: Unknown Alexa message received' + % self.__class__.__name__) return False except: - log.error('Could not parse Alexa message') + log.error('%s: Could not parse Alexa message' + % self.__class__.__name__) return False process_command(message.attrib['path'][1:], message.attrib, From da4be6d7e4a98afa274898b8d484b6ffa0d106d1 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 19:24:26 +0200 Subject: [PATCH 058/509] Fix changed Plex metadata not synced repeatedly --- resources/lib/librarysync.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index bc4b34bb..bb7a5296 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -44,11 +44,6 @@ class LibrarySync(Thread): def __init__(self, callback=None): self.mgr = callback - # Dict of items we just processed in order to prevent a reprocessing - # caused by websocket - self.just_processed = {} - # How long do we wait until we start re-processing? (in seconds) - self.ignore_just_processed = 10*60 self.itemsToProcess = [] self.sessionKeys = [] self.fanartqueue = Queue.Queue() @@ -253,9 +248,6 @@ class LibrarySync(Thread): # True: we're syncing only the delta, e.g. different checksum self.compare = not repair - # Empty our list of item's we've just processed in the past - self.just_processed = {} - self.new_items_only = True # This will also update playstates and userratings! log.info('Running fullsync for NEW PMS items with repair=%s' % repair) @@ -619,7 +611,6 @@ class LibrarySync(Thread): self.allPlexElementsId APPENDED(!!) dict = {itemid: checksum} """ - now = getUnixTimestamp() if self.new_items_only is True: # Only process Plex items that Kodi does not already have in lib for item in xml: @@ -627,8 +618,8 @@ class LibrarySync(Thread): if not itemId: # Skipping items 'title=All episodes' without a 'ratingKey' continue - self.allPlexElementsId[itemId] = ("K%s%s" % - (itemId, item.attrib.get('updatedAt', ''))) + self.allPlexElementsId[itemId] = "K%s%s" % \ + (itemId, item.attrib.get('updatedAt', '')) if itemId not in self.allKodiElementsId: self.updatelist.append({ 'itemId': itemId, @@ -640,10 +631,8 @@ class LibrarySync(Thread): 'mediaType': item.attrib.get('type'), 'get_children': get_children }) - self.just_processed[itemId] = now return - - if self.compare: + elif self.compare: # Only process the delta - new or changed items for item in xml: itemId = item.attrib.get('ratingKey') @@ -667,7 +656,6 @@ class LibrarySync(Thread): 'mediaType': item.attrib.get('type'), 'get_children': get_children }) - self.just_processed[itemId] = now else: # Initial or repair sync: get all Plex movies for item in xml: @@ -675,8 +663,8 @@ class LibrarySync(Thread): if not itemId: # Skipping items 'title=All episodes' without a 'ratingKey' continue - self.allPlexElementsId[itemId] = ("K%s%s" - % (itemId, item.attrib.get('updatedAt', ''))) + self.allPlexElementsId[itemId] = "K%s%s" \ + % (itemId, item.attrib.get('updatedAt', '')) self.updatelist.append({ 'itemId': itemId, 'itemType': itemType, @@ -687,7 +675,6 @@ class LibrarySync(Thread): 'mediaType': item.attrib.get('type'), 'get_children': get_children }) - self.just_processed[itemId] = now def GetAndProcessXMLs(self, itemType): """ @@ -1146,8 +1133,6 @@ class LibrarySync(Thread): continue else: successful = self.process_newitems(item) - if successful: - self.just_processed[str(item['ratingKey'])] = now if successful and settings('FanartTV') == 'true': plex_type = v.PLEX_TYPE_FROM_WEBSOCKET[item['type']] if plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): @@ -1247,14 +1232,6 @@ class LibrarySync(Thread): if plex_id == '0': log.error('Received malformed PMS message: %s' % item) continue - try: - if (now - self.just_processed[plex_id] < - self.ignore_just_processed and status != 9): - log.debug('We just processed %s: ignoring' % plex_id) - continue - except KeyError: - # Item has NOT just been processed - pass # Have we already added this element? for existingItem in self.itemsToProcess: if existingItem['ratingKey'] == plex_id: From 3ada7d1a98222a2dc21b39009ad57d025fb8f4c6 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 19:26:48 +0200 Subject: [PATCH 059/509] More specific exception handling --- resources/lib/websocket_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index c7d929c4..ccebfb7b 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -190,9 +190,9 @@ class PMS_Websocket(WebSocket): try: message = loads(message) - except Exception as ex: - log.error('%s: Error decoding message from websocket: %s' - % (self.__class__.__name__, ex)) + except ValueError: + log.error('%s: Error decoding message from websocket' + % self.__class__.__name__) log.error(message) return False try: From fc03ebc8d4ee9730cce0672fd45c20aca7275b17 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 19:30:19 +0200 Subject: [PATCH 060/509] Remove obsolete timestamp --- resources/lib/librarysync.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index bb7a5296..205c7e32 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1217,7 +1217,6 @@ class LibrarySync(Thread): PMS is messing with the library items, e.g. new or changed. Put in our "processing queue" for later """ - now = getUnixTimestamp() for item in data: if 'tv.plex' in item.get('identifier', ''): # Ommit Plex DVR messages - the Plex IDs are not corresponding From 5fcccba10523d59a81a64113192acf35f950ad30 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Sep 2017 19:55:27 +0200 Subject: [PATCH 061/509] Compile regex only once --- resources/lib/PlexFunctions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index e6e9954e..94cfcda5 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -15,7 +15,7 @@ from variables import PLEX_TO_KODI_TIMEFACTOR log = getLogger("PLEX."+__name__) CONTAINERSIZE = int(settings('limitindex')) - +REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''') ############################################################################### @@ -36,9 +36,8 @@ def GetPlexKeyNumber(plexKey): Returns ('','') if nothing is found """ - regex = re.compile(r'''/(.+)/(\d+)$''') try: - result = regex.findall(plexKey)[0] + result = REGEX_PLEX_KEY.findall(plexKey)[0] except IndexError: result = ('', '') return result From 274ed4b43086a5f9d7e2c72998086e2a22facc2e Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:06:31 +0200 Subject: [PATCH 062/509] Background sync now picks up more PMS changes --- resources/lib/librarysync.py | 278 ++++++++++++++++++------------ resources/lib/websocket_client.py | 33 ++-- 2 files changed, 182 insertions(+), 129 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 205c7e32..a9152cc0 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -21,7 +21,8 @@ import videonodes import variables as v from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \ - GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus, get_plex_sections + GetPlexSectionResults, GetPlexKeyNumber, GetPMSStatus, get_plex_sections, \ + GetAllPlexChildren import PlexAPI from library_sync.get_metadata import Threaded_Get_Metadata from library_sync.process_metadata import Threaded_Process_Metadata @@ -1075,11 +1076,24 @@ class LibrarySync(Thread): processes json.loads() messages from websocket. Triage what we need to do with "process_" methods """ - typus = message.get('type') - if typus == 'playing': - self.process_playing(message['PlaySessionStateNotification']) - elif typus == 'timeline': - self.process_timeline(message['TimelineEntry']) + if message['type'] == 'playing': + try: + self.process_playing(message['PlaySessionStateNotification']) + except KeyError: + log.error('Received invalid PMS message for playstate: %s' + % message) + elif message['type'] == 'timeline': + try: + self.process_timeline(message['TimelineEntry']) + except (KeyError, ValueError): + log.error('Received invalid PMS message for timeline: %s' + % message) + elif message['type'] == 'activity': + try: + self.process_activity(message['ActivityNotification']) + except KeyError: + log.error('Received invalid PMS message for activity: %s' + % message) def multi_delete(self, liste, deleteListe): """ @@ -1134,11 +1148,10 @@ class LibrarySync(Thread): else: successful = self.process_newitems(item) if successful and settings('FanartTV') == 'true': - plex_type = v.PLEX_TYPE_FROM_WEBSOCKET[item['type']] - if plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): + if item['type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): self.fanartqueue.put({ 'plex_id': item['ratingKey'], - 'plex_type': plex_type, + 'plex_type': item['type'], 'refresh': False }) if successful is True: @@ -1194,22 +1207,25 @@ class LibrarySync(Thread): return True def process_deleteditems(self, item): - if item.get('type') == 1: - log.debug("Removing movie %s" % item.get('ratingKey')) + if item['type'] == v.PLEX_TYPE_MOVIE: + log.debug("Removing movie %s" % item['ratingKey']) self.videoLibUpdate = True with itemtypes.Movies() as movie: - movie.remove(item.get('ratingKey')) - elif item.get('type') in (2, 3, 4): - log.debug("Removing episode/season/tv show %s" - % item.get('ratingKey')) + movie.remove(item['ratingKey']) + elif item['type'] in (v.PLEX_TYPE_SHOW, + v.PLEX_TYPE_SEASON, + v.PLEX_TYPE_EPISODE): + log.debug("Removing episode/season/tv show %s" % item['ratingKey']) self.videoLibUpdate = True with itemtypes.TVShows() as show: - show.remove(item.get('ratingKey')) - elif item.get('type') in (8, 9, 10): - log.debug("Removing song/album/artist %s" % item.get('ratingKey')) + show.remove(item['ratingKey']) + elif item['type'] in (v.PLEX_TYPE_ARTIST, + v.PLEX_TYPE_ALBUM, + v.PLEX_TYPE_SONG): + log.debug("Removing song/album/artist %s" % item['ratingKey']) self.musicLibUpdate = True with itemtypes.Music() as music: - music.remove(item.get('ratingKey')) + music.remove(item['ratingKey']) return True def process_timeline(self, data): @@ -1223,14 +1239,13 @@ class LibrarySync(Thread): # (DVR ratingKeys are not unique and might correspond to a # movie or episode) continue - typus = int(item.get('type', 0)) - status = int(item.get('state', 0)) - if status == 9 or (typus in (1, 4, 10) and status == 5): + typus = v.PLEX_TYPE_FROM_WEBSOCKET[int(item['type'])] + status = int(item['state']) + if status == 9 or (typus in (v.PLEX_TYPE_MOVIE, + v.PLEX_TYPE_EPISODE, + v.PLEX_TYPE_SONG) and status == 5): # Only process deleted items OR movies, episodes, tracks/songs - plex_id = str(item.get('itemID', '0')) - if plex_id == '0': - log.error('Received malformed PMS message: %s' % item) - continue + plex_id = str(item['itemID']) # Have we already added this element? for existingItem in self.itemsToProcess: if existingItem['ratingKey'] == plex_id: @@ -1245,101 +1260,136 @@ class LibrarySync(Thread): 'attempt': 0 }) + def process_activity(self, data): + """ + PMS is re-scanning an item, e.g. after having changed a movie poster. + WATCH OUT for this if it's triggered by our PKC library scan! + """ + for item in data: + if item['event'] != 'ended': + # Scan still going on, so skip for now + continue + elif item['Activity']['type'] != 'library.refresh.items': + # Not the type of message relevant for us + continue + plex_id = GetPlexKeyNumber(item['Activity']['Context']['key'])[1] + if plex_id == '': + raise KeyError('Could not extract the Plex id') + # We're only looking at existing elements - have we synced yet? + with plexdb.Get_Plex_DB() as plex_db: + kodi_info = plex_db.getItem_byId(plex_id) + if kodi_info is None: + log.debug('Plex id %s not synced yet - skipping' % plex_id) + continue + # Have we already added this element? + for existingItem in self.itemsToProcess: + if existingItem['ratingKey'] == plex_id: + break + else: + # Haven't added this element to the queue yet + self.itemsToProcess.append({ + 'state': None, # Don't need a state here + 'type': kodi_info[5], + 'ratingKey': plex_id, + 'timestamp': getUnixTimestamp(), + 'attempt': 0 + }) + def process_playing(self, data): """ Someone (not necessarily the user signed in) is playing something some- where """ items = [] - with plexdb.Get_Plex_DB() as plex_db: - for item in data: - # Drop buffering messages immediately - status = item.get('state') - if status == 'buffering': - continue - ratingKey = item.get('ratingKey') - kodiInfo = plex_db.getItem_byId(ratingKey) - if kodiInfo is None: - # Item not (yet) in Kodi library - continue - sessionKey = item.get('sessionKey') - # Do we already have a sessionKey stored? - if sessionKey not in self.sessionKeys: - if settings('plex_serverowned') == 'false': - # Not our PMS, we are not authorized to get the - # sessions - # On the bright side, it must be us playing :-) - self.sessionKeys = { - sessionKey: {} - } - else: - # PMS is ours - get all current sessions - self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) - log.debug('Updated current sessions. They are: %s' - % self.sessionKeys) - if sessionKey not in self.sessionKeys: - log.warn('Session key %s still unknown! Skip ' - 'item' % sessionKey) - continue - - currSess = self.sessionKeys[sessionKey] - if settings('plex_serverowned') != 'false': - # Identify the user - same one as signed on with PKC? Skip - # update if neither session's username nor userid match - # (Owner sometime's returns id '1', not always) - if (not state.PLEX_TOKEN and currSess['userId'] == '1'): - # PKC not signed in to plex.tv. Plus owner of PMS is - # playing (the '1'). - # Hence must be us (since several users require plex.tv - # token for PKC) - pass - elif not (currSess['userId'] == state.PLEX_USER_ID - or - currSess['username'] == state.PLEX_USERNAME): - log.debug('Our username %s, userid %s did not match ' - 'the session username %s with userid %s' - % (state.PLEX_USERNAME, - state.PLEX_USER_ID, - currSess['username'], - currSess['userId'])) - continue - - # Get an up-to-date XML from the PMS - # because PMS will NOT directly tell us: - # duration of item - # viewCount - if currSess.get('duration') is None: - xml = GetPlexMetadata(ratingKey) - if xml in (None, 401): - log.error('Could not get up-to-date xml for item %s' - % ratingKey) - continue - API = PlexAPI.API(xml[0]) - userdata = API.getUserData() - currSess['duration'] = userdata['Runtime'] - currSess['viewCount'] = userdata['PlayCount'] - # Sometimes, Plex tells us resume points in milliseconds and - # not in seconds - thank you very much! - if item.get('viewOffset') > currSess['duration']: - resume = item.get('viewOffset') / 1000 + for item in data: + # Drop buffering messages immediately + status = item['state'] + if status == 'buffering': + continue + ratingKey = str(item['ratingKey']) + with plexdb.Get_Plex_DB() as plex_db: + kodi_info = plex_db.getItem_byId(ratingKey) + if kodi_info is None: + # Item not (yet) in Kodi library + continue + sessionKey = item['sessionKey'] + # Do we already have a sessionKey stored? + if sessionKey not in self.sessionKeys: + if settings('plex_serverowned') == 'false': + # Not our PMS, we are not authorized to get the + # sessions + # On the bright side, it must be us playing :-) + self.sessionKeys = { + sessionKey: {} + } else: - resume = item.get('viewOffset') - # Append to list that we need to process - items.append({ - 'ratingKey': ratingKey, - 'kodi_id': kodiInfo[0], - 'file_id': kodiInfo[1], - 'kodi_type': kodiInfo[4], - 'viewOffset': resume, - 'state': status, - 'duration': currSess['duration'], - 'viewCount': currSess['viewCount'], - 'lastViewedAt': DateToKodi(getUnixTimestamp()) - }) - log.debug('Update playstate for user %s with id %s: %s' - % (state.PLEX_USERNAME, - state.PLEX_USER_ID, - items[-1])) + # PMS is ours - get all current sessions + self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) + log.debug('Updated current sessions. They are: %s' + % self.sessionKeys) + if sessionKey not in self.sessionKeys: + log.warn('Session key %s still unknown! Skip ' + 'item' % sessionKey) + continue + + currSess = self.sessionKeys[sessionKey] + if settings('plex_serverowned') != 'false': + # Identify the user - same one as signed on with PKC? Skip + # update if neither session's username nor userid match + # (Owner sometime's returns id '1', not always) + if (not state.PLEX_TOKEN and currSess['userId'] == '1'): + # PKC not signed in to plex.tv. Plus owner of PMS is + # playing (the '1'). + # Hence must be us (since several users require plex.tv + # token for PKC) + pass + elif not (currSess['userId'] == state.PLEX_USER_ID + or + currSess['username'] == state.PLEX_USERNAME): + log.debug('Our username %s, userid %s did not match ' + 'the session username %s with userid %s' + % (state.PLEX_USERNAME, + state.PLEX_USER_ID, + currSess['username'], + currSess['userId'])) + continue + + # Get an up-to-date XML from the PMS + # because PMS will NOT directly tell us: + # duration of item + # viewCount + if currSess.get('duration') is None: + xml = GetPlexMetadata(ratingKey) + if xml in (None, 401): + log.error('Could not get up-to-date xml for item %s' + % ratingKey) + continue + API = PlexAPI.API(xml[0]) + userdata = API.getUserData() + currSess['duration'] = userdata['Runtime'] + currSess['viewCount'] = userdata['PlayCount'] + # Sometimes, Plex tells us resume points in milliseconds and + # not in seconds - thank you very much! + if item.get('viewOffset') > currSess['duration']: + resume = item.get('viewOffset') / 1000 + else: + resume = item.get('viewOffset') + # Append to list that we need to process + items.append({ + 'ratingKey': ratingKey, + 'kodi_id': kodi_info[0], + 'file_id': kodi_info[1], + 'kodi_type': kodi_info[4], + 'viewOffset': resume, + 'state': status, + 'duration': currSess['duration'], + 'viewCount': currSess['viewCount'], + 'lastViewedAt': DateToKodi(getUnixTimestamp()) + }) + log.debug('Update playstate for user %s with id %s: %s' + % (state.PLEX_USERNAME, + state.PLEX_USER_ID, + items[-1])) # Now tell Kodi where we are for item in items: itemFkt = getattr(itemtypes, diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index ccebfb7b..ba3f97e1 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -186,7 +186,7 @@ class PMS_Websocket(WebSocket): def process(self, opcode, message): if opcode not in self.opcode_data: - return False + return try: message = loads(message) @@ -194,28 +194,32 @@ class PMS_Websocket(WebSocket): log.error('%s: Error decoding message from websocket' % self.__class__.__name__) log.error(message) - return False + return try: message = message['NotificationContainer'] except KeyError: log.error('%s: Could not parse PMS message: %s' % (self.__class__.__name__, message)) - return False + return # Triage typus = message.get('type') if typus is None: log.error('%s: No message type, dropping message: %s' % (self.__class__.__name__, message)) - return False + return log.debug('%s: Received message from PMS server: %s' % (self.__class__.__name__, message)) # Drop everything we're not interested in - if typus not in ('playing', 'timeline'): - return True - - # Put PMS message on queue and let libsync take care of it - self.queue.put(message) - return True + if typus not in ('playing', 'timeline', 'activity'): + return + elif typus == 'activity' and state.DB_SCAN is True: + # Only add to processing if PKC is NOT doing a lib scan (and thus + # possibly causing these reprocessing messages en mass) + log.debug('%s: Dropping message as PKC is currently synching' + % self.__class__.__name__) + else: + # Put PMS message on queue and let libsync take care of it + self.queue.put(message) def IOError_response(self): log.warn("Repeatedly could not connect to PMS, " @@ -244,7 +248,7 @@ class Alexa_Websocket(WebSocket): def process(self, opcode, message): if opcode not in self.opcode_data: - return False + return log.debug('%s: Received the following message from Alexa:' % self.__class__.__name__) log.debug('%s: %s' % (self.__class__.__name__, message)) @@ -253,22 +257,21 @@ class Alexa_Websocket(WebSocket): except Exception as ex: log.error('%s: Error decoding message from Alexa: %s' % (self.__class__.__name__, ex)) - return False + return try: if message.attrib['command'] == 'processRemoteControlCommand': message = message[0] else: log.error('%s: Unknown Alexa message received' % self.__class__.__name__) - return False + return except: log.error('%s: Could not parse Alexa message' % self.__class__.__name__) - return False + return process_command(message.attrib['path'][1:], message.attrib, queue=self.mgr.plexCompanion.queue) - return True def IOError_response(self): pass From 1f0baf5128f1e818e4539c6bc301e10f09501aa1 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:12:29 +0200 Subject: [PATCH 063/509] Ignore PMS message related to an entire library --- resources/lib/librarysync.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index a9152cc0..64e8f28b 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1269,6 +1269,9 @@ class LibrarySync(Thread): if item['event'] != 'ended': # Scan still going on, so skip for now continue + elif item['Activity'].get('Context') is None: + # Not related to any Plex element, but entire library + continue elif item['Activity']['type'] != 'library.refresh.items': # Not the type of message relevant for us continue From 060bc6f1d16d8a4166dba8635f69765e2c444f63 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:34:13 +0200 Subject: [PATCH 064/509] Detect Plex item deletion more reliably --- resources/lib/librarysync.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 64e8f28b..48702328 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1240,13 +1240,25 @@ class LibrarySync(Thread): # movie or episode) continue typus = v.PLEX_TYPE_FROM_WEBSOCKET[int(item['type'])] + if typus == v.PLEX_TYPE_CLIP: + # No need to process extras or trailers + continue status = int(item['state']) - if status == 9 or (typus in (v.PLEX_TYPE_MOVIE, - v.PLEX_TYPE_EPISODE, - v.PLEX_TYPE_SONG) and status == 5): - # Only process deleted items OR movies, episodes, tracks/songs + if status == 9: + # Immediately and always process deletions (as the PMS will + # send additional message with other codes) + self.itemsToProcess.append({ + 'state': status, + 'type': typus, + 'ratingKey': str(item['itemID']), + 'timestamp': getUnixTimestamp(), + 'attempt': 0 + }) + elif typus in (v.PLEX_TYPE_MOVIE, + v.PLEX_TYPE_EPISODE, + v.PLEX_TYPE_SONG) and status == 5: plex_id = str(item['itemID']) - # Have we already added this element? + # Have we already added this element for processing? for existingItem in self.itemsToProcess: if existingItem['ratingKey'] == plex_id: break From 6d4ad61c7be049169d94382860edd8323fe4ed6c Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:36:26 +0200 Subject: [PATCH 065/509] Ignore PMS message related to a bunch of items --- resources/lib/librarysync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 48702328..0c8f2e66 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1289,7 +1289,8 @@ class LibrarySync(Thread): continue plex_id = GetPlexKeyNumber(item['Activity']['Context']['key'])[1] if plex_id == '': - raise KeyError('Could not extract the Plex id') + # Likely a Plex id like /library/metadata/3/children + continue # We're only looking at existing elements - have we synced yet? with plexdb.Get_Plex_DB() as plex_db: kodi_info = plex_db.getItem_byId(plex_id) From b0813177d796784326efac75370f8a60c773d5c5 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:50:03 +0200 Subject: [PATCH 066/509] Update Readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index e84a35bd..e9592042 100644 --- a/README.md +++ b/README.md @@ -105,14 +105,11 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio 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:* 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 issues](https://github.com/croneter/PlexKodiConnect/issues/14) for more details. **Workaround**: use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Set-up-Direct-Paths) instead of addon paths. *Background Sync:* The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them only on full/delta syncs (standard settings is every 60 minutes) - Toggle the viewstate of an item to (un)watched outside of Kodi -- Changing details of an item, e.g. replacing a poster - -However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie. ### Issues being worked on From 826712340b99bcc175e28a5f6dbd2f220a74e03a Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:54:43 +0200 Subject: [PATCH 067/509] Fix library sync crashing trying to display an error - Fixes #340 --- resources/lib/librarysync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index c60a866c..d2ec6ff8 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -108,7 +108,7 @@ class LibrarySync(Thread): dialog('notification', heading='{plex}', message=message, - type='{error}') + icon='{error}') def syncPMStime(self): """ From 65ba59678e1c2578e9b59f2b26d57533ed6042d6 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 12:56:07 +0200 Subject: [PATCH 068/509] Version bump --- README.md | 4 ++-- addon.xml | 7 +++++-- changelog.txt | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e84a35bd..d7c833b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.11-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.11-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.12-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 3555ea9e..b2d15559 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,10 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.11: + version 1.8.12: +- Fix library sync crashing trying to display an error + +version 1.8.11: - version 1.8.10 for everybody version 1.8.10 (beta only): diff --git a/changelog.txt b/changelog.txt index f19679ed..44a0d39e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 1.8.12: +- Fix library sync crashing trying to display an error + version 1.8.11: - version 1.8.10 for everybody From 0c20716c9b08857ea2577c312f93c9da015cf09c Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Sep 2017 13:34:46 +0200 Subject: [PATCH 069/509] Version bump --- README.md | 2 +- addon.xml | 13 +++++++++++-- changelog.txt | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e892184f..927921bc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.12-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.13-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index b2d15559..7331cb12 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,16 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.12: + version 1.8.13 (beta only): +- Background sync now picks up more PMS changes +- Detect Plex item deletion more reliably +- Fix changed Plex metadata not synced repeatedly +- Detect (some, not all) changes to PKC settings and apply them on-the-fly +- Fix resuming interrupted sync +- PKC logging now uses Kodi log levels +- Further code optimizations + +version 1.8.12: - Fix library sync crashing trying to display an error version 1.8.11: diff --git a/changelog.txt b/changelog.txt index 44a0d39e..e335f914 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +version 1.8.13 (beta only): +- Background sync now picks up more PMS changes +- Detect Plex item deletion more reliably +- Fix changed Plex metadata not synced repeatedly +- Detect (some, not all) changes to PKC settings and apply them on-the-fly +- Fix resuming interrupted sync +- PKC logging now uses Kodi log levels +- Further code optimizations + version 1.8.12: - Fix library sync crashing trying to display an error From cb39dbd19d414db95c29e739faf74ac7e5d9f6d8 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:06:46 +0200 Subject: [PATCH 070/509] Move pickl_window function --- default.py | 6 +++--- resources/lib/pickler.py | 27 ++++++++++++++++++++------- resources/lib/utils.py | 18 ------------------ 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/default.py b/default.py index 43b7bfc6..ec25242f 100644 --- a/default.py +++ b/default.py @@ -32,9 +32,9 @@ sys_path.append(_base_resource) ############################################################################### import entrypoint -from utils import window, pickl_window, reset, passwordsXML, language as lang,\ - dialog, plex_command -from pickler import unpickle_me +from utils import window, reset, passwordsXML, language as lang, dialog, \ + plex_command +from pickler import unpickle_me, pickl_window from PKC_listitem import convert_PKC_to_listitem import variables as v diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index 9bd73bec..1d6baee6 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -1,13 +1,26 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging -import cPickle as Pickle +from logging import getLogger +from cPickle import dumps, loads -from utils import pickl_window +from xbmcgui import Window +############################################################################### +log = getLogger("PLEX."+__name__) +WINDOW = Window(10000) ############################################################################### -log = logging.getLogger("PLEX."+__name__) -############################################################################### + +def pickl_window(property, value=None, clear=False): + """ + Get or set window property - thread safe! For use with Pickle + Property and value must be string + """ + if clear: + WINDOW.clearProperty(property) + elif value is not None: + WINDOW.setProperty(property, value) + else: + return WINDOW.getProperty(property) def pickle_me(obj, window_var='plex_result'): @@ -20,7 +33,7 @@ def pickle_me(obj, window_var='plex_result'): functions won't work. See the Pickle documentation """ log.debug('Start pickling: %s' % obj) - pickl_window(window_var, value=Pickle.dumps(obj)) + pickl_window(window_var, value=dumps(obj)) log.debug('Successfully pickled') @@ -32,7 +45,7 @@ def unpickle_me(window_var='plex_result'): result = pickl_window(window_var) pickl_window(window_var, clear=True) log.debug('Start unpickling') - obj = Pickle.loads(result) + obj = loads(result) log.debug('Successfully unpickled: %s' % obj) return obj diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 3c24a36f..eddc8e7f 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -59,24 +59,6 @@ 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 plex_command(key, value): """ Used to funnel states between different Python instances. NOT really thread From 47675bc60f310384bd268e130b57610e79f9869f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:09:32 +0200 Subject: [PATCH 071/509] Greatly speed up displaying context menu --- contextmenu.py | 52 +++++++++++++------------------ resources/lib/command_pipeline.py | 2 ++ resources/lib/playback_starter.py | 4 +++ 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index 304032ce..29eb3b7d 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -1,52 +1,44 @@ # -*- coding: utf-8 -*- ############################################################################### +from logging import getLogger +from os import path as os_path +from sys import path as sys_path -import logging -import os -import sys +from xbmcaddon import Addon +from xbmc import translatePath, sleep +from xbmcgui import Window -import xbmc -import xbmcaddon - -############################################################################### - -_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) + +from pickler import unpickle_me, pickl_window ############################################################################### - -import loghandler -from context_entry import ContextMenu - -############################################################################### - -loghandler.config() -log = logging.getLogger("PLEX.contextmenu") - +log = getLogger("PLEX."+__name__) ############################################################################### if __name__ == "__main__": - - try: - # Start the context menu - ContextMenu() - except Exception as error: - log.error(error) - import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) - raise + win = Window(10000) + while win.getProperty('plex_command'): + sleep(20) + win.setProperty('plex_command', 'CONTEXT_menu') + while not pickl_window('plex_result'): + sleep(50) + result = unpickle_me() + if result is None: + log.error('Error encountered, aborting') diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 13ec7be9..92c6cc57 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -62,6 +62,8 @@ class Monitor_Window(Thread): value.replace('PLEX_USERNAME-', '') or None elif value.startswith('RUN_LIB_SCAN-'): state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '') + elif value == 'CONTEXT_menu': + queue.put('dummy?mode=context_menu') else: raise NotImplementedError('%s not implemented' % value) else: diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index f0ac27f5..aabfc3ac 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -17,6 +17,7 @@ import variables as v from downloadutils import DownloadUtils from PKC_listitem import convert_PKC_to_listitem import plexdb_functions as plexdb +from context_entry import ContextMenu import state ############################################################################### @@ -142,6 +143,9 @@ class Playback_Starter(Thread): params.get('view_offset'), directplay=True if params.get('play_directly') else False, node=False if params.get('node') == 'false' else True) + elif mode == 'context_menu': + ContextMenu() + result = Playback_Successful() except: log.error('Error encountered for mode %s, params %s' % (mode, params)) From 907c5429509a91aaa3875a29a884e4a8e615b85d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:10:40 +0200 Subject: [PATCH 072/509] Fix logging --- contextmenu.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contextmenu.py b/contextmenu.py index 29eb3b7d..ede4b012 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -26,9 +26,11 @@ except TypeError: 'lib')).decode() sys_path.append(_base_resource) +import loghandler from pickler import unpickle_me, pickl_window ############################################################################### +loghandler.config() log = getLogger("PLEX."+__name__) ############################################################################### From 6ed00a7b11f2f69161348411d9433dcd93e44cd7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:12:53 +0200 Subject: [PATCH 073/509] Reduce number of imports --- resources/lib/loghandler.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/resources/lib/loghandler.py b/resources/lib/loghandler.py index 9aebb3e9..f81c962d 100644 --- a/resources/lib/loghandler.py +++ b/resources/lib/loghandler.py @@ -2,8 +2,6 @@ ############################################################################### import logging import xbmc - -from utils import tryEncode ############################################################################### LEVELS = { logging.ERROR: xbmc.LOGERROR, @@ -14,6 +12,22 @@ LEVELS = { ############################################################################### +def tryEncode(uniString, encoding='utf-8'): + """ + Will try to encode uniString (in unicode) to encoding. This possibly + fails with e.g. Android TV's Python, which does not accept arguments for + string.encode() + """ + if isinstance(uniString, str): + # already encoded + return uniString + try: + uniString = uniString.encode(encoding, "ignore") + except TypeError: + uniString = uniString.encode() + return uniString + + def config(): logger = logging.getLogger('PLEX') logger.addHandler(LogHandler()) From 6e482c04d845bb3e884e63738e5b1f2ab7e24c3f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:17:35 +0200 Subject: [PATCH 074/509] Reduce number of imports --- contextmenu.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index ede4b012..1ab7f553 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- ############################################################################### -from logging import getLogger from os import path as os_path from sys import path as sys_path from xbmcaddon import Addon -from xbmc import translatePath, sleep +from xbmc import translatePath, sleep, log, LOGERROR from xbmcgui import Window _addon = Addon(id='plugin.video.plexkodiconnect') @@ -26,12 +25,8 @@ except TypeError: 'lib')).decode() sys_path.append(_base_resource) -import loghandler from pickler import unpickle_me, pickl_window -############################################################################### -loghandler.config() -log = getLogger("PLEX."+__name__) ############################################################################### if __name__ == "__main__": @@ -43,4 +38,4 @@ if __name__ == "__main__": sleep(50) result = unpickle_me() if result is None: - log.error('Error encountered, aborting') + log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR) From 9c17b8503ae36ed9718b0f2f5f1866ade804d226 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 10 Sep 2017 15:22:06 +0200 Subject: [PATCH 075/509] Reduce number of imports --- resources/lib/pickler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index 1d6baee6..b5579cd4 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- ############################################################################### -from logging import getLogger from cPickle import dumps, loads from xbmcgui import Window +from xbmc import log, LOGDEBUG ############################################################################### -log = getLogger("PLEX."+__name__) WINDOW = Window(10000) +PREFIX = 'PLEX.%s: ' % __name__ ############################################################################### @@ -32,9 +32,9 @@ def pickle_me(obj, window_var='plex_result'): 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) + log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG) pickl_window(window_var, value=dumps(obj)) - log.debug('Successfully pickled') + log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG) def unpickle_me(window_var='plex_result'): @@ -44,9 +44,9 @@ def unpickle_me(window_var='plex_result'): """ result = pickl_window(window_var) pickl_window(window_var, clear=True) - log.debug('Start unpickling') + log('%sStart unpickling' % PREFIX, level=LOGDEBUG) obj = loads(result) - log.debug('Successfully unpickled: %s' % obj) + log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG) return obj From 256d2c3f871b214713db8fdfe56a42d97a090832 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 15:32:44 +0200 Subject: [PATCH 076/509] Fix KeyError for TV live channels for getGeople --- resources/lib/PlexAPI.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 53375015..c7bf53b4 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1346,14 +1346,18 @@ class API(): cast = [] producer = [] for child in self.item: - if child.tag == 'Director': - director.append(child.attrib['tag']) - elif child.tag == 'Writer': - writer.append(child.attrib['tag']) - elif child.tag == 'Role': - cast.append(child.attrib['tag']) - elif child.tag == 'Producer': - producer.append(child.attrib['tag']) + try: + if child.tag == 'Director': + director.append(child.attrib['tag']) + elif child.tag == 'Writer': + writer.append(child.attrib['tag']) + elif child.tag == 'Role': + cast.append(child.attrib['tag']) + elif child.tag == 'Producer': + producer.append(child.attrib['tag']) + except KeyError: + log.warn('Malformed PMS answer for getPeople: %s: %s' + % (child.tag, child.attrib)) return { 'Director': director, 'Writer': writer, From 14fc33442285adb4b8bc661c9b7b04a589ad1354 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 15:41:06 +0200 Subject: [PATCH 077/509] Fix IndexError e.g. for channels if stream info missing --- resources/lib/PlexAPI.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index c7bf53b4..8de6fbcf 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1754,8 +1754,16 @@ class API(): videotracks = [] audiotracks = [] subtitlelanguages = [] - # Sometimes, aspectratio is on the "toplevel" - aspectratio = self.item[0].attrib.get('aspectRatio', None) + try: + # Sometimes, aspectratio is on the "toplevel" + aspectratio = self.item[0].attrib.get('aspectRatio', None) + except IndexError: + # There is no stream info at all, returning empty + return { + 'video': videotracks, + 'audio': audiotracks, + 'subtitle': subtitlelanguages + } # TODO: what if several Media tags exist?!? # Loop over parts for child in self.item[0]: From a3514ec1044b2cc00422e137dd201a0c0168ef69 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 19:59:16 +0200 Subject: [PATCH 078/509] Don't sleep before updating playstate to fully watched --- resources/lib/itemtypes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 96597681..7261861f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -161,8 +161,9 @@ class Items(object): # If offset exceeds duration skip update if item['viewOffset'] > item['duration']: - log.error("Error while updating play state, viewOffset exceeded duration") - return + log.error("Error while updating play state, viewOffset " + "exceeded duration") + return complete = float(item['viewOffset']) / float(item['duration']) log.info('Item %s stopped with completion rate %s percent.' @@ -170,7 +171,6 @@ class Items(object): % (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1) if complete >= MARK_PLAYED_AT: log.info('Marking as completely watched in Kodi') - sleep(500) try: item['viewCount'] += 1 except TypeError: From eaff13998b7bad0dd79ac197bc97b6269c7b805c Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 20:01:17 +0200 Subject: [PATCH 079/509] Remove obsolete imports --- resources/lib/player.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 1dafbe68..91f144b4 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -5,10 +5,8 @@ import logging import json import xbmc -import xbmcgui -from utils import window, settings, language as lang, DateToKodi, \ - getUnixTimestamp, tryDecode, tryEncode +from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode import downloadutils import plexdb_functions as plexdb import kodidb_functions as kodidb From d7c3be5a68d0f5fb702cba44c7d7b0795063c88e Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 20:21:09 +0200 Subject: [PATCH 080/509] Sleep a bit before marking item as fully watched --- resources/lib/player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/player.py b/resources/lib/player.py index 91f144b4..63596d88 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -352,6 +352,8 @@ class Player(xbmc.Player): log.info("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayed)) if percentComplete >= markPlayed: + # Kodi seems to sometimes overwrite our playstate, so wait + xbmc.sleep(500) # Tell Kodi that we've finished watching (Plex knows) if (data['fileid'] is not None and data['itemType'] in (v.KODI_TYPE_MOVIE, From 0dc27dd98ca8006dd1753fe389b490e6a7a8ac78 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Sep 2017 20:24:35 +0200 Subject: [PATCH 081/509] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 927921bc..fa978ee3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.13-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 7331cb12..8c4a7abb 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,14 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.13 (beta only): + version 1.8.14 (beta only): +- Greatly speed up displaying context menu +- Fix IndexError e.g. for channels if stream info missing +- Sleep a bit before marking item as fully watched +- Don't sleep before updating playstate to fully watched (if you watch on another Plex client) +- Fix KeyError for TV live channels for getGeople + +version 1.8.13 (beta only): - Background sync now picks up more PMS changes - Detect Plex item deletion more reliably - Fix changed Plex metadata not synced repeatedly diff --git a/changelog.txt b/changelog.txt index e335f914..808b0d7c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 1.8.14 (beta only): +- Greatly speed up displaying context menu +- Fix IndexError e.g. for channels if stream info missing +- Sleep a bit before marking item as fully watched +- Don't sleep before updating playstate to fully watched (if you watch on another Plex client) +- Fix KeyError for TV live channels for getGeople + version 1.8.13 (beta only): - Background sync now picks up more PMS changes - Detect Plex item deletion more reliably From 14b8df4f9cc455efeb7533980507e01b30fef00e Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 15 Sep 2017 20:01:20 +0200 Subject: [PATCH 082/509] Fix Alexa websocket not exiting on Handshake Status 403 --- resources/lib/websocket_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index ba3f97e1..5a4466d9 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -111,7 +111,8 @@ class WebSocket(Thread): except websocket.WebSocketException as e: log.info('%s: WebSocketException: %s' % (self.__class__.__name__, e)) - if 'Handshake Status 401' in e.args: + if ('Handshake Status 401' in e.args + or 'Handshake Status 403' in e.args): handshake_counter += 1 if handshake_counter >= 5: log.info('%s: Error in handshake detected. ' From 741c00ed641e830c806652ea5f609ce0feef91fe Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 17 Sep 2017 14:07:14 +0200 Subject: [PATCH 083/509] Update translations --- resources/language/resource.language.da_DK/strings.po | 5 +++++ resources/language/resource.language.de_DE/strings.po | 4 ++-- resources/language/resource.language.es_AR/strings.po | 5 +++++ resources/language/resource.language.es_ES/strings.po | 9 ++++++--- resources/language/resource.language.es_MX/strings.po | 5 +++++ resources/language/resource.language.fr_CA/strings.po | 8 +++++++- resources/language/resource.language.fr_FR/strings.po | 5 +++++ resources/language/resource.language.it_IT/strings.po | 5 +++++ resources/language/resource.language.nl_NL/strings.po | 5 +++++ resources/language/resource.language.no_NO/strings.po | 11 ++++++++--- resources/language/resource.language.pt_BR/strings.po | 5 +++++ resources/language/resource.language.pt_PT/strings.po | 5 +++++ resources/language/resource.language.zh_CN/strings.po | 5 +++++ resources/language/resource.language.zh_TW/strings.po | 5 +++++ 14 files changed, 73 insertions(+), 9 deletions(-) diff --git a/resources/language/resource.language.da_DK/strings.po b/resources/language/resource.language.da_DK/strings.po index 346d7772..30f5f76e 100644 --- a/resources/language/resource.language.da_DK/strings.po +++ b/resources/language/resource.language.da_DK/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Jeg ejer denne Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Seneste af antal viste Musik albums:" diff --git a/resources/language/resource.language.de_DE/strings.po b/resources/language/resource.language.de_DE/strings.po index 356af051..eea8aa07 100644 --- a/resources/language/resource.language.de_DE/strings.po +++ b/resources/language/resource.language.de_DE/strings.po @@ -2051,13 +2051,13 @@ msgstr "" "dass du all deine Videos mit Plex verwaltest (und keine direkt mit Kodi). Du" " wirst möglicherweise Daten verlieren, die bereits in der Kodi Video- " "und/oder Musik-Datenbank gespeichert sind (da dieses Addon beide Datenbanken" -" direkt verändert). Verwendung auf eigene Gefahr!" +" direkt verändert). Benutzung auf eigene Gefahr!" # For use with addon.xml (PKC metadata for Kodi, e.g. description) # Addon Disclaimer msgctxt "#39705" msgid "Use at your own risk" -msgstr "Verwendung auf eigene Gefahr" +msgstr "Benutzung auf eigene Gefahr" # If user gets prompted to choose between several subtitles. Leave the number # one at the beginning of the string! diff --git a/resources/language/resource.language.es_AR/strings.po b/resources/language/resource.language.es_AR/strings.po index a1bab325..272428f0 100644 --- a/resources/language/resource.language.es_AR/strings.po +++ b/resources/language/resource.language.es_AR/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Soy dueño de este Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Información" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Número de Álbumes de Música recientes para mostrar:" diff --git a/resources/language/resource.language.es_ES/strings.po b/resources/language/resource.language.es_ES/strings.po index 598ef877..9bf003b4 100644 --- a/resources/language/resource.language.es_ES/strings.po +++ b/resources/language/resource.language.es_ES/strings.po @@ -1,15 +1,13 @@ # XBMC Media Center language file # Translators: # Bartolome Soriano , 2017 -# Dani , 2017 -# Ivan , 2017 msgid "" msgstr "" "Project-Id-Version: PlexKodiConnect\n" "Report-Msgid-Bugs-To: croneter@gmail.com\n" "POT-Creation-Date: 2017-04-15 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Ivan , 2017\n" +"Last-Translator: Bartolome Soriano , 2017\n" "Language-Team: Spanish (Spain) (https://www.transifex.com/croneter/teams/73837/es_ES/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -106,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Soy dueño de este Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Información" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Número de Álbumes de Música recientes para mostrar:" diff --git a/resources/language/resource.language.es_MX/strings.po b/resources/language/resource.language.es_MX/strings.po index a407fe58..df84db11 100644 --- a/resources/language/resource.language.es_MX/strings.po +++ b/resources/language/resource.language.es_MX/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Soy dueño de este Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Información" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Número de Álbumes de Música recientes para mostrar:" diff --git a/resources/language/resource.language.fr_CA/strings.po b/resources/language/resource.language.fr_CA/strings.po index 5c0fea67..a9728958 100644 --- a/resources/language/resource.language.fr_CA/strings.po +++ b/resources/language/resource.language.fr_CA/strings.po @@ -1,13 +1,14 @@ # XBMC Media Center language file # Translators: # Croneter None , 2017 +# Elixir59 , 2017 msgid "" msgstr "" "Project-Id-Version: PlexKodiConnect\n" "Report-Msgid-Bugs-To: croneter@gmail.com\n" "POT-Creation-Date: 2017-04-15 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Croneter None , 2017\n" +"Last-Translator: Elixir59 , 2017\n" "Language-Team: French (Canada) (https://www.transifex.com/croneter/teams/73837/fr_CA/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -104,6 +105,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Je possède ce Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Information" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Nombre d'albums de musique récent à afficher :" diff --git a/resources/language/resource.language.fr_FR/strings.po b/resources/language/resource.language.fr_FR/strings.po index 2b8bf52a..0942b50b 100644 --- a/resources/language/resource.language.fr_FR/strings.po +++ b/resources/language/resource.language.fr_FR/strings.po @@ -106,6 +106,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Je possède ce Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Information" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Nombre d'albums de musique récent à afficher :" diff --git a/resources/language/resource.language.it_IT/strings.po b/resources/language/resource.language.it_IT/strings.po index 19ab3ad6..affdf826 100644 --- a/resources/language/resource.language.it_IT/strings.po +++ b/resources/language/resource.language.it_IT/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Possiedo questo Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Numero di Album Musicali recenti da mostrare:" diff --git a/resources/language/resource.language.nl_NL/strings.po b/resources/language/resource.language.nl_NL/strings.po index e502ca23..6b2e7933 100644 --- a/resources/language/resource.language.nl_NL/strings.po +++ b/resources/language/resource.language.nl_NL/strings.po @@ -105,6 +105,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Ik beheer deze Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Aantal recente muziekalbums tonen:" diff --git a/resources/language/resource.language.no_NO/strings.po b/resources/language/resource.language.no_NO/strings.po index 7545a7c5..3c1f1d52 100644 --- a/resources/language/resource.language.no_NO/strings.po +++ b/resources/language/resource.language.no_NO/strings.po @@ -105,6 +105,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Jeg eier denne Plex Media Server" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Informasjon" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Antall nylige musikkalbum å vise:" @@ -1740,11 +1745,11 @@ msgstr "" msgctxt "#39218" msgid "Error contacting PMS" -msgstr "" +msgstr "Kunne ikke kontakte PMS" msgctxt "#39219" msgid "Abort (Yes) or save address anyway (No)?" -msgstr "" +msgstr "Avbryt (Ja) eller lagre adressen likevel (Nei)?" msgctxt "#39220" msgid "connected" @@ -1766,7 +1771,7 @@ msgstr "" msgctxt "#39224" msgid "Refresh all" -msgstr "" +msgstr "Forny alt" msgctxt "#39225" msgid "Missing only" diff --git a/resources/language/resource.language.pt_BR/strings.po b/resources/language/resource.language.pt_BR/strings.po index 05f615bf..0f0e42e1 100644 --- a/resources/language/resource.language.pt_BR/strings.po +++ b/resources/language/resource.language.pt_BR/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Eu possuo este Servidor Plex Media" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Número de álbuns de música recente para mostrar:" diff --git a/resources/language/resource.language.pt_PT/strings.po b/resources/language/resource.language.pt_PT/strings.po index 234917b9..622ac2fe 100644 --- a/resources/language/resource.language.pt_PT/strings.po +++ b/resources/language/resource.language.pt_PT/strings.po @@ -106,6 +106,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "Eu possuo este Servidor Plex Media" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "Número de álbuns de música recente para mostrar:" diff --git a/resources/language/resource.language.zh_CN/strings.po b/resources/language/resource.language.zh_CN/strings.po index 2c356cf0..aa41853d 100644 --- a/resources/language/resource.language.zh_CN/strings.po +++ b/resources/language/resource.language.zh_CN/strings.po @@ -105,6 +105,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "我拥有这台Plex媒体服务器" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "要显示的最近音乐专辑数量" diff --git a/resources/language/resource.language.zh_TW/strings.po b/resources/language/resource.language.zh_TW/strings.po index 08301559..ca45a219 100644 --- a/resources/language/resource.language.zh_TW/strings.po +++ b/resources/language/resource.language.zh_TW/strings.po @@ -104,6 +104,11 @@ msgctxt "#30031" msgid "I own this Plex Media Server" msgstr "我擁有這個伺服器" +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "" + msgctxt "#30035" msgid "Number of recent Music Albums to show:" msgstr "顯示幾張新加的專輯:" From 80875a15ec9beff0983ab51b62fab3d053bc5d25 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 17 Sep 2017 14:09:38 +0200 Subject: [PATCH 084/509] Version bump --- README.md | 4 ++-- addon.xml | 8 ++++++-- changelog.txt | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fa978ee3..6c1a27ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.15-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.15-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 8c4a7abb..356b4f67 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,11 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.14 (beta only): + version 1.8.15: +- version 1.8.14 for everyone +- Update translations + +version 1.8.14 (beta only): - Greatly speed up displaying context menu - Fix IndexError e.g. for channels if stream info missing - Sleep a bit before marking item as fully watched diff --git a/changelog.txt b/changelog.txt index 808b0d7c..de2cf3ad 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +version 1.8.15: +- version 1.8.14 for everyone +- Update translations + version 1.8.14 (beta only): - Greatly speed up displaying context menu - Fix IndexError e.g. for channels if stream info missing From 5cebbcb763f79bc567546a6dca527d5b63089171 Mon Sep 17 00:00:00 2001 From: dazedcrazy Date: Sat, 23 Sep 2017 14:56:26 +0100 Subject: [PATCH 085/509] Update itemtypes.py --- resources/lib/itemtypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 7261861f..8c01114d 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -251,6 +251,7 @@ class Movies(Items): rating = userdata['Rating'] year = API.getYear() + premieredate = API.getPremiereDate() imdb = API.getProvider('imdb') mpaa = API.getMpaa() countries = API.getCountry() @@ -350,7 +351,7 @@ class Movies(Items): kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating_id, writer, year, uniqueid, sorttitle, runtime, mpaa, genre, director, title, studio, trailer, - country, playurl, pathid, fileid, year, + country, playurl, pathid, fileid, premieredate, userdata['UserRating'], movieid)) else: query = ''' @@ -398,7 +399,7 @@ class Movies(Items): kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount, rating_id, writer, year, uniqueid, sorttitle, runtime, mpaa, genre, director, - title, studio, trailer, country, playurl, pathid, year, + title, studio, trailer, country, playurl, pathid, premieredate, userdata['UserRating'])) else: query = ''' From aa83776a8bcb5891aecd2a9a796ed6fdb2191478 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 23 Sep 2017 18:40:30 +0200 Subject: [PATCH 086/509] Move MARK_PLAYED_AT to variables.py --- resources/lib/itemtypes.py | 5 ++--- resources/lib/player.py | 6 ++---- resources/lib/variables.py | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 8c01114d..4347be0f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -22,7 +22,6 @@ import state log = logging.getLogger("PLEX."+__name__) -MARK_PLAYED_AT = 0.90 ############################################################################### @@ -168,8 +167,8 @@ class Items(object): complete = float(item['viewOffset']) / float(item['duration']) log.info('Item %s stopped with completion rate %s percent.' 'Mark item played at %s percent.' - % (item['ratingKey'], str(complete), MARK_PLAYED_AT), 1) - if complete >= MARK_PLAYED_AT: + % (item['ratingKey'], str(complete), v.MARK_PLAYED_AT), 1) + if complete >= v.MARK_PLAYED_AT: log.info('Marking as completely watched in Kodi') try: item['viewCount'] += 1 diff --git a/resources/lib/player.py b/resources/lib/player.py index 63596d88..2672e275 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -347,11 +347,9 @@ class Player(xbmc.Player): except ZeroDivisionError: # Runtime is 0. percentComplete = 0 - - markPlayed = 0.90 log.info("Percent complete: %s Mark played at: %s" - % (percentComplete, markPlayed)) - if percentComplete >= markPlayed: + % (percentComplete, v.MARK_PLAYED_AT)) + if percentComplete >= v.MARK_PLAYED_AT: # Kodi seems to sometimes overwrite our playstate, so wait xbmc.sleep(500) # Tell Kodi that we've finished watching (Plex knows) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 95042299..856197d3 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -21,6 +21,7 @@ def tryDecode(string, encoding='utf-8'): string = string.decode() return string +MARK_PLAYED_AT = 0.9 _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' From 2bddec60dbababea199e91086043e2d674471473 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 23 Sep 2017 18:49:59 +0200 Subject: [PATCH 087/509] Fix items not getting marked as fully watched - Hopefully fixes #341 --- resources/lib/librarysync.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 6b3e8560..c10e39ba 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1390,6 +1390,10 @@ class LibrarySync(Thread): resume = item.get('viewOffset') / 1000 else: resume = item.get('viewOffset') + if resume >= v.MARK_PLAYED_AT and status not in ('stopped', 'ended'): + # We need to drop these as we'll otherwise NOT mark an item as + # completely watched after having seen >90% + continue # Append to list that we need to process items.append({ 'ratingKey': ratingKey, From 12cf23a4b5337e8a664fb881f9a657e0334c83fc Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 23 Sep 2017 18:52:42 +0200 Subject: [PATCH 088/509] Revert "Sleep a bit before marking item as fully watched" --- resources/lib/player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 2672e275..887a1d70 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -350,8 +350,6 @@ class Player(xbmc.Player): log.info("Percent complete: %s Mark played at: %s" % (percentComplete, v.MARK_PLAYED_AT)) if percentComplete >= v.MARK_PLAYED_AT: - # Kodi seems to sometimes overwrite our playstate, so wait - xbmc.sleep(500) # Tell Kodi that we've finished watching (Plex knows) if (data['fileid'] is not None and data['itemType'] in (v.KODI_TYPE_MOVIE, From 6e5a14cf209053cdf97d0b158c82cc147193f519 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 23 Sep 2017 18:56:39 +0200 Subject: [PATCH 089/509] Version bump --- README.md | 4 ++-- addon.xml | 8 ++++++-- changelog.txt | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6c1a27ed..61957465 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.15-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.15-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.16-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.16-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 356b4f67..6881cf57 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,11 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.15: + version 1.8.16: +- Add premiere dates for movies, thanks @dazedcrazy +- Fix items not getting marked as fully watched + +version 1.8.15: - version 1.8.14 for everyone - Update translations diff --git a/changelog.txt b/changelog.txt index de2cf3ad..64a24455 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +version 1.8.16: +- Add premiere dates for movies, thanks @dazedcrazy +- Fix items not getting marked as fully watched + version 1.8.15: - version 1.8.14 for everyone - Update translations From 345a24f89647e081115f82e30fb08c78a1e9323f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 28 Sep 2017 14:13:00 +0200 Subject: [PATCH 090/509] Fix subtitle languages showing up as unknown - Fixes #342 --- resources/lib/PlexAPI.py | 8 ++++-- resources/lib/variables.py | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 8de6fbcf..561a68c0 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2369,11 +2369,15 @@ class API(): # ext = stream.attrib.get('format') if key: # We do know the language - temporarily download - if stream.attrib.get('language') is not None: + if stream.attrib.get('languageCode') is not None: + try: + language = v.LANGUAGECODE_TO_LANGUAGE[stream.attrib['languageCode']] + except KeyError: + language = stream.attrib['languageCode'] path = self.download_external_subtitles( "{server}%s" % key, "subtitle%02d.%s.%s" % (fileindex, - stream.attrib['language'], + language, stream.attrib['codec'])) fileindex += 1 # We don't know the language - no need to download diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 856197d3..5e8b6b43 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -21,8 +21,66 @@ def tryDecode(string, encoding='utf-8'): string = string.decode() return string +# When does Plex mark a video as completely played? MARK_PLAYED_AT = 0.9 +# Matching table for using Plex XML's stream 'languageCode' +LANGUAGECODE_TO_LANGUAGE = { + 'afr': 'Afrikaans', + 'ara': 'Arabic', + 'hye': 'Armenian', + 'bul': 'Bulgarian', + 'cat': 'Catala', + 'chi': 'Mandarin', + 'hrv': 'Hrvatski', + 'cze': 'Cesky', + 'dan': 'Dansk', + 'dut': 'Nederlands', + 'eng': 'English', + 'epo': 'Esperanto', + 'fin': 'Suomi', + 'fre': 'Francais', + 'ger': 'Deutsch', + 'geo': 'Georgian', + 'gre': 'Greek', + 'heb': 'Hebrew', + 'hin': 'Hindi', + 'hun': 'Magyar', + 'ind': 'Bahasa Indonesia', + 'gle': 'Gaeilge', + 'ice': 'Islenska', + 'ita': 'Italiano', + 'jpn': 'Japanese', + 'kor': 'Korean', + 'kur': 'Kurdi', + 'lat': 'Latin', + 'mac': 'Macedonian', + 'may': 'Malay', + 'mlt': 'Malti', + 'nep': 'Nepali', + 'nor': 'Norsk', + 'per': 'Persian', + 'pol': 'Polszczyzna', + 'por': 'Portugues', + 'rum': 'Romana', + 'rus': 'Russian', + 'srp': 'Serbian', + 'gla': 'Gaidhlig', + 'slo': 'Slovencina', + 'slv': 'Slovenski Jezik', + 'spa': 'Espanol', + 'swe': 'Svenska', + 'tam': 'Tamil', + 'tha': 'Thai', + 'tur': 'Turkish', + 'tah': 'Tahitian', + 'ukr': 'Ukrainian', + 'uzb': 'Ozbek', + 'vie': 'Tieng Viet', + 'wel': 'Cymraeg', + 'yid': 'Yiddish', +} + _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' ADDON_ID = 'plugin.video.plexkodiconnect' From cb285f97e7581e86fecc108c0dcc3364e96a62de Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Oct 2017 17:36:54 +0200 Subject: [PATCH 091/509] Revert "Fix subtitle languages showing up as unknown" This reverts commit 345a24f89647e081115f82e30fb08c78a1e9323f. --- resources/lib/PlexAPI.py | 8 ++---- resources/lib/variables.py | 58 -------------------------------------- 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 561a68c0..8de6fbcf 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2369,15 +2369,11 @@ class API(): # ext = stream.attrib.get('format') if key: # We do know the language - temporarily download - if stream.attrib.get('languageCode') is not None: - try: - language = v.LANGUAGECODE_TO_LANGUAGE[stream.attrib['languageCode']] - except KeyError: - language = stream.attrib['languageCode'] + if stream.attrib.get('language') is not None: path = self.download_external_subtitles( "{server}%s" % key, "subtitle%02d.%s.%s" % (fileindex, - language, + stream.attrib['language'], stream.attrib['codec'])) fileindex += 1 # We don't know the language - no need to download diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 5e8b6b43..856197d3 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -21,66 +21,8 @@ def tryDecode(string, encoding='utf-8'): string = string.decode() return string -# When does Plex mark a video as completely played? MARK_PLAYED_AT = 0.9 -# Matching table for using Plex XML's stream 'languageCode' -LANGUAGECODE_TO_LANGUAGE = { - 'afr': 'Afrikaans', - 'ara': 'Arabic', - 'hye': 'Armenian', - 'bul': 'Bulgarian', - 'cat': 'Catala', - 'chi': 'Mandarin', - 'hrv': 'Hrvatski', - 'cze': 'Cesky', - 'dan': 'Dansk', - 'dut': 'Nederlands', - 'eng': 'English', - 'epo': 'Esperanto', - 'fin': 'Suomi', - 'fre': 'Francais', - 'ger': 'Deutsch', - 'geo': 'Georgian', - 'gre': 'Greek', - 'heb': 'Hebrew', - 'hin': 'Hindi', - 'hun': 'Magyar', - 'ind': 'Bahasa Indonesia', - 'gle': 'Gaeilge', - 'ice': 'Islenska', - 'ita': 'Italiano', - 'jpn': 'Japanese', - 'kor': 'Korean', - 'kur': 'Kurdi', - 'lat': 'Latin', - 'mac': 'Macedonian', - 'may': 'Malay', - 'mlt': 'Malti', - 'nep': 'Nepali', - 'nor': 'Norsk', - 'per': 'Persian', - 'pol': 'Polszczyzna', - 'por': 'Portugues', - 'rum': 'Romana', - 'rus': 'Russian', - 'srp': 'Serbian', - 'gla': 'Gaidhlig', - 'slo': 'Slovencina', - 'slv': 'Slovenski Jezik', - 'spa': 'Espanol', - 'swe': 'Svenska', - 'tam': 'Tamil', - 'tha': 'Thai', - 'tur': 'Turkish', - 'tah': 'Tahitian', - 'ukr': 'Ukrainian', - 'uzb': 'Ozbek', - 'vie': 'Tieng Viet', - 'wel': 'Cymraeg', - 'yid': 'Yiddish', -} - _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' ADDON_ID = 'plugin.video.plexkodiconnect' From 02a60fac207bd1960b997680577b04712154e0da Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Oct 2017 17:41:28 +0200 Subject: [PATCH 092/509] Revert "More descriptive downloadable subtitles" This reverts commit 8af180968b41f0600a440cceb5967b1ce6d7e4e2. --- resources/lib/PlexAPI.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 8de6fbcf..8ca34e15 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2369,11 +2369,11 @@ class API(): # ext = stream.attrib.get('format') if key: # We do know the language - temporarily download - if stream.attrib.get('language') is not None: + if stream.attrib.get('languageCode') is not None: path = self.download_external_subtitles( "{server}%s" % key, "subtitle%02d.%s.%s" % (fileindex, - stream.attrib['language'], + stream.attrib['languageCode'], stream.attrib['codec'])) fileindex += 1 # We don't know the language - no need to download From 1e8ec2f0d715b58f1ccf99063ad89bfa09a095a2 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Oct 2017 17:49:57 +0200 Subject: [PATCH 093/509] Remove obsolete PKC settings show contextmenu --- resources/lib/kodimonitor.py | 1 - resources/settings.xml | 1 - service.py | 2 -- 3 files changed, 4 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 27db9a73..a9933924 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -22,7 +22,6 @@ log = getLogger("PLEX."+__name__) # settings: window-variable WINDOW_SETTINGS = { - 'enableContext': 'plex_context', 'plex_restricteduser': 'plex_restricteduser', 'force_transcode_pix': 'plex_force_transcode_pix', 'fetch_pms_item_number': 'fetch_pms_item_number' diff --git a/resources/settings.xml b/resources/settings.xml index bfe2d62a..2470f1b4 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -38,7 +38,6 @@ - diff --git a/service.py b/service.py index 6b8f464f..676b0b19 100644 --- a/service.py +++ b/service.py @@ -85,8 +85,6 @@ class Service(): window('plex_kodiProfile', value=tryDecode(translatePath("special://profile"))) - window('plex_context', - value='true' if settings('enableContext') == "true" else "") window('fetch_pms_item_number', value=settings('fetch_pms_item_number')) From 2f073c3a1522fcc24b7d2c058cda90c906fd2ba7 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Oct 2017 18:18:05 +0200 Subject: [PATCH 094/509] Enable channels for Plex home users --- resources/lib/entrypoint.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index de4b9f83..6ac3366d 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -752,10 +752,6 @@ def channels(): """ Listing for Plex Channels """ - if window('plex_restricteduser') == 'true': - log.error('No Plex Channels - restricted user') - return xbmcplugin.endOfDirectory(HANDLE, False) - xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all') try: xml[0].attrib From 1b61a05656e81b9e7dac734271e068029b92c433 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Oct 2017 18:19:19 +0200 Subject: [PATCH 095/509] Version bump --- README.md | 4 ++-- addon.xml | 10 ++++++++-- changelog.txt | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 61957465..1595dac0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.16-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.16-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.17-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.17-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 6881cf57..d6d8edf3 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,13 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.16: + version 1.8.17: +- Hopefully fix stable repo +- Fix subtitles not working or showing up as Unknown +- Enable channels for Plex home users +- Remove obsolete PKC settings show contextmenu + +version 1.8.16: - Add premiere dates for movies, thanks @dazedcrazy - Fix items not getting marked as fully watched diff --git a/changelog.txt b/changelog.txt index 64a24455..59aa4ae3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +version 1.8.17: +- Hopefully fix stable repo +- Fix subtitles not working or showing up as Unknown +- Enable channels for Plex home users +- Remove obsolete PKC settings show contextmenu + version 1.8.16: - Add premiere dates for movies, thanks @dazedcrazy - Fix items not getting marked as fully watched From 38d611aa276a0a63e7738de2af0f973326fcde4a Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Oct 2017 08:09:21 +0200 Subject: [PATCH 096/509] PEP8 --- resources/lib/PlexAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 8ca34e15..b6efc417 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -57,7 +57,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = logging.getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') From f3c71fadf230cdac3567746c0fbac85d6159887d Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Oct 2017 08:41:16 +0200 Subject: [PATCH 097/509] Deal better with missing stream info (e.g. channels) --- resources/lib/PlexAPI.py | 97 +++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b6efc417..c59b397c 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1756,61 +1756,56 @@ class API(): subtitlelanguages = [] try: # Sometimes, aspectratio is on the "toplevel" - aspectratio = self.item[0].attrib.get('aspectRatio', None) + aspect = self.item[0].attrib.get('aspectRatio') except IndexError: # There is no stream info at all, returning empty return { - 'video': videotracks, - 'audio': audiotracks, - 'subtitle': subtitlelanguages - } - # TODO: what if several Media tags exist?!? + 'video': videotracks, + 'audio': audiotracks, + 'subtitle': subtitlelanguages + } # Loop over parts for child in self.item[0]: - container = child.attrib.get('container', None) + container = child.attrib.get('container') # Loop over Streams for grandchild in child: - mediaStream = grandchild.attrib - mediaType = int(mediaStream.get('streamType', 999)) - if mediaType == 1: # Video streams - videotrack = {} - videotrack['codec'] = mediaStream['codec'].lower() - if "msmpeg4" in videotrack['codec']: - videotrack['codec'] = "divx" - elif "mpeg4" in videotrack['codec']: - # if "simple profile" in profile or profile == "": - # videotrack['codec'] = "xvid" - pass - elif "h264" in videotrack['codec']: - if container in ("mp4", "mov", "m4v"): - videotrack['codec'] = "avc1" - videotrack['height'] = mediaStream.get('height', None) - videotrack['width'] = mediaStream.get('width', None) - # TODO: 3d Movies?!? - # videotrack['Video3DFormat'] = item.get('Video3DFormat') - aspectratio = mediaStream.get('aspectRatio', aspectratio) - videotrack['aspect'] = aspectratio - # TODO: Video 3d format - videotrack['video3DFormat'] = None - videotracks.append(videotrack) - - elif mediaType == 2: # Audio streams - audiotrack = {} - audiotrack['codec'] = mediaStream['codec'].lower() - if ("dca" in audiotrack['codec'] and - "ma" in mediaStream.get('profile', '').lower()): - audiotrack['codec'] = "dtshd_ma" - audiotrack['channels'] = mediaStream.get('channels') + stream = grandchild.attrib + media_type = int(stream.get('streamType', 999)) + track = {} + if media_type == 1: # Video streams + if 'codec' in stream: + track['codec'] = stream['codec'].lower() + if "msmpeg4" in track['codec']: + track['codec'] = "divx" + elif "mpeg4" in track['codec']: + # if "simple profile" in profile or profile == "": + # track['codec'] = "xvid" + pass + elif "h264" in track['codec']: + if container in ("mp4", "mov", "m4v"): + track['codec'] = "avc1" + track['height'] = stream.get('height') + track['width'] = stream.get('width') + # track['Video3DFormat'] = item.get('Video3DFormat') + track['aspect'] = stream.get('aspectRatio', aspect) + track['duration'] = self.getRuntime()[1] + track['video3DFormat'] = None + videotracks.append(track) + elif media_type == 2: # Audio streams + if 'codec' in stream: + track['codec'] = stream['codec'].lower() + if ("dca" in track['codec'] and + "ma" in stream.get('profile', '').lower()): + track['codec'] = "dtshd_ma" + track['channels'] = stream.get('channels') # 'unknown' if we cannot get language - audiotrack['language'] = mediaStream.get( + track['language'] = stream.get( 'languageCode', lang(39310)).lower() - audiotracks.append(audiotrack) - - elif mediaType == 3: # Subtitle streams + audiotracks.append(track) + elif media_type == 3: # Subtitle streams # 'unknown' if we cannot get language subtitlelanguages.append( - mediaStream.get('languageCode', - lang(39310)).lower()) + stream.get('languageCode', lang(39310)).lower()) return { 'video': videotracks, 'audio': audiotracks, @@ -2564,17 +2559,9 @@ class API(): Add media stream information to xbmcgui.ListItem """ mediastreams = self.getMediaStreams() - videostreamFound = False - if mediastreams: - for key, value in mediastreams.iteritems(): - if key == "video" and value: - videostreamFound = True - if value: - listItem.addStreamInfo(key, value) - if not videostreamFound: - # just set empty streamdetails to prevent errors in the logs - listItem.addStreamInfo( - "video", {'duration': self.getRuntime()[1]}) + for key, value in mediastreams.iteritems(): + if value: + listItem.addStreamInfo(key, value) def validatePlayurl(self, path, typus, forceCheck=False, folder=False, omitCheck=False): From e3dba1974f3bbfdb7aa65cc5973b57912b83603f Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Oct 2017 08:42:04 +0200 Subject: [PATCH 098/509] Code optimization --- resources/lib/PlexAPI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index c59b397c..19f27677 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2558,8 +2558,7 @@ class API(): """ Add media stream information to xbmcgui.ListItem """ - mediastreams = self.getMediaStreams() - for key, value in mediastreams.iteritems(): + for key, value in self.getMediaStreams(): if value: listItem.addStreamInfo(key, value) From eaff533489703cb8a3ac2e7ecc352a0b406e08a9 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 6 Oct 2017 08:46:05 +0200 Subject: [PATCH 099/509] Fix AttributeError if Plex key is missing --- resources/lib/PlexAPI.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 19f27677..9017ec9a 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1177,9 +1177,9 @@ class API(): def getKey(self): """ - Returns the Plex key such as '/library/metadata/246922' + Returns the Plex key such as '/library/metadata/246922' or empty string """ - return self.item.attrib.get('key') + return self.item.attrib.get('key', '') def getFilePath(self, forceFirstMediaStream=False): """ From 39d95adda433b37346855b90c6bcb723be141873 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 9 Oct 2017 22:09:08 +0200 Subject: [PATCH 100/509] Fix Plex context menu not showing up --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index d6d8edf3..47911a6b 100644 --- a/addon.xml +++ b/addon.xml @@ -13,7 +13,7 @@ 30416 - [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(plexid))] + !IsEmpty(Window(10000).Property(plex_context)) + [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(plexid))] From a8d4e2b8c19bc8d7eca2adf1c20b9b8caf5c9771 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 9 Oct 2017 22:12:30 +0200 Subject: [PATCH 101/509] Fix ValueError for channels --- resources/lib/PlexAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 9017ec9a..da08ea42 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2558,7 +2558,7 @@ class API(): """ Add media stream information to xbmcgui.ListItem """ - for key, value in self.getMediaStreams(): + for key, value in self.getMediaStreams().iteritems(): if value: listItem.addStreamInfo(key, value) From 60b9d31bd55f1303e81e04e3476b4bda62372c27 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 9 Oct 2017 22:16:54 +0200 Subject: [PATCH 102/509] Version bump --- README.md | 4 ++-- addon.xml | 9 +++++++-- changelog.txt | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1595dac0..36fa9753 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.17-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.17-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.18-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 47911a6b..aed5a046 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,12 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.17: + version 1.8.18: +- Fix Plex context menu not showing up +- Deal better with missing stream info (e.g. channels) +- Fix AttributeError if Plex key is missing + +version 1.8.17: - Hopefully fix stable repo - Fix subtitles not working or showing up as Unknown - Enable channels for Plex home users diff --git a/changelog.txt b/changelog.txt index 59aa4ae3..146d7fea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +version 1.8.18: +- Fix Plex context menu not showing up +- Deal better with missing stream info (e.g. channels) +- Fix AttributeError if Plex key is missing + version 1.8.17: - Hopefully fix stable repo - Fix subtitles not working or showing up as Unknown From eb66435d2d4b88e0583a47d3afa1b7c8ac5ce8f5 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 9 Oct 2017 22:28:31 +0200 Subject: [PATCH 103/509] Russian translation, thanks @UncleStark, @xom2000, @AlexFreit --- addon.xml | 1 + changelog.txt | 1 + .../resource.language.ru_RU/strings.po | 2128 +++++++++++++++++ 3 files changed, 2130 insertions(+) create mode 100644 resources/language/resource.language.ru_RU/strings.po diff --git a/addon.xml b/addon.xml index aed5a046..16721c24 100644 --- a/addon.xml +++ b/addon.xml @@ -60,6 +60,7 @@ Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar version 1.8.18: +- Russian translation, thanks @UncleStark, @xom2000, @AlexFreit - Fix Plex context menu not showing up - Deal better with missing stream info (e.g. channels) - Fix AttributeError if Plex key is missing diff --git a/changelog.txt b/changelog.txt index 146d7fea..85ca2adb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,5 @@ version 1.8.18: +- Russian translation, thanks @UncleStark, @xom2000, @AlexFreit - Fix Plex context menu not showing up - Deal better with missing stream info (e.g. channels) - Fix AttributeError if Plex key is missing diff --git a/resources/language/resource.language.ru_RU/strings.po b/resources/language/resource.language.ru_RU/strings.po new file mode 100644 index 00000000..60539c47 --- /dev/null +++ b/resources/language/resource.language.ru_RU/strings.po @@ -0,0 +1,2128 @@ +# XBMC Media Center language file +# Translators: +# Croneter None , 2017 +# Vladimir Supranenok , 2017 +# Алексей Коробцов , 2017 +# Павел Хоменко , 2017 +# Alex Freit , 2017 +msgid "" +msgstr "" +"Project-Id-Version: PlexKodiConnect\n" +"Report-Msgid-Bugs-To: croneter@gmail.com\n" +"POT-Creation-Date: 2017-04-15 13:13+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Alex Freit , 2017\n" +"Language-Team: Russian (Russia) (https://www.transifex.com/croneter/teams/73837/ru_RU/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru_RU\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" + +# Add-on settings +msgctxt "#29999" +msgid "PlexKodiConnect" +msgstr "PlexKodiConnect" + +msgctxt "#30000" +msgid "Server Address (IP)" +msgstr "Адрес сервера (IP)" + +msgctxt "#30002" +msgid "Preferred playback method" +msgstr "Предпочтительное воспроизведение" + +msgctxt "#30004" +msgid "Log level" +msgstr "Логирование" + +msgctxt "#30005" +msgid "Username: " +msgstr "Имя пользователя: " + +msgctxt "#30006" +msgid "Password: " +msgstr "Пароль: " + +msgctxt "#30007" +msgid "Network Username: " +msgstr "Логин: " + +msgctxt "#30008" +msgid "Network Password: " +msgstr "Пароль: " + +msgctxt "#30009" +msgid "Transcode: " +msgstr "Транскодинг: " + +msgctxt "#30010" +msgid "Enable Performance Profiling" +msgstr "Задействовать профили производительности" + +msgctxt "#30011" +msgid "Local caching system" +msgstr "Локальная система кеширования" + +msgctxt "#30012" +msgid "OK" +msgstr "Ок" + +msgctxt "#30013" +msgid "Never show" +msgstr "Никогда не показывать " + +msgctxt "#30014" +msgid "Connection" +msgstr "Подключение" + +msgctxt "#30015" +msgid "Network" +msgstr "Сеть" + +msgctxt "#30016" +msgid "Device Name" +msgstr "Имя устройства" + +msgctxt "#30017" +msgid "Unauthorized for PMS" +msgstr "Не авторизован на сервере Plex" + +msgctxt "#30022" +msgid "Advanced" +msgstr "Расширенные" + +msgctxt "#30024" +msgid "Username" +msgstr "Имя пользователя" + +msgctxt "#30025" +msgid "Display message if PMS goes offline" +msgstr "Сообщать об отключении от сервера Plex " + +msgctxt "#30030" +msgid "Port Number" +msgstr "Порт" + +msgctxt "#30031" +msgid "I own this Plex Media Server" +msgstr "Я владелец этого сервера Plex" + +# Kodi context menu entry for movie and episode information screen +msgctxt "#30032" +msgid "Information" +msgstr "Информация" + +msgctxt "#30035" +msgid "Number of recent Music Albums to show:" +msgstr "Количество последних музыкальных альбомов для отображения:" + +msgctxt "#30036" +msgid "Number of recent Movies to show:" +msgstr "Количество последних фильмов для отображения:" + +msgctxt "#30037" +msgid "Number of recent TV episodes to show:" +msgstr "Количество последних телевизионных эпизодов для отображения:" + +msgctxt "#30038" +msgid "Mark watched at start of playback:" +msgstr "Отметить просмотренным при начале воспроизведения:" + +msgctxt "#30039" +msgid "Set Season poster for episodes" +msgstr "Установить обложку для сезона" + +msgctxt "#30040" +msgid "Genre Filter ..." +msgstr "Фильтр жанра" + +msgctxt "#30041" +msgid "Play All from Here" +msgstr "Воспроизвести Все начиная отсюда" + +msgctxt "#30042" +msgid "Refresh" +msgstr "Обновить" + +msgctxt "#30043" +msgid "Delete" +msgstr "Удалить" + +msgctxt "#30044" +msgid "Incorrect Username/Password" +msgstr "неправильные имя пользователя/пароль" + +msgctxt "#30045" +msgid "Username not found" +msgstr "Имя пользователя не найдено" + +msgctxt "#30046" +msgid "Add Movie to CouchPotato" +msgstr "Добавить фильм в CouchPotato" + +msgctxt "#30052" +msgid "Deleting" +msgstr "Удаление" + +msgctxt "#30053" +msgid "Waiting for server to delete" +msgstr "Ожидание удаления c сервера" + +msgctxt "#30059" +msgid "Server Default" +msgstr "Сервер по умолчанию" + +msgctxt "#30060" +msgid "Title" +msgstr "Название" + +msgctxt "#30061" +msgid "Year" +msgstr "Год" + +msgctxt "#30062" +msgid "Premiere Date" +msgstr "Дата премьеры" + +msgctxt "#30063" +msgid "Date Created" +msgstr "Дата создания" + +msgctxt "#30064" +msgid "Critic Rating" +msgstr "Рейтинг критиков" + +msgctxt "#30065" +msgid "Community Rating" +msgstr "Рейтинг пользователей" + +msgctxt "#30066" +msgid "Play Count" +msgstr "Количество просмотров" + +msgctxt "#30067" +msgid "Budget" +msgstr "Бюджет " + +# Runtime added as 30226 below +msgctxt "#30068" +msgid "Sort By" +msgstr "Сортировать по" + +msgctxt "#30069" +msgid "None" +msgstr "Ничего" + +msgctxt "#30070" +msgid "Action" +msgstr "Экшен" + +msgctxt "#30071" +msgid "Adventure" +msgstr "Приключение" + +msgctxt "#30072" +msgid "Animation" +msgstr "Мультипликация" + +msgctxt "#30073" +msgid "Crime" +msgstr "Криминал" + +msgctxt "#30074" +msgid "Comedy" +msgstr "Комедия" + +msgctxt "#30075" +msgid "Documentary" +msgstr "Документальный" + +msgctxt "#30076" +msgid "Drama" +msgstr "Драма" + +msgctxt "#30077" +msgid "Fantasy" +msgstr "Фэнтези" + +msgctxt "#30078" +msgid "Foreign" +msgstr "Иностранные" + +msgctxt "#30079" +msgid "History" +msgstr "Исторический" + +msgctxt "#30080" +msgid "Horror" +msgstr "Ужасы" + +msgctxt "#30081" +msgid "Music" +msgstr "Музыка" + +msgctxt "#30082" +msgid "Musical" +msgstr "Музыкальный" + +msgctxt "#30083" +msgid "Mystery" +msgstr "Мистика" + +msgctxt "#30084" +msgid "Romance" +msgstr "Любовный" + +msgctxt "#30085" +msgid "Science Fiction" +msgstr "Научная фантастика" + +msgctxt "#30086" +msgid "Short" +msgstr "Короткометражка" + +msgctxt "#30087" +msgid "Suspense" +msgstr "Саспенс" + +msgctxt "#30088" +msgid "Thriller" +msgstr "Триллер" + +msgctxt "#30089" +msgid "Western" +msgstr "Вестерн" + +msgctxt "#30090" +msgid "Genre Filter" +msgstr "Фильтр жанров" + +msgctxt "#30091" +msgid "Confirm file deletion" +msgstr "Подтвердить удаление файла" + +msgctxt "#30092" +msgid "" +"Delete this item? This action will delete media and associated data files." +msgstr "Удалить? Это действие удалит элемент и связанные файлы." + +msgctxt "#30093" +msgid "Mark Watched" +msgstr "Отметить как просмотренное" + +msgctxt "#30094" +msgid "Mark Unwatched" +msgstr "Отметить как непросмотренное" + +msgctxt "#30095" +msgid "Add to Favorites" +msgstr "Добавить в Избранное" + +msgctxt "#30096" +msgid "Remove from Favorites" +msgstr "Удалить из Избранного" + +msgctxt "#30097" +msgid "Sort By ..." +msgstr "Сортировать по ..." + +msgctxt "#30098" +msgid "Sort Order Descending" +msgstr "Сортировать по убыванию" + +msgctxt "#30099" +msgid "Sort Order Ascending" +msgstr "Сортировать по возрастанию" + +msgctxt "#30100" +msgid "Show People" +msgstr "Показать людей" + +# resume dialog +msgctxt "#30105" +msgid "Resume" +msgstr "Продолжить" + +msgctxt "#30106" +msgid "Resume from" +msgstr "Продолжить с" + +msgctxt "#30107" +msgid "Start from beginning" +msgstr "Начать с начала" + +msgctxt "#30110" +msgid "Interface" +msgstr "Интерфейс" + +msgctxt "#30111" +msgid "Include Stream Info" +msgstr "Добавить информацию о потоке" + +msgctxt "#30112" +msgid "Include People" +msgstr "Добавить Людей" + +msgctxt "#30113" +msgid "Include Overview" +msgstr "Добавить описание" + +msgctxt "#30114" +msgid "Offer delete after playback" +msgstr "Предлагать удалить после просмотра" + +msgctxt "#30115" +msgid "For Episodes" +msgstr "Для серий" + +msgctxt "#30116" +msgid "For Movies" +msgstr "Для фильмов" + +msgctxt "#30117" +msgid "Background Art Refresh Rate (seconds)" +msgstr "Время обновления фоновой иллюстрации (в секундах)" + +msgctxt "#30118" +msgid "Add Resume Percent" +msgstr "Добавить процент продолжения" + +msgctxt "#30119" +msgid "Add Episode Number" +msgstr "Добавить номер Эпизода" + +msgctxt "#30120" +msgid "Show Load Progress" +msgstr "Показывать процесс загрузки" + +msgctxt "#30121" +msgid "Loading Content" +msgstr "Загрузка содержимого" + +msgctxt "#30122" +msgid "Retrieving Data" +msgstr "Получение данных" + +msgctxt "#30125" +msgid "Done" +msgstr "Готово" + +msgctxt "#30126" +msgid "Processing Item : " +msgstr "Текущий элемент:" + +msgctxt "#30128" +msgid "Play Error" +msgstr "Ошибка воспроизведения" + +msgctxt "#30129" +msgid "This item is not playable" +msgstr "Невозможно воспроизвести" + +msgctxt "#30130" +msgid "Local path detected" +msgstr "Обнаружен локальный путь" + +msgctxt "#30131" +msgid "" +"Your MB3 Server contains local paths. Please change server paths to UNC or " +"change XBMB3C setting 'Play from Stream' to true. Path: " +msgstr "" +"Ваш сервер MB3 содержит локальные пути. Пожалуйста измените их на UNC, либо " +"включите настройку \"Воспроизводить потоком\" в XBMB3C. Путь:" + +msgctxt "#30132" +msgid "Warning" +msgstr "Предупреждение" + +msgctxt "#30133" +msgid "Debug logging enabled." +msgstr "Включен режим отладки" + +msgctxt "#30134" +msgid "This will affect performance." +msgstr "Это повлияет на производительность." + +msgctxt "#30135" +msgid "Error" +msgstr "Ошибка" + +msgctxt "#30136" +msgid "Monitoring service is not running" +msgstr "Служба мониторинга не работает" + +msgctxt "#30137" +msgid "If you have just installed please restart Kodi" +msgstr "Если только что установили - перезапустите Kodi" + +msgctxt "#30138" +msgid "Search" +msgstr "Поиск" + +msgctxt "#30139" +msgid "Enable Theme Music (Requires Restart)" +msgstr "Включить музыкальную тему (требуется перезагрузка)" + +msgctxt "#30140" +msgid " - Loop Theme Music" +msgstr "- Зациклить музыкальную тему" + +msgctxt "#30141" +msgid "Enable Background Image (Requires Restart)" +msgstr "Включить фоновое изображение (требуется перезагрузка)" + +msgctxt "#30142" +msgid "Services" +msgstr "Службы" + +msgctxt "#30143" +msgid "Always transcode if video bitrate is above" +msgstr "Транскодировать, если битрейт видео больше" + +msgctxt "#30150" +msgid "Skin does not support setting views" +msgstr "Обложка не поддерживает настройки просмотра" + +msgctxt "#30151" +msgid "Select item action (Requires Restart)" +msgstr "Выбрать действие элемента (необходим перезапуск)" + +msgctxt "#30156" +msgid "Sort NextUp by Show Title" +msgstr "Сортировать \"Следующее\" по названию" + +msgctxt "#30157" +msgid "Enable Enhanced Images (eg CoverArt)" +msgstr "Включить улучшенные изображения (например CoverArt)" + +msgctxt "#30158" +msgid "Metadata" +msgstr "Метаданные" + +msgctxt "#30159" +msgid "Artwork" +msgstr "Иллюстрация" + +msgctxt "#30160" +msgid "Video Quality if Transcoding necessary" +msgstr "Качество при транскодинге" + +msgctxt "#30161" +msgid "Enable Suggested Loader (Requires Restart)" +msgstr "Включить загрузку \"предлагаемого\" (требуется перезапуск)" + +msgctxt "#30162" +msgid "Add Season Number" +msgstr "Добавить номер Сезона" + +msgctxt "#30163" +msgid "Flatten Seasons" +msgstr "Сквозная нумерация" + +msgctxt "#30164" +msgid "Direct Play - HTTP" +msgstr "Прямое воспроизведение - HTTP" + +msgctxt "#30165" +msgid "Direct Play" +msgstr "Прямое воспроизведение" + +msgctxt "#30166" +msgid "Transcoding" +msgstr "Транскодирование" + +msgctxt "#30167" +msgid "Server Detection Succeeded" +msgstr "Успешное обнаружение сервера" + +msgctxt "#30168" +msgid "Found server" +msgstr "Найден сервер" + +msgctxt "#30169" +msgid "Address : " +msgstr "Адрес : " + +# Video nodes +msgctxt "#30170" +msgid "Recently Added TV Shows" +msgstr "Недавно добавленные Сериалы" + +msgctxt "#30171" +msgid "In Progress TV Shows" +msgstr "Просматриваемые Сериалы" + +msgctxt "#30172" +msgid "All Music" +msgstr "Вся Музыка" + +msgctxt "#30173" +msgid "Channels" +msgstr "Каналы" + +msgctxt "#30174" +msgid "Recently Added" +msgstr "Недавно добавленные" + +msgctxt "#30175" +msgid "Recently Added Episodes" +msgstr "Недавно добавленные Серии" + +msgctxt "#30176" +msgid "Recently Added Albums" +msgstr "Недавно добавленные Альбомы" + +msgctxt "#30177" +msgid "In Progress Movies" +msgstr "Просматриваемые Фильмы" + +msgctxt "#30178" +msgid "In Progress Episodes" +msgstr "Просматриваемые Эпизоды" + +msgctxt "#30179" +msgid "Next Episodes" +msgstr "Следующий Эпизод" + +msgctxt "#30180" +msgid "Favorite Movies" +msgstr "Избранные Фильмы" + +msgctxt "#30181" +msgid "Favorite Shows" +msgstr "Избранные Сериалы" + +msgctxt "#30182" +msgid "Favorite Episodes" +msgstr "Избранные Эпизоды" + +msgctxt "#30183" +msgid "Frequent Played Albums" +msgstr "Часто проигрываемые альбомы" + +msgctxt "#30184" +msgid "Upcoming TV" +msgstr "Скоро на ТВ" + +msgctxt "#30185" +msgid "BoxSets" +msgstr "Сборники" + +msgctxt "#30186" +msgid "Trailers" +msgstr "Трейлеры" + +msgctxt "#30187" +msgid "Music Videos" +msgstr "Видеоклипы" + +msgctxt "#30188" +msgid "Photos" +msgstr "Фотографии" + +msgctxt "#30189" +msgid "Unwatched Movies" +msgstr "Непросмотренные Фильмы" + +msgctxt "#30190" +msgid "Movie Genres" +msgstr "Жанры фильмов" + +msgctxt "#30191" +msgid "Movie Studios" +msgstr "Киностудии" + +msgctxt "#30192" +msgid "Movie Actors" +msgstr "Киноактеры" + +msgctxt "#30193" +msgid "Unwatched Episodes" +msgstr "Непросмотренные Серии" + +msgctxt "#30194" +msgid "TV Genres" +msgstr "ТВ Жанры" + +msgctxt "#30195" +msgid "TV Networks" +msgstr "Телевизионные сети" + +msgctxt "#30196" +msgid "TV Actors" +msgstr "Телеактеры" + +msgctxt "#30197" +msgid "Playlists" +msgstr "Плейлисты" + +msgctxt "#30198" +msgid "Search" +msgstr "Поиск" + +msgctxt "#30199" +msgid "Set Views" +msgstr "Задать просмотры" + +msgctxt "#30200" +msgid "Select User" +msgstr "Выбрать пользователя" + +msgctxt "#30201" +msgid "Profiling enabled." +msgstr "Профили задействованы" + +msgctxt "#30202" +msgid "Please remember to turn off when finished testing." +msgstr "Пожалуйста, не забудьте отключить, когда закончится тестирование" + +msgctxt "#30203" +msgid "Error in ArtworkRotationThread" +msgstr "Ошибка в ArtworkRotationThread" + +msgctxt "#30204" +msgid "Unable to connect to server" +msgstr "Невозможно подключиться к серверу" + +msgctxt "#30205" +msgid "Error in LoadMenuOptionsThread" +msgstr "Ошибка в LoadMenuOptionsThread" + +msgctxt "#30206" +msgid "Enable Playlists Loader (Requires Restart)" +msgstr "Включить загрузку Плейлистов (требуется перезагрузка)" + +msgctxt "#30207" +msgid "Songs" +msgstr "Песни" + +msgctxt "#30208" +msgid "Albums" +msgstr "Альбомы" + +msgctxt "#30209" +msgid "Album Artists" +msgstr "Исполнители Альбома" + +msgctxt "#30210" +msgid "Artists" +msgstr "Исполнители" + +msgctxt "#30211" +msgid "Music Genres" +msgstr "Музыкальные жанры" + +msgctxt "#30212" +msgid "Enable Theme Videos (Requires Restart)" +msgstr "Включить музыкальную тему (требуется перезагрузка)" + +msgctxt "#30213" +msgid " - Loop Theme Videos" +msgstr " - Зациклить музыкальную тему" + +msgctxt "#30216" +msgid "AutoPlay remaining episodes in a season" +msgstr "Авто воспроизведение остававшихся серий в сезоне" + +msgctxt "#30218" +msgid "Compress Artwork" +msgstr "Сжимать дополнительные изображения" + +msgctxt "#30220" +msgid "Latest " +msgstr "Последний" + +msgctxt "#30221" +msgid "In Progress " +msgstr "В процессе" + +msgctxt "#30222" +msgid "NextUp " +msgstr "Следующее" + +msgctxt "#30223" +msgid "User Views" +msgstr "Пользовательские просмотры" + +msgctxt "#30224" +msgid "Report Metrics" +msgstr "Отправить данные" + +msgctxt "#30225" +msgid "Use Kodi Sorting" +msgstr "Использовать сортировку Kodi" + +msgctxt "#30226" +msgid "Runtime" +msgstr "Время работы" + +msgctxt "#30227" +msgid "Random" +msgstr "Случайно" + +msgctxt "#30228" +msgid "Recently releases" +msgstr "Недавно добавлено" + +msgctxt "#30229" +msgid "Random Items" +msgstr "Случайные" + +msgctxt "#30230" +msgid "Recommended" +msgstr "Рекомендуемые" + +msgctxt "#30235" +msgid "Extras" +msgstr "Дополнительно" + +msgctxt "#30236" +msgid "Sync Theme Music" +msgstr "Синхронизировать музыкальную тему" + +msgctxt "#30237" +msgid "Sync Extra Fanart" +msgstr "Синхронизировать дополнительные иллюстрации" + +msgctxt "#30238" +msgid "Sync Movie BoxSets" +msgstr "Синхронизировать сборники фильмов" + +msgctxt "#30239" +msgid "[COLOR yellow]Reset local Kodi database[/COLOR]" +msgstr "[COLOR yellow]Сбросить локальную базу данных Kodi[/COLOR]" + +msgctxt "#30240" +msgid "Enable watched/resume status sync" +msgstr "Включить синхронизацию статуса просмотра / возобновления" + +msgctxt "#30241" +msgid "DB Sync Indication:" +msgstr "Индикатор синхронизации ДБ:" + +msgctxt "#30242" +msgid "Play Count Sync Indication:" +msgstr "Индикатор синхронизации количества воспроизведений:" + +msgctxt "#30243" +msgid "Enable HTTPS" +msgstr "Включить HTTPS" + +msgctxt "#30245" +msgid "Force Transcoding Codecs" +msgstr "Принудительные кодеки транскодирования" + +msgctxt "#30246" +msgid "Enable Netflix style next up notification" +msgstr "Включить уведомления в стиле Netflix" + +msgctxt "#30247" +msgid " - The number of seconds before the end to show the notification" +msgstr "- количество секунд до конца для показа уведомления" + +msgctxt "#30248" +msgid "Show Emby Info dialog on play/select action" +msgstr "Показать диалог информации Emby при выборе/воспроизведении" + +msgctxt "#30249" +msgid "Enable server connection message on startup" +msgstr "Включить сообщение о подключении к серверу при запуске" + +msgctxt "#30251" +msgid "Recently added Home Videos" +msgstr "Недавно добавленное Домашнее видео" + +msgctxt "#30252" +msgid "Recently added Photos" +msgstr "Недавно добавленные Фотографии" + +msgctxt "#30253" +msgid "Favorite Home Videos" +msgstr "Избранное Домашнее видео" + +msgctxt "#30254" +msgid "Favorite Photos" +msgstr "Избранные фотографии" + +msgctxt "#30255" +msgid "Favorite Albums" +msgstr "Избранные Альбомы" + +msgctxt "#30256" +msgid "Recently added Music videos" +msgstr "Недавно добавленные Видеоклипы" + +msgctxt "#30257" +msgid "In progress Music videos" +msgstr "Просматриваемые Видеоклипы" + +msgctxt "#30258" +msgid "Unwatched Music videos" +msgstr "Непросмотренные Видеоклипы" + +# Default views +msgctxt "#30300" +msgid "Active" +msgstr "Активно" + +msgctxt "#30301" +msgid "Clear Settings" +msgstr "Сбросить настройки" + +msgctxt "#30302" +msgid "Movies" +msgstr "Фильмы" + +msgctxt "#30303" +msgid "BoxSets" +msgstr "Сборники" + +msgctxt "#30304" +msgid "Trailers" +msgstr "Трейлеры" + +msgctxt "#30305" +msgid "Series" +msgstr "Серии" + +msgctxt "#30306" +msgid "Seasons" +msgstr "Сезоны" + +msgctxt "#30307" +msgid "Episodes" +msgstr "Эпизоды" + +msgctxt "#30308" +msgid "Music Artists" +msgstr "Исполнители" + +msgctxt "#30309" +msgid "Music Albums" +msgstr "Альбомы" + +msgctxt "#30310" +msgid "Music Videos" +msgstr "Видеоклипы" + +msgctxt "#30311" +msgid "Music Tracks" +msgstr "Треки" + +msgctxt "#30312" +msgid "Channels" +msgstr "Каналы" + +# contextmenu +msgctxt "#30401" +msgid "Plex options" +msgstr "Параметры Plex" + +msgctxt "#30402" +msgid "Clear like for this item" +msgstr "Убрать лайк с элемента" + +msgctxt "#30403" +msgid "Like this item" +msgstr "Like на элемент" + +msgctxt "#30404" +msgid "Dislike this item" +msgstr "Dislike на элемент" + +msgctxt "#30405" +msgid "Add to Plex favorites" +msgstr "Добавить в избранное" + +msgctxt "#30406" +msgid "Remove from Plex favorites" +msgstr "Удалить из избранного" + +msgctxt "#30407" +msgid "Set custom song rating" +msgstr "Задать свой рейтинг песни" + +msgctxt "#30408" +msgid "Plex addon settings" +msgstr "Настройки аддонов Plex" + +msgctxt "#30409" +msgid "Delete item from server" +msgstr "Удалить элемент с сервера" + +msgctxt "#30410" +msgid "Refresh this item" +msgstr "Обновить этот элемент" + +msgctxt "#30411" +msgid "Set custom song rating (0-5)" +msgstr "Установить пользовательский рейтинг песни (0-5)" + +msgctxt "#30412" +msgid "Force transcode" +msgstr "Принудительное транскодирование" + +msgctxt "#30413" +msgid "Enable Plex context menu in Kodi" +msgstr "Включить контекстное меню Plex в Kodi" + +msgctxt "#30414" +msgid "" +"Could not delete the Plex item. Is item deletion enabled on the Plex Media " +"Server?" +msgstr "Не возможно удалить элемент. На сервере Plex разрешено удаление?" + +msgctxt "#30415" +msgid "Start playback via PMS" +msgstr "Начать воспроизведение через Plex" + +msgctxt "#30416" +msgid "Settings for the Plex Server" +msgstr "Настройки для сервера Plex" + +# add-on settings +msgctxt "#30500" +msgid "Verify Host SSL Certificate (more secure)" +msgstr "Проверять SSL сертификат хоста (более безопасно)" + +msgctxt "#30501" +msgid "Client SSL certificate" +msgstr "SSL сертификат клиента" + +msgctxt "#30502" +msgid "Use alternate address" +msgstr "Использовать альтернативный адрес" + +msgctxt "#30503" +msgid "Alternate Server Address" +msgstr "Адрес альтернативного сервера" + +msgctxt "#30504" +msgid "Use alternate device Name" +msgstr "Использовать альтернативное имя устройства" + +msgctxt "#30505" +msgid "[COLOR yellow]Reset login attempts[/COLOR]" +msgstr "[COLOR yellow]Сбросить попытки входа[/COLOR]" + +msgctxt "#30506" +msgid "Sync Options" +msgstr "Синхронизация" + +msgctxt "#30507" +msgid "Show syncing progress" +msgstr "Отображать прогресс синхронизации" + +msgctxt "#30508" +msgid "Sync empty TV Shows" +msgstr "Синхронизировать пустые Сериалы" + +msgctxt "#30509" +msgid "Enable Music Library" +msgstr "Включить музыкальную библиотеку" + +msgctxt "#30510" +msgid "Direct stream music library" +msgstr "Прямое воспроизведение музыки из библиотеки" + +msgctxt "#30511" +msgid "Playback Mode" +msgstr "Режим воспроизведения" + +msgctxt "#30512" +msgid "Force artwork caching" +msgstr "Принудительное кеширование иллюстраций" + +msgctxt "#30513" +msgid "Limit artwork cache threads (recommended for rpi)" +msgstr "" +"Ограничить потоки кешированиa иллюстраций (рекомендуется для Raspberry)" + +msgctxt "#30514" +msgid "Enable fast startup (requires server plugin)" +msgstr "Включить быстрый запуск (требуется серверный плагин)" + +msgctxt "#30515" +msgid "Maximum items to request from the server at once" +msgstr "Число элементов запрашиваемых с сервера за раз" + +msgctxt "#30516" +msgid "Playback" +msgstr "Воспроизведение" + +msgctxt "#30517" +msgid "[COLOR yellow]Enter network credentials[/COLOR]" +msgstr "[COLOR yellow]Введите сетевые учетные данные[/COLOR]" + +msgctxt "#30518" +msgid "Enable Plex Trailers (Plexpass is needed)" +msgstr "Включить Plex Trailers (требуется Plexpass)" + +msgctxt "#30519" +msgid "Ask to play trailers" +msgstr "Предложить воспроизвести трейлер" + +msgctxt "#30520" +msgid "" +"Skip Plex delete confirmation for the context menu (use at your own risk)" +msgstr "" +"Пропустить подтверждение удаления с Plex из контекстного меню (используйте " +"на свой страх и риск)" + +msgctxt "#30521" +msgid "Jump back on resume (in seconds)" +msgstr "Вернуться назад при возобновлении (в секундах)" + +msgctxt "#30522" +msgid "Force transcode h265/HEVC" +msgstr "Транскодировать h265/HEVC" + +msgctxt "#30523" +msgid "Music metadata options (not compatible with direct stream)" +msgstr "Опции метаданных для музыки (не доступно при прямом воспроизведении)" + +msgctxt "#30524" +msgid "Import music song rating directly from files" +msgstr "Импортировать рейтинг песни из файла" + +msgctxt "#30525" +msgid "Convert music song rating to Emby rating" +msgstr "Преобразование рейтинга музыкальной песни в рейтинг Emby" + +msgctxt "#30526" +msgid "Allow rating in song files to be updated" +msgstr "Разрешить обновлять рейтинг в файлах музыки" + +msgctxt "#30527" +msgid "Ignore specials in next episodes" +msgstr "Игнорировать бонусные в следующих сериях" + +msgctxt "#30528" +msgid "Permanent users to add to the session" +msgstr "Добавить пользователя в сессию навсегда" + +msgctxt "#30529" +msgid "Startup delay (in seconds)" +msgstr "Задержка запуска (в секундах)" + +msgctxt "#30530" +msgid "Enable server restart message" +msgstr "Включить уведомление о перезапуске сервера" + +msgctxt "#30531" +msgid "Enable new content notification" +msgstr "Включить уведомление о новом контенте" + +msgctxt "#30532" +msgid "Duration of the video library pop up (in seconds)" +msgstr "Длительность всплывающего окна в библиотеке видео (в секундах)" + +msgctxt "#30533" +msgid "Duration of the music library pop up (in seconds)" +msgstr "Длительность всплывающего окна в библиотеке музыки (в секундах)" + +msgctxt "#30534" +msgid "Server messages" +msgstr "Сообщения сервера" + +msgctxt "#30535" +msgid "" +"[COLOR yellow]Generate a new unique device Id (e.g. when cloning " +"Kodi)[/COLOR]" +msgstr "" +"[COLOR yellow]Сгенерировать новый ID (например если вы скопировали Kodi на " +"новое устройство)[/COLOR]" + +msgctxt "#30536" +msgid "Users must log in every time Kodi restarts" +msgstr "Пользователь должен входить при каждом запуске Kodi" + +msgctxt "#30537" +msgid "RESTART KODI IF YOU MAKE ANY CHANGES" +msgstr "ПЕРЕЗАПУСТИТЕ KODI, ЕСЛИ ВНОСИЛИ КАКИЕ-ТО ИЗМЕНЕНИЯ" + +msgctxt "#30538" +msgid "Complete Re-Sync necessary" +msgstr "Необходима полная пересинхронизация" + +msgctxt "#30539" +msgid "Download additional art from FanArtTV" +msgstr "Загрузить дополнительные иллюстрации с FanArtTV" + +msgctxt "#30540" +msgid "Download movie set/collection art from FanArtTV" +msgstr "Загружать иллюстрации сборников с FanArtTV" + +msgctxt "#30541" +msgid "Don't ask to pick a certain stream/quality" +msgstr "Не просить выбрать качество потока" + +msgctxt "#30542" +msgid "Always pick best quality for trailers" +msgstr "Всегда выбирать лучшее качество для трейлеров" + +msgctxt "#30543" +msgid "Kodi runs on a low-power device (e.g. Raspberry Pi)" +msgstr "Kodi запущен на слабом устройстве (например Raspberry Pi)" + +msgctxt "#30544" +msgid "Artwork" +msgstr "Иллюстрации" + +msgctxt "#30545" +msgid "Force transcode pictures" +msgstr "Принудительно транскодировать изображения" + +# service add-on +msgctxt "#33000" +msgid "Welcome" +msgstr "Добро пожаловать" + +msgctxt "#33001" +msgid "Error connecting" +msgstr "Ошибка соединения" + +msgctxt "#33002" +msgid "Server is unreachable" +msgstr "Сервер недоступен" + +msgctxt "#33003" +msgid "Server is online" +msgstr "Сервер в сети" + +msgctxt "#33004" +msgid "items added to playlist" +msgstr "элементы добавлены в плейлист" + +msgctxt "#33005" +msgid "items queued to playlist" +msgstr "элементы добавлены в очередь плейлиста" + +msgctxt "#33006" +msgid "Server is restarting" +msgstr "Сервер перезагружается" + +msgctxt "#33007" +msgid "Access is enabled" +msgstr "Доступ разрешен" + +msgctxt "#33008" +msgid "Enter password for user:" +msgstr "Введите пароль Пользователя:" + +msgctxt "#33009" +msgid "Invalid username or password" +msgstr "Неверное имя пользователя или пароль" + +msgctxt "#33010" +msgid "Failed to authenticate too many times. Reset in the settings." +msgstr "Множественная ошибка авторизации. Сбросьте настройки." + +msgctxt "#33011" +msgid "Unable to direct play" +msgstr "Прямое воспроизведение невозможно" + +msgctxt "#33012" +msgid "Direct play failed 3 times. Enabled play from HTTP." +msgstr "" +"Прямое воспроизведение не удалось 3 раза. Установлено воспроизведение через " +"HTTP." + +msgctxt "#33013" +msgid "Choose the audio stream" +msgstr "Выберите звуковую дорожку" + +msgctxt "#33014" +msgid "Choose the subtitles stream" +msgstr "Выберите субтитры" + +msgctxt "#33015" +msgid "Delete file from your Emby server?" +msgstr "Удалить файл с Emby-сервера?" + +msgctxt "#33016" +msgid "Play trailers?" +msgstr "Воспроизвести трейлер?" + +msgctxt "#33017" +msgid "Gathering movies from:" +msgstr "Получение фильмов с" + +msgctxt "#33018" +msgid "Gathering boxsets" +msgstr "Получение сборников" + +msgctxt "#33019" +msgid "Gathering music videos from:" +msgstr "Получение видеоклипов с" + +msgctxt "#33020" +msgid "Gathering tv shows from:" +msgstr "Получение сериалов с" + +msgctxt "#33021" +msgid "Gathering:" +msgstr "Получение:" + +msgctxt "#33022" +msgid "" +"Detected the database needs to be recreated for this version of Emby for " +"Kodi. Proceed?" +msgstr "" +"Обнаружено что база данных должна быть пересоздана для этой версии Emby for " +"Kodi. Выполнить?" + +msgctxt "#33023" +msgid "Emby for Kodi may not work correctly until the database is reset." +msgstr "Emby for Kodi может работать неправильно до сброса базы данных." + +msgctxt "#33024" +msgid "" +"Cancelling the database syncing process. The current Kodi version is " +"unsupported." +msgstr "Отмена синхронизации базы данных. Эта версия Kodi не поддерживается." + +msgctxt "#33025" +msgid "completed in:" +msgstr "выполнено:" + +msgctxt "#33026" +msgid "Comparing movies from:" +msgstr "Сравнение фильмов с:" + +msgctxt "#33027" +msgid "Comparing boxsets" +msgstr "Сравнение сборников" + +msgctxt "#33028" +msgid "Comparing music videos from:" +msgstr "Сравнение видеоклипов с:" + +msgctxt "#33029" +msgid "Comparing tv shows from:" +msgstr "Сравнение сериалов с:" + +msgctxt "#33030" +msgid "Comparing episodes from:" +msgstr "Сравнение серий с:" + +msgctxt "#33031" +msgid "Comparing:" +msgstr "Сравнение:" + +msgctxt "#33032" +msgid "" +"Failed to generate a new device Id. See your logs for more information." +msgstr "" +"Генерация нового ID устройства неудачна. Смотрите логи для подробностей." + +msgctxt "#33033" +msgid "Kodi will now restart to apply the changes." +msgstr "Kodi будет перезапущен для применения изменений." + +msgctxt "#33041" +msgid "" +"Delete file(s) from Plex Server? This will also delete the file(s) from " +"disk!" +msgstr "Удалить файл(ы) с сервера Plex? Это также удалит их с диска!" + +# New to Plex +msgctxt "#39000" +msgid "- Number of trailers to play before a movie" +msgstr "- Количество трейлеров, проигрываемых перед фильмом." + +msgctxt "#39001" +msgid "Boost audio when transcoding" +msgstr "Усиливать звук при транскодировании" + +msgctxt "#39002" +msgid "Burnt-in subtitle size" +msgstr "Размер субтитров внедряемых в видео" + +msgctxt "#39003" +msgid "Limit download sync threads (rec. for rpi: 1)" +msgstr "Количество потоков синхронизации(для RPi:1)" + +msgctxt "#39004" +msgid "Enable Plex Companion (restart Kodi!)" +msgstr "Включить Plex Companion (перезапустите Kodi!)" + +msgctxt "#39005" +msgid "Plex Companion Port (change only if needed)" +msgstr "Порт Plex Companion (меняйте только если необходимо)" + +msgctxt "#39006" +msgid "Activate Plex Companion debug log" +msgstr "Включить лог отладки Plex Companion" + +msgctxt "#39007" +msgid "Activate Plex Companion GDM debug log" +msgstr "Включить лог отладки Plex Companion для GDM" + +msgctxt "#39008" +msgid "Plex Companion: Allows flinging media to Kodi through Plex" +msgstr "Plex Companion: позволяет отправлять медиа в Kodi через Plex" + +msgctxt "#39009" +msgid "Could not login to plex.tv. Please try signing in again." +msgstr "Не удалось войти в plex.tv. Пожалуйста попробуйте ещё раз" + +msgctxt "#39010" +msgid "Problems connecting to plex.tv. Network or internet issue?" +msgstr "Невозможно подключиться к plex.tv. Проблемы с интернетом?" + +msgctxt "#39011" +msgid "Could not find any Plex server in the network. Aborting..." +msgstr "Не найдено ни одного сервера Plex в сети. Отмена..." + +msgctxt "#39012" +msgid "Choose your Plex server" +msgstr "Выберите Ваш сервер Plex" + +msgctxt "#39013" +msgid "Not yet authorized for Plex server " +msgstr "Не авторизован на сервере Plex" + +msgctxt "#39014" +msgid "Please sign in to plex.tv." +msgstr "Пожалуйста войдите в plex.tv." + +msgctxt "#39015" +msgid "Problems connecting to server. Pick another server?" +msgstr "Проблемы при подключении к серверу. Выбрать другой?" + +msgctxt "#39016" +msgid "" +"Disable Plex music library? (It is HIGHLY recommended to use Plex music only" +" with direct paths for large music libraries. Kodi might crash otherwise)" +msgstr "" +"Отключить библиотеку музыки в Plex? (это ЧРЕЗВЫЧАЙНО рекомендуемо при " +"использовании прямых путей и большой библиотеке музыки. Kodi может " +"крашиться)" + +msgctxt "#39017" +msgid "" +"Would you now like to go to the plugin's settings to fine-tune PKC? You will" +" need to RESTART Kodi!" +msgstr "" +"Хотите войти в настройки чтобы донастроить PKC? Вам нужно будет " +"ПЕРЕЗАПУСТИТЬ Kodi!" + +msgctxt "#39018" +msgid "[COLOR yellow]Repair local database (force update all content)[/COLOR]" +msgstr "" +"[COLOR yellow]Исправить базу данных (принудительно обновить весь " +"контент)[/COLOR]" + +msgctxt "#39019" +msgid "[COLOR red]Partial or full reset of Database and PKC[/COLOR]" +msgstr "[COLOR red]Частичный или полный сброс базы данных и PKC[/COLOR]" + +msgctxt "#39020" +msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]" +msgstr "" +"[COLOR yellow]Кешировать все изображения сейчас в Kodi texture cache[/COLOR]" + +msgctxt "#39021" +msgid "[COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR]" +msgstr "[COLOR yellow]Синхронизировать музыку темы из Emby в Kodi[/COLOR]" + +msgctxt "#39022" +msgid "local" +msgstr "local" + +msgctxt "#39023" +msgid "Failed to authenticate. Did you login to plex.tv?" +msgstr "Ошибка авторизации. Вы вошли в plex.tv?" + +msgctxt "#39025" +msgid "Automatically log into plex.tv on startup" +msgstr "Автоматический вход в plex.tv при запуске" + +msgctxt "#39026" +msgid "Enable constant background sync" +msgstr "Включить фоновую синхронизацию" + +msgctxt "#39027" +msgid "Playback Mode" +msgstr "Режим воспроизведения" + +msgctxt "#39028" +msgid "" +"CAUTION! If you choose \"Native\" mode , you might loose access to certain " +"Plex features such as: Plex trailers and transcoding options. ALL Plex " +"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or " +"\\\\myNAS/mymovie.mkv)!" +msgstr "" +"ВНИМАНИЕ! Если выберете режим \"Native\" вы потеряете доступ к некоторым " +"функциям Plex, таким как: трейлеры и опции транскодирования. ВСЕ библиотеки " +"Plex должны иметь прямые пути (например smb://myNAS/mymovie.mkv либо " +"\\\\myNAS/mymovie.mkv)!" + +msgctxt "#39029" +msgid "Network credentials" +msgstr "Сетевые учетные данные" + +msgctxt "#39030" +msgid "" +"Add network credentials to allow Kodi access to your content? Note: Skipping" +" this step may generate a message during the initial scan of your content if" +" Kodi can't locate your content." +msgstr "" +"Добавить сетевые учетные данные, чтобы дать доступ Kodi к вашему контенту? " +"Если пропустить этот шаг могут появляться сообщения при первичном " +"сканировании контента, если Kodi не сможет его найти" + +msgctxt "#39031" +msgid "Kodi can't locate file: " +msgstr "Kodi не может найти файл: " + +msgctxt "#39032" +msgid "" +"Please verify the path. You may need to verify your network credentials in " +"the add-on settings or use different Plex paths. Stop syncing?" +msgstr "" +"Пожалуйста проверьте путь. Возможно Вам нужно ввести сетевые учетные данные " +"в настройках плагина либо использовать другие пути Plex. Остановить " +"синхронизацию?" + +msgctxt "#39033" +msgid "" +"Transform Plex UNC library paths \\\\myNas\\mymovie.mkv automatically to smb" +" paths, smb://myNas/mymovie.mkv? (recommended)" +msgstr "" +"Изменить UNC пути библиотек Plex \\\\myNas\\mymovie.mkv автоматически в " +"Samba пути: smb://myNas/mymovie.mkv? (рекомендуется)" + +msgctxt "#39034" +msgid "Replace Plex UNC paths \\\\myNas with smb://myNas" +msgstr "Заменить UNC пути Plex \\\\myNas на smb://myNas" + +msgctxt "#39035" +msgid "" +"Replace Plex paths /volume1/media or \\\\myserver\\media with custom SMB " +"paths smb://NAS/mystuff" +msgstr "" +"Заменить пути Plex /volume1/media либо \\\\myserver\\media на " +"пользовательские Samba пути smb://NAS/mystuff" + +msgctxt "#39037" +msgid "Original Plex MOVIE path to replace:" +msgstr "Исходный путь ФИЛЬМОВ Plex для замены:" + +msgctxt "#39038" +msgid "Replace Plex MOVIE with:" +msgstr "Заменить путь ФИЛЬМОВ Plex на:" + +msgctxt "#39039" +msgid "Original Plex TV SHOWS path to replace:" +msgstr "Исходный путь СЕРИАЛОВ Plex для замены:" + +msgctxt "#39040" +msgid "Replace Plex TV SHOWS with:" +msgstr "Заменить путь СЕРИАЛОВ Plex на:" + +msgctxt "#39041" +msgid "Original Plex MUSIC path to replace:" +msgstr "Исходный путь МУЗЫКИ Plex для замены:" + +msgctxt "#39042" +msgid "Replace Plex MUSIC with:" +msgstr "Заменить путь МУЗЫКИ Plex на:" + +msgctxt "#39043" +msgid "" +"Go a step further and completely replace all original Plex library paths " +"(/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?" +msgstr "" +"Выполнить ещё один шаг и полностью заменить все оригинальные пути библиотек " +"Plex (/volume1/media) на свои Samba пути (smb://NAS/MyStuff)?" + +msgctxt "#39044" +msgid "" +"Please enter your custom smb paths in the settings under \"Sync Options\" " +"and then restart Kodi" +msgstr "" +"Пожалуйста введите свои Samba пути в настройках \"Параметры синхронизации\" " +"и перезапустите Kodi" + +msgctxt "#39045" +msgid "Original Plex PHOTO path to replace:" +msgstr "Исходный путь ФОТОГРАФИЙ Plex для замены:" + +msgctxt "#39046" +msgid "Replace Plex PHOTO with:" +msgstr "Заменить путь ФОТОГРАФИЙ Plex на:" + +msgctxt "#39047" +msgid "On Deck: Append show title to episode" +msgstr "Текущие: Показывать название эпизода" + +msgctxt "#39048" +msgid "On Deck: Append season- and episode-number SxxExx" +msgstr "Текущие: Показывать номер сезона и эпизода как SxxExx" + +msgctxt "#39049" +msgid "Nothing works? Try a full reset!" +msgstr "Ничего не работает? Попробуйте общий сброс!" + +msgctxt "#39050" +msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]" +msgstr "[COLOR yellow]Выберите сервер Plex из списка[/COLOR]" + +msgctxt "#39051" +msgid "Wait before sync new/changed PMS item [s]" +msgstr "Ожидание новый/измененный элемент [с]" + +msgctxt "#39052" +msgid "Background Sync" +msgstr "Фоновая синхронизация" + +msgctxt "#39053" +msgid "Do a full library sync every x minutes" +msgstr "Полная синхронизация библиотеки каждые x минут" + +msgctxt "#39054" +msgid "remote" +msgstr "remote" + +msgctxt "#39055" +msgid "Searching for Plex Server" +msgstr "Поиск сервера Plex" + +msgctxt "#39056" +msgid "Used by Sync and when attempting to Direct Play" +msgstr "Используется при синхронизации и прямом воспроизведении" + +msgctxt "#39057" +msgid "Customize Paths" +msgstr "Изменить пути" + +msgctxt "#39058" +msgid "Extend Plex TV Series \"On Deck\" view to all shows" +msgstr "В \"Текущем\" показывать все сериалы" + +msgctxt "#39059" +msgid "Recently Added: Append show title to episode" +msgstr "Недавно добавлено: Показывать название эпизода" + +msgctxt "#39060" +msgid "Recently Added: Append season- and episode-number SxxExx" +msgstr "Недавно добавлено: Показывать номер сезона и эпизода как SxxExx" + +msgctxt "#39061" +msgid "" +"Would you like to download additional artwork from FanArtTV in the " +"background?" +msgstr "Хотите загружать дополнительные иллюстрации с FanArtTV в фоне?" + +msgctxt "#39062" +msgid "Sync when screensaver is deactivated" +msgstr "Синхронизация если заставка отключена" + +msgctxt "#39063" +msgid "Force Transcode Hi10P" +msgstr "Принудительно транскодировать Hi10P" + +msgctxt "#39064" +msgid "Recently Added: Also show already watched episodes" +msgstr "Недавно добавлено: также показывать просмотренные эпизоды" + +msgctxt "#39066" +msgid "" +"Recently Added: Also show already watched movies (Refresh Plex " +"playlist/nodes!)" +msgstr "" +"Недавно добавлено: также показывать просмотренные фильмы (обновите " +"плейлисты/списки Plex)" + +msgctxt "#39067" +msgid "Your current Plex Media Server:" +msgstr "Ваш текущий сервер Plex:" + +msgctxt "#39068" +msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]" +msgstr "[COLOR yellow]Вручную ввести адрес сервера Plex[/COLOR]" + +msgctxt "#39069" +msgid "Current address:" +msgstr "Текущий адрес:" + +msgctxt "#39070" +msgid "Current port:" +msgstr "Текущий порт:" + +msgctxt "#39071" +msgid "Current plex.tv status:" +msgstr "Текущий статус на plex.tv:" + +msgctxt "#39072" +msgid "" +"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." +msgstr "" +"Kodi установлен на слабое устройство типа Raspberry Pi? Если да, мы уменьшим" +" нагрузку чтобы предотвратить его сбой." + +msgctxt "#39073" +msgid "Appearance Tweaks" +msgstr "Дополнительно" + +msgctxt "#39074" +msgid "TV Shows" +msgstr "Сериалы" + +msgctxt "#39075" +msgid "Always use default Plex subtitle if possible" +msgstr "Использовать субтитры по умолчанию из Plex, если доступны" + +msgctxt "#39076" +msgid "" +"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" +msgstr "" +"Если вы используете несколько библиотек одного типа, например \"Фильмы " +"детей\" и \"Фильмы родителей\" не забудьте почитать Wiki: " +"https://goo.gl/JFtQV9 (пока на английском)" + +msgctxt "#39077" +msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")" +msgstr "Количество элементов, отображаемых в виджетах (например \"Текущие\")" + +msgctxt "#39078" +msgid "Plex Companion Update Port (change only if needed)" +msgstr "Порт обновления Plex Companion (меняйте только если необходимо)" + +msgctxt "#39079" +msgid "" +"Plex Companion could not open the GDM port. Please change it in the PKC " +"settings." +msgstr "" +"Plex Companion не может открыть порт GDM. Смените его в настройках PKC." + +# Plex Entrypoint.py +msgctxt "#39200" +msgid "Log-out Plex Home User " +msgstr "Выйти из Plex" + +msgctxt "#39201" +msgid "Settings" +msgstr "Настройки" + +msgctxt "#39202" +msgid "Network credentials" +msgstr "Сетевые учетные данные" + +msgctxt "#39203" +msgid "Refresh Plex playlists/nodes" +msgstr "Обновить плейлисты/списки Plex" + +msgctxt "#39204" +msgid "Perform manual library sync" +msgstr "Синхронизировать вручную" + +msgctxt "#39205" +msgid "Unable to run the sync, the add-on is not connected to a Plex server." +msgstr "" +"Невозможно запустить синхронизацию, плагин не подключен к серверу Plex." + +msgctxt "#39206" +msgid "" +"Plex might lock your account if you fail to log in too many times. Proceed " +"anyway?" +msgstr "" +"Plex заблокирует ваш аккаунт при нескольких неудачных авторизациях. Всё " +"равно продолжить?" + +msgctxt "#39207" +msgid "Resetting PMS connections, please wait" +msgstr "Сброс соединений Plex, пожалуйста подождите" + +msgctxt "#39208" +msgid "Failed to reset PKC. Try to restart Kodi." +msgstr "Невозможно сбросить PKC. Попробуйте перезапустить Kodi." + +msgctxt "#39209" +msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]" +msgstr "" +"[COLOR yellow]Переключить авторизацию plex.tv(войти или выйти)[/COLOR]" + +msgctxt "#39210" +msgid "Not yet connected to Plex Server" +msgstr "Не подключен к серверу Plex" + +msgctxt "#39211" +msgid "Watch later" +msgstr "Смотреть позже" + +msgctxt "#39213" +msgid "is offline" +msgstr "Нет соединения" + +msgctxt "#39214" +msgid "Even though we signed in to plex.tv, we could not authorize for PMS" +msgstr "" +"Несмотря на то, что логин в plex.tv удачен не удалось авторизоваться на " +"сервере Plex" + +msgctxt "#39215" +msgid "Enter your Plex Media Server's IP or URL, Examples are:" +msgstr "Введите IP или URL Вашего Plex-сервера. Например:" + +msgctxt "#39217" +msgid "" +"Does your Plex Media Server support SSL connections? (https instead of " +"http)?" +msgstr "Ваш Plex-сервер поддерживает SSL соединение (https вместо http)?" + +msgctxt "#39218" +msgid "Error contacting PMS" +msgstr "Ошибка при обращении к серверу Plex" + +msgctxt "#39219" +msgid "Abort (Yes) or save address anyway (No)?" +msgstr "Отменить (Да) или сохранить адрес (Нет)?" + +msgctxt "#39220" +msgid "connected" +msgstr "подключен" + +msgctxt "#39221" +msgid "plex.tv toggle successful" +msgstr "переключение plex.tv удачно" + +msgctxt "#39222" +msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]" +msgstr "[COLOR yellow]Найти отсутствующие иллюстрации на FanartTV[/COLOR]" + +msgctxt "#39223" +msgid "" +"Only look for missing fanart or refresh all fanart? The scan will take quite" +" a while and happen in the background." +msgstr "" +"Искать только отсутствующие иллюстрации или обновить все? Сканирование " +"займёт довольно много времени и будет выполнено в фоне." + +msgctxt "#39224" +msgid "Refresh all" +msgstr "Обновить все" + +msgctxt "#39225" +msgid "Missing only" +msgstr "Недостающие" + +# Message in the PKC settings if user has not logged in to plex.tv +msgctxt "#39226" +msgid "Not logged in to plex.tv" +msgstr "Вы не вошли в plex.tv" + +# Message in the PKC settings if user is logged in to plex.tv +msgctxt "#39227" +msgid "Logged in to plex.tv" +msgstr "Вы вошли в plex.tv" + +# Message in the PKC settings to display the plex.tv username. Leave the colon +# : +msgctxt "#39228" +msgid "Plex user:" +msgstr "Пользователь Plex:" + +# Plex Artwork.py +msgctxt "#39250" +msgid "" +"Running the image cache process can take some time. It will happen in the " +"background. Are you sure you want continue?" +msgstr "" +"Процесс кеширования изображений займёт продолжительное время и будет " +"выполнен в фоне. Вы уверены что хотите продолжить?" + +msgctxt "#39251" +msgid "Reset all existing cache data first?" +msgstr "Сначала удалить весь имеющийся кеш?" + +# Plex PlexAPI.py +msgctxt "#39300" +msgid ": Enter plex.tv username. Or nothing to cancel." +msgstr ": Введите имя пользователя plex.tv. Либо ничего, чтобы отменить." + +msgctxt "#39301" +msgid "Enter password for plex.tv user " +msgstr "Введите пароль пользователя plex.tv" + +msgctxt "#39302" +msgid "Could not sign in user " +msgstr "Ошибка входа" + +msgctxt "#39303" +msgid "Problems trying to contact plex.tv. Try again later" +msgstr "Не удалось подключиться к plex.tv. Попробуйте позже" + +msgctxt "#39304" +msgid "Go to https://plex.tv/pin and enter the code: " +msgstr "Зайдите на https://plex.tv/pin и введите код:" + +msgctxt "#39305" +msgid "Could not sign in to plex.tv. Try again later" +msgstr "Ошибка входа в plex.tv. Попробуйте позже" + +msgctxt "#39306" +msgid ": Select User" +msgstr ": Выберите пользователя" + +msgctxt "#39307" +msgid "Enter PIN for user " +msgstr "Введите PIN пользователя" + +msgctxt "#39308" +msgid "Could not log in user " +msgstr "Ошибка входа" + +msgctxt "#39309" +msgid "Please try again." +msgstr "Попробуйте ещё." + +msgctxt "#39310" +msgid "unknown" +msgstr "неизвестно" + +msgctxt "#39311" +msgid "or press No to not sign in." +msgstr "или нажмите Нет, чтобы отменить вход." + +# Plex Librarysync.py +msgctxt "#39400" +msgid "" +"Library sync thread has crashed. You should restart Kodi now. Please report " +"this on the forum" +msgstr "" +"Синхронизация библиотеки завершилась с ошибкой. Вы должны перезапустить " +"Kodi. Пожалуйста, напишите об этом на форуме" + +msgctxt "#39401" +msgid "" +"Detected Kodi database needs to be recreated for this version. This might " +"take a while. Proceed?" +msgstr "" +"База данных Kodi должна быть пересоздана для этой версии. Это займёт время. " +"Продолжить?" + +msgctxt "#39402" +msgid " may not work correctly until the database is reset." +msgstr "может работать неправильно до сброса базы данных." + +msgctxt "#39403" +msgid "" +"Cancelling the database syncing process. Current Kodi version is " +"unsupported. Please verify your logs for more info." +msgstr "" +"Синхронизация отменена, текущая версия Kodi не поддерживается. Больше " +"информации Вы найдёте в логах." + +msgctxt "#39404" +msgid "" +"Startup syncing process failed repeatedly. Try restarting Kodi. Stopping " +"Sync for now." +msgstr "" +"Процесс синхронизации несколько раз был неудачен. Попробуйте перезапустить " +"Kodi. На данный момент синхронизация отключена." + +msgctxt "#39405" +msgid "Plex playlists/nodes refreshed" +msgstr "Завершено обновление плейлистов/списков Plex" + +msgctxt "#39406" +msgid "Plex playlists/nodes refresh failed" +msgstr "Обновление плейлистов/списков Plex не удалось" + +msgctxt "#39407" +msgid "Full library sync finished" +msgstr "Полная синхронизация библиотеки завершена" + +msgctxt "#39408" +msgid "" +"Sync had to skip some items because they could not be processed. Kodi may be" +" instable now!! Please post your Kodi logs to the Plex forum." +msgstr "" +"Синхронизацию пришлось прекратить, потому что некоторые элементы не могут " +"быть обработаны. Kodi работает нестабильно, опубликуйте логи работы Kodi на " +"форуме Plex." + +msgctxt "#39409" +msgid "" +"The Plex Server did not like you asking for so much data at once and " +"returned ERRORS. Try lowering the number of sync download threads in the " +"settings. Skipped some items for now." +msgstr "" +"Сервер Plex получил слишком много запросов одновременно и выдал ошибку. " +"Попробуйте уменьшить количество потоков синхронизации в настройках. " +"Некоторые элементы пропущены." + +msgctxt "#39410" +msgid "ERROR in library sync" +msgstr "Ошибка синхронизации библиотеки" + +# Plex videonodes.py +msgctxt "#39500" +msgid "On Deck" +msgstr "Текущие" + +msgctxt "#39501" +msgid "Collections" +msgstr "Коллекции" + +# Plex utils.py +msgctxt "#39600" +msgid "" +"Are you sure you want to reset your local Kodi database? A re-sync of the " +"Plex data will take time afterwards." +msgstr "" +"Вы действительно уверены что хотите удалить локальную базу данных Kodi? " +"Потребуется повторная синхронизация с Plex." + +msgctxt "#39601" +msgid "Could not stop the database from running. Please try again later." +msgstr "Не удалось остановить работу базы данных. Попробуйте позже." + +msgctxt "#39602" +msgid "Remove all cached artwork? (recommended!)" +msgstr "Удалить все кешированные иллюстрации? (рекомендуется)" + +msgctxt "#39603" +msgid "" +"Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended " +"and unnecessary!)" +msgstr "" +"Сбросить все настройки PlexKodiConnect Addon? (обычно это не нужно и НЕ " +"рекомендуется!)" + +msgctxt "#39700" +msgid "Amazon Alexa (Voice Recognition)" +msgstr "Amazon Alexa (Распознавание голоса)" + +msgctxt "#39701" +msgid "Activate Alexa" +msgstr "Активировать Amazon Alexa" + +msgctxt "#39702" +msgid "Browse by folder" +msgstr "Просмотр по папкам" + +# For use with addon.xml (PKC metadata for Kodi, e.g. description) +# Addon Summary +msgctxt "#39703" +msgid "Native Integration of Plex into Kodi" +msgstr "Нативная интеграция сервера Plex в Kodi" + +# For use with addon.xml (PKC metadata for Kodi, e.g. description) +# Addon Description +msgctxt "#39704" +msgid "" +"Connect Kodi to your Plex Media Server. 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!" +msgstr "" +"Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете" +" своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы " +"данных музыки и видео в Kodi (так как плагин напрямую их изменяет). " +"Используйте на свой страх и риск" + +# For use with addon.xml (PKC metadata for Kodi, e.g. description) +# Addon Disclaimer +msgctxt "#39705" +msgid "Use at your own risk" +msgstr "Используйте на свой страх и риск" + +# If user gets prompted to choose between several subtitles. Leave the number +# one at the beginning of the string! +msgctxt "#39706" +msgid "1 No subtitles" +msgstr "1 Без субтитров" + +# If user gets prompted to choose between several audio/subtitle tracks and +# language is unknown +msgctxt "#39707" +msgid "unknown" +msgstr "неизвестно" + +# If user gets prompted to choose between several subtitles and Plex adds the +# "default" flag +msgctxt "#39708" +msgid "Default" +msgstr "По-умолчанию" + +# If user gets prompted to choose between several subtitles and Plex adds the +# "forced" flag +msgctxt "#39709" +msgid "Forced" +msgstr "Forced" + +# If user gets prompted to choose between several subtitles the subtitle +# cannot be downloaded (has no 'key' attribute from the PMS), the subtitle +# needs to be burned in +msgctxt "#39710" +msgid "burn-in" +msgstr "встроить" + +# Dialog text if PKC detected a new Music library and Kodi needs to be +# restarted +msgctxt "#39711" +msgid "" +"New Plex music library detected. Sorry, but we need to restart Kodi now due " +"to the changes made." +msgstr "" +"Обнаружена новая библиотека музыки в Plex. К сожалению придётся " +"перезапустить Kodi" + +# Shown during sync process +msgctxt "#39712" +msgid "downloaded" +msgstr "скачано" + +# Shown during sync process +msgctxt "#39713" +msgid "processed" +msgstr "обработано" + +# Shown during sync process +msgctxt "#39714" +msgid "Sync" +msgstr "Синхронизация" + +# Shown during sync process +msgctxt "#39715" +msgid "items" +msgstr "элементов" + +# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is +# screwed up; formated the wrong way). Do NOT replace {0} and {1}! +msgctxt "#39716" +msgid "" +"Kodi cannot parse {0}. PKC will not function correctly. Please visit {1} and" +" correct your file!" +msgstr "" +"Kodi не может распознать {0}. PKC не будет работать нормально. Пожалуйста " +"исправьте свой файл {1}" + +# Shown once on first installation to comply with the terms of use of +# themoviedb.org +msgctxt "#39717" +msgid "PKC uses free additional artwork from www.themoviedb.org. Many thanks!" +msgstr "" +"PKC будет использовать дополнительные иллюстрации с www.themoviedb.org. " +"Большое спасибо!" + +# Shown during very first PKC setup only +msgctxt "#39718" +msgid "" +"Do you want to replace your custom user ratings with an indicator of how " +"many versions of a media item you posses?" +msgstr "" +"Вы хотите заменить свои пользовательские рейтинги индикатором количества " +"имеющихся версий элемента?" + +# In PKC Settings under Sync +msgctxt "#39719" +msgid "Replace user ratings with number of media versions" +msgstr "Заменить пользовательский рейтинг счетчиком версий элемента" From f1c784d458681be14f22565dc979f2824e3e6c44 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 5 Nov 2017 12:51:45 +0100 Subject: [PATCH 104/509] Support playback of .strm files --- resources/lib/playutils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 47320379..5cfbadd9 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### - import logging from downloadutils import DownloadUtils @@ -19,11 +18,9 @@ log = logging.getLogger("PLEX."+__name__) class PlayUtils(): def __init__(self, item): - self.item = item self.API = PlexAPI.API(item) self.doUtils = DownloadUtils().downloadUrl - self.machineIdentifier = window('plex_machineIdentifier') def getPlayUrl(self, partNumber=None): @@ -74,6 +71,14 @@ class PlayUtils(): if self.API.shouldStream() is True: log.info("Plex item optimized for direct streaming") return + # Check whether we have a strm file that we need to throw at Kodi 1:1 + path = self.API.getFilePath() + if path is not None and path.endswith('.strm'): + log.info('.strm file detected') + playurl = self.API.validatePlayurl(path, + self.API.getType(), + forceCheck=True) + return tryEncode(playurl) # set to either 'Direct Stream=1' or 'Transcode=2' # and NOT to 'Direct Play=0' if settings('playType') != "0": @@ -82,33 +87,28 @@ class PlayUtils(): return if self.mustTranscode(): return - return self.API.validatePlayurl(self.API.getFilePath(), + return self.API.validatePlayurl(path, self.API.getType(), forceCheck=True) def directPlay(self): - try: playurl = self.item['MediaSources'][0]['Path'] except (IndexError, KeyError): playurl = self.item['Path'] - if self.item.get('VideoType'): # Specific format modification if self.item['VideoType'] == "Dvd": playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl elif self.item['VideoType'] == "BluRay": playurl = "%s/BDMV/index.bdmv" % playurl - # Assign network protocol if playurl.startswith('\\\\'): playurl = playurl.replace("\\\\", "smb://") playurl = playurl.replace("\\", "/") - if "apple.com" in playurl: USER_AGENT = "QuickTime/7.7.4" playurl += "?|User-Agent=%s" % USER_AGENT - return playurl def mustTranscode(self): From f86582689b9f790dc45901418f6f7cfe5aca57a4 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 4 Dec 2017 19:41:59 +0100 Subject: [PATCH 105/509] Only transcode 10bit video for h265 - Fixes #367 --- resources/lib/playutils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 5cfbadd9..84eda6e0 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -130,15 +130,16 @@ class PlayUtils(): 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 codec is None: # e.g. trailers. Avoids TypeError with "'h265' in codec" log.info('No codec from PMS, not transcoding.') return False + if ((settings('transcodeHi10P') == 'true' and + videoCodec['bitDepth'] == '10') and + ('h265' in codec or 'hevc' in codec)): + log.info('Option to transcode 10bit h265 video content enabled.') + return True try: bitrate = int(videoCodec['bitrate']) except (TypeError, ValueError): From 9052b8401196c300ec36b4ab7aecdcc758d552a5 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 4 Dec 2017 19:50:12 +0100 Subject: [PATCH 106/509] Fix .strm playback failing for addon paths - Partially fixes #354 --- resources/lib/playutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 84eda6e0..3e268957 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -78,7 +78,10 @@ class PlayUtils(): playurl = self.API.validatePlayurl(path, self.API.getType(), forceCheck=True) - return tryEncode(playurl) + if playurl is None: + return + else: + return tryEncode(playurl) # set to either 'Direct Stream=1' or 'Transcode=2' # and NOT to 'Direct Play=0' if settings('playType') != "0": From 116a2956ac50c135b84039ad0d3051218530eabe Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 5 Dec 2017 11:14:41 +0100 Subject: [PATCH 107/509] Minor Plex Companion improvements --- resources/lib/plexbmchelper/subscribers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 9e936f05..82d4d833 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -69,13 +69,14 @@ class SubscriptionManager: if playerid is not None: info = self.getPlayerProperties(playerid) # save this info off so the server update can use it too - self.playerprops[playerid] = info; + self.playerprops[playerid] = info status = info['state'] time = info['time'] else: status = "stopped" time = 0 - ret = "\n"+' Date: Wed, 6 Dec 2017 11:40:27 +0100 Subject: [PATCH 108/509] Add docstrings --- resources/lib/playlist_func.py | 84 ++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 61ab9ac3..a0084855 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -21,21 +21,26 @@ REGEX = re_compile(r'''metadata%2F(\d+)''') 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] + """ + Base class + """ + playlistid = None + type = None + kodi_pl = None + items = [] + old_kodi_pl = [] + ID = None + version = None selectedItemID = None selectedItemOffset = None - shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? - repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? - # If Companion playback is initiated by another user + shuffled = 0 + repeat = 0 plex_transient_token = None def __repr__(self): + """ + Print the playlist, e.g. to log + """ answ = "<%s: " % (self.__class__.__name__) # For some reason, can't use dir directly answ += "ID: %s, " % self.ID @@ -68,25 +73,66 @@ class Playlist_Object_Baseclase(object): class Playlist_Object(Playlist_Object_Baseclase): + """ + To be done for synching Plex playlists to Kodi + """ kind = 'playList' class Playqueue_Object(Playlist_Object_Baseclase): + """ + PKC object to represent PMS playQueues and Kodi playlist for queueing + + playlistid = None [int] Kodi playlist ID (0, 1, 2) + type = None [str] Kodi type: 'audio', 'video', 'picture' + kodi_pl = None Kodi xbmc.PlayList object + items = [] [list] of Playlist_Items + old_kodi_pl = [] [list] store old Kodi JSON result with all pl items + ID = None [str] Plex playQueueID, unique Plex identifier + version = None [int] Plex version of the playQueue + selectedItemID = None + [str] Plex selectedItemID, playing element in queue + selectedItemOffset = None + [str] Offset of the playing element in queue + shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ??? + repeat = 0 [int] 0: not repeated, 1: ??? 2: ??? + + If Companion playback is initiated by another user: + plex_transient_token = None + """ kind = 'playQueue' class Playlist_Item(object): - ID = None # Plex playlist/playqueue id, e.g. playQueueItemID - plex_id = None # Plex unique item id, "ratingKey" - plex_type = None # Plex type, e.g. 'movie', 'clip' - 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. STRING!! - uri = None # Weird Plex uri path involving plex_UUID. STRING! - guid = None # Weird Plex guid + """ + Object to fill our playqueues and playlists with. + + ID = None Plex playlist/playqueue id, e.g. playQueueItemID + plex_id = None Plex unique item id, "ratingKey" + plex_type = None Plex type, e.g. 'movie', 'clip' + 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. STRING!! + uri = None Weird Plex uri path involving plex_UUID. STRING! + guid = None Weird Plex guid + xml = None etree XML from PMS, 1 lvl below + """ + ID = None + plex_id = None + plex_type = None + plex_UUID = None + kodi_id = None + kodi_type = None + file = None + uri = None + guid = None + xml = None def __repr__(self): + """ + Print the playlist item, e.g. to log + """ answ = "<%s: " % (self.__class__.__name__) for key in self.__dict__: if type(getattr(self, key)) in (str, unicode): From dc590d7ed145da35aea20cdff8e97b3c504ba68d Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 6 Dec 2017 18:05:01 +0100 Subject: [PATCH 109/509] Fx docstrings --- resources/lib/playlist_func.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a0084855..1b9a87d7 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -58,8 +58,7 @@ class Playlist_Object_Baseclase(object): """ Resets the playlist object to an empty playlist """ - # Clear Kodi playlist object - self.kodi_pl.clear() + self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] self.old_kodi_pl = [] self.ID = None @@ -107,16 +106,16 @@ class Playlist_Item(object): """ Object to fill our playqueues and playlists with. - ID = None Plex playlist/playqueue id, e.g. playQueueItemID - plex_id = None Plex unique item id, "ratingKey" - plex_type = None Plex type, e.g. 'movie', 'clip' - plex_UUID = None Plex librarySectionUUID + ID = None [str] Plex playlist/playqueue id, e.g. playQueueItemID + plex_id = None [str] Plex unique item id, "ratingKey" + plex_type = None [str] Plex type, e.g. 'movie', 'clip' + plex_UUID = None [str] 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. STRING!! - uri = None Weird Plex uri path involving plex_UUID. STRING! - guid = None Weird Plex guid - xml = None etree XML from PMS, 1 lvl below + kodi_type = None [str] Kodi type: 'movie' + file = None [str] Path to the item's file. STRING!! + uri = None [str] Weird Plex uri path involving plex_UUID. STRING! + guid = None [str] Weird Plex guid + xml = None [etree] XML from PMS, 1 lvl below """ ID = None plex_id = None From 208997b1674d2c64c3f8fea37d6b9cb2fbf6ad9f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 17:15:13 +0100 Subject: [PATCH 110/509] Remove obsolete method --- resources/lib/player.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 887a1d70..c50e0e6c 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -35,9 +35,6 @@ class Player(xbmc.Player): xbmc.Player.__init__(self) log.info("Started playback monitor.") - def GetPlayStats(self): - return self.playStats - def onPlayBackStarted(self): """ Will be called when xbmc starts playing a file. From 2a6d8757e6ccd96ba10625e1deaea322d8283aed Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 17:15:54 +0100 Subject: [PATCH 111/509] Class must only be initiated and used once Hence no borg necessary --- resources/lib/player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index c50e0e6c..34a6e805 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -22,15 +22,10 @@ log = logging.getLogger("PLEX."+__name__) class Player(xbmc.Player): - # Borg - multiple instances, shared state - _shared_state = {} - - played_info = {} playStats = {} currentFile = None def __init__(self): - self.__dict__ = self._shared_state self.doUtils = downloadutils.DownloadUtils().downloadUrl xbmc.Player.__init__(self) log.info("Started playback monitor.") From 65a48ebe7ba61e1e26e1206ed7d12cc3d944fd52 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 17:25:24 +0100 Subject: [PATCH 112/509] Attach PMS xml piece to playlist item --- resources/lib/playlist_func.py | 69 ++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 1b9a87d7..054f7bfd 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -128,6 +128,9 @@ class Playlist_Item(object): guid = None xml = None + # Yet to be implemented: handling of a movie with several parts + part = 0 + def __repr__(self): """ Print the playlist item, e.g. to log @@ -203,6 +206,8 @@ def playlist_item_from_plex(plex_id): def playlist_item_from_xml(playlist, xml_video_element): """ Returns a playlist element for the playqueue using the Plex xml + + xml_video_element: etree xml piece 1 level underneath """ item = Playlist_Item() api = API(xml_video_element) @@ -219,6 +224,7 @@ def playlist_item_from_xml(playlist, xml_video_element): item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4] except TypeError: pass + item.xml = xml_video_element log.debug('Created new playlist item from xml: %s' % item) return item @@ -285,8 +291,9 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ Initializes the Plex side without changing the Kodi playlists + WILL ALSO UPDATE OUR PLAYLISTS. - WILL ALSO UPDATE OUR PLAYLISTS + Returns True if successful, False otherwise """ log.debug('Initializing the playlist %s on the Plex side' % playlist) try: @@ -303,11 +310,14 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): action_type="POST", parameters=params) get_playlist_details_from_xml(playlist, xml) - except KeyError: - log.error('Could not init Plex playlist') - return + item.xml = xml[0] + except (KeyError, IndexError, TypeError): + log.error('Could not init Plex playlist with plex_id %s and ' + 'kodi_item %s', plex_id, kodi_item) + return False playlist.items.append(item) log.debug('Initialized the playlist on the Plex side: %s' % playlist) + return True def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, @@ -351,41 +361,56 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, 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) + success = 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) + success = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) + if success is False: + return False + # Now add the item to the Kodi playlist - WITHOUT adding it to our PKC pl + item = playlist.items[pos] + params = { + 'playlistid': playlist.playlistid, + 'position': pos + } + if item.kodi_id is not None: + params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)} + else: + params['item'] = {'file': item.file} + reply = JSONRPC('Playlist.Insert').execute(params) + if reply.get('error') is not None: + log.error('Could not add item to playlist. Kodi reply. %s', reply) + return False + return True 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 + + Returns True if successful, False otherwise """ if plex_id: try: item = playlist_item_from_plex(plex_id) except KeyError: log.error('Could not add new item to the PMS playlist') - return + return False else: item = playlist_item_from_kodi(kodi_item) url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri) # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: + item.xml = xml[-1] 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)) - return + return False # Get the guid for this item for plex_item in xml: if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: @@ -400,6 +425,7 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): len(playlist.items) - 1, pos) log.debug('Successfully added item on the Plex side: %s' % playlist) + return True def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, @@ -426,10 +452,16 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, if reply.get('error') is not None: log.error('Could not add item to playlist. Kodi reply. %s' % reply) return False - else: - playlist.items.insert(pos, playlist_item_from_kodi( - {'id': kodi_id, 'type': kodi_type, 'file': file})) - return True + item = playlist_item_from_kodi( + {'id': kodi_id, 'type': kodi_type, 'file': file}, playlist) + if item.plex_id is not None: + xml = GetPlexMetadata(item.plex_id) + try: + item.xml = xml[-1] + except (TypeError, IndexError): + log.error('Could not get metadata for playlist item %s', item) + playlist.items.insert(pos, item) + return True def move_playlist_item(playlist, before_pos, after_pos): @@ -563,8 +595,7 @@ def add_to_Kodi_playlist(playlist, xml_video_element): log.error('Could not add item %s to Kodi playlist. Error: %s' % (xml_video_element, reply)) return None - else: - return item + return item def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, From e6a5b1c157d95e7125597ba35b180134638dff02 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 17:25:48 +0100 Subject: [PATCH 113/509] Move Kodi playback info to state.py --- resources/lib/player.py | 3 ++- resources/lib/state.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 34a6e805..52e6e515 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -22,7 +22,8 @@ log = logging.getLogger("PLEX."+__name__) class Player(xbmc.Player): - playStats = {} + played_info = state.PLAYED_INFO + playStats = state.PLAYER_STATES currentFile = None def __init__(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index 97da71c9..26ff403c 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -74,3 +74,7 @@ PLEX_USER_ID = None # Token passed along, e.g. if playback initiated by Plex Companion. Might be # another user playing something! Token identifies user PLEX_TRANSIENT_TOKEN = None + +# Kodi player states +PLAYER_STATES = {} +PLAYED_INFO = {} From 18a5bcd7dbbe36d0a10bf6c1132519953951bfe5 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 18:19:54 +0100 Subject: [PATCH 114/509] Fix potentially telling wrong PMS to stop transcode --- resources/lib/player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 52e6e515..3d6944e9 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -27,7 +27,7 @@ class Player(xbmc.Player): currentFile = None def __init__(self): - self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.doUtils = downloadutils.DownloadUtils xbmc.Player.__init__(self) log.info("Started playback monitor.") @@ -375,7 +375,7 @@ class Player(xbmc.Player): # Stop transcoding if playMethod == "Transcode": log.info("Transcoding for %s terminating" % itemid) - self.doUtils( + self.doUtils().downloadUrl( "{server}/video/:/transcode/universal/stop", parameters={'session': window('plex_client_Id')}) From a09b6a4562cf6a12bcb064da080e1801d098693e Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 7 Dec 2017 18:22:52 +0100 Subject: [PATCH 115/509] Fix SSLError not being recognized as such ConnectionError is ancestor of SSLError --- resources/lib/downloadutils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 53eb8d84..7178a09d 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -196,6 +196,10 @@ class DownloadUtils(): r = self._doDownload(s, action_type, **kwargs) # THE EXCEPTIONS + except requests.exceptions.SSLError as e: + log.warn("Invalid SSL certificate for: %s" % url) + log.warn(e) + except requests.exceptions.ConnectionError as e: # Connection error log.warn("Server unreachable at: %s" % url) @@ -209,10 +213,6 @@ class DownloadUtils(): log.warn('HTTP Error at %s' % url) log.warn(e) - except requests.exceptions.SSLError as e: - log.warn("Invalid SSL certificate for: %s" % url) - log.warn(e) - except requests.exceptions.TooManyRedirects as e: log.warn("Too many redirects connecting to: %s" % url) log.warn(e) From f6b666e8927217ced8b6b3c0f483cb768908aa00 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Dec 2017 07:53:01 +0100 Subject: [PATCH 116/509] Move companion json rpc commands --- resources/lib/companion.py | 159 ++++++++----------------------- resources/lib/json_rpc.py | 185 +++++++++++++++++++++++++++++++++++++ resources/lib/utils.py | 17 ++++ 3 files changed, 241 insertions(+), 120 deletions(-) create mode 100644 resources/lib/json_rpc.py diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 7608c920..feda49a6 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -1,94 +1,50 @@ -# -*- coding: utf-8 -*- -import logging +""" +Processes Plex companion inputs from the plexbmchelper to Kodi commands +""" +from logging import getLogger from xbmc import Player -from utils import JSONRPC from variables import ALEXA_TO_COMPANION from playqueue import Playqueue from PlexFunctions import GetPlexKeyNumber +import json_rpc as js ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### -def getPlayers(): - info = JSONRPC("Player.GetActivePlayers").execute()['result'] or [] - ret = {} - for player in info: - player['playerid'] = int(player['playerid']) - ret[player['type']] = player - return ret - - -def getPlayerIds(): - ret = [] - for player in getPlayers().values(): - ret.append(player['playerid']) - return ret - - -def getPlaylistId(typus): +def skip_to(params): """ - typus: one of the Kodi types, e.g. audio or video + Skip to a specific playlist position. - Returns None if nothing was found + Does not seem to be implemented yet by Plex! """ - for playlist in getPlaylists(): - if playlist.get('type') == typus: - return playlist.get('playlistid') - - -def getPlaylists(): - """ - Returns a list, e.g. - [ - {u'playlistid': 0, u'type': u'audio'}, - {u'playlistid': 1, u'type': u'video'}, - {u'playlistid': 2, u'type': u'picture'} - ] - """ - return JSONRPC('Playlist.GetPlaylists').execute() - - -def millisToTime(t): - millis = int(t) - seconds = millis / 1000 - minutes = seconds / 60 - hours = minutes / 60 - seconds = seconds % 60 - minutes = minutes % 60 - millis = millis % 1000 - return {'hours': hours, - 'minutes': minutes, - 'seconds': seconds, - 'milliseconds': millis} - - -def skipTo(params): - # Does not seem to be implemented yet - playQueueItemID = params.get('playQueueItemID', 'not available') - library, plex_id = GetPlexKeyNumber(params.get('key')) - log.debug('Skipping to playQueueItemID %s, plex_id %s' - % (playQueueItemID, plex_id)) + playqueue_item_id = params.get('playQueueItemID', 'not available') + _, plex_id = GetPlexKeyNumber(params.get('key')) + LOG.debug('Skipping to playQueueItemID %s, plex_id %s', + playqueue_item_id, plex_id) found = True playqueues = Playqueue() - for (player, ID) in getPlayers().iteritems(): + for (player, _) in js.get_players().iteritems(): playqueue = playqueues.get_playqueue_from_type(player) for i, item in enumerate(playqueue.items): - if item.ID == playQueueItemID or item.plex_id == plex_id: + if item.ID == playqueue_item_id or item.plex_id == plex_id: break else: - log.debug('Item not found to skip to') + LOG.debug('Item not found to skip to') found = False if found: Player().play(playqueue.kodi_pl, None, False, i) def convert_alexa_to_companion(dictionary): + """ + The params passed by Alexa must first be converted to Companion talk + """ for key in dictionary: if key in ALEXA_TO_COMPANION: dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key] @@ -101,7 +57,7 @@ def process_command(request_path, params, queue=None): """ if params.get('deviceName') == 'Alexa': convert_alexa_to_companion(params) - log.debug('Received request_path: %s, params: %s' % (request_path, params)) + LOG.debug('Received request_path: %s, params: %s', request_path, params) if "/playMedia" in request_path: # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' @@ -109,84 +65,47 @@ def process_command(request_path, params, queue=None): 'action': action, 'data': params }) - elif request_path == 'player/playback/refreshPlayQueue': queue.put({ 'action': 'refreshPlayQueue', 'data': params }) - elif request_path == "player/playback/setParameters": if 'volume' in params: - volume = int(params['volume']) - log.debug("Adjusting the volume to %s" % volume) - JSONRPC('Application.SetVolume').execute({"volume": volume}) + js.set_volume(int(params['volume'])) else: - log.error('Unknown parameters: %s' % params) - + LOG.error('Unknown parameters: %s', params) elif request_path == "player/playback/play": - for playerid in getPlayerIds(): - JSONRPC("Player.PlayPause").execute({"playerid": playerid, - "play": True}) - + js.play() elif request_path == "player/playback/pause": - for playerid in getPlayerIds(): - JSONRPC("Player.PlayPause").execute({"playerid": playerid, - "play": False}) - + js.pause() elif request_path == "player/playback/stop": - for playerid in getPlayerIds(): - JSONRPC("Player.Stop").execute({"playerid": playerid}) - + js.stop() elif request_path == "player/playback/seekTo": - for playerid in getPlayerIds(): - JSONRPC("Player.Seek").execute( - {"playerid": playerid, - "value": millisToTime(params.get('offset', 0))}) - + js.seek_to(int(params.get('offset', 0))) elif request_path == "player/playback/stepForward": - for playerid in getPlayerIds(): - JSONRPC("Player.Seek").execute({"playerid": playerid, - "value": "smallforward"}) - + js.smallforward() elif request_path == "player/playback/stepBack": - for playerid in getPlayerIds(): - JSONRPC("Player.Seek").execute({"playerid": playerid, - "value": "smallbackward"}) - + js.smallbackward() elif request_path == "player/playback/skipNext": - for playerid in getPlayerIds(): - JSONRPC("Player.GoTo").execute({"playerid": playerid, - "to": "next"}) - + js.skipnext() elif request_path == "player/playback/skipPrevious": - for playerid in getPlayerIds(): - JSONRPC("Player.GoTo").execute({"playerid": playerid, - "to": "previous"}) - + js.skipprevious() elif request_path == "player/playback/skipTo": - skipTo(params) - + skip_to(params) elif request_path == "player/navigation/moveUp": - JSONRPC("Input.Up").execute() - + js.input_up() elif request_path == "player/navigation/moveDown": - JSONRPC("Input.Down").execute() - + js.input_down() elif request_path == "player/navigation/moveLeft": - JSONRPC("Input.Left").execute() - + js.input_left() elif request_path == "player/navigation/moveRight": - JSONRPC("Input.Right").execute() - + js.input_right() elif request_path == "player/navigation/select": - JSONRPC("Input.Select").execute() - + js.input_select() elif request_path == "player/navigation/home": - JSONRPC("Input.Home").execute() - + js.input_home() elif request_path == "player/navigation/back": - JSONRPC("Input.Back").execute() - + js.input_back() else: - log.error('Unknown request path: %s' % request_path) + LOG.error('Unknown request path: %s', request_path) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py new file mode 100644 index 00000000..971a5679 --- /dev/null +++ b/resources/lib/json_rpc.py @@ -0,0 +1,185 @@ +""" +Collection of functions using the Kodi JSON RPC interface. +See http://kodi.wiki/view/JSON-RPC_API +""" +from utils import JSONRPC, milliseconds_to_kodi_time + + +def get_players(): + """ + Returns all the active Kodi players (usually 3) in a dict: + { + 'video': {'playerid': int, 'type': 'video'} + 'audio': ... + 'picture': ... + } + """ + info = JSONRPC("Player.GetActivePlayers").execute()['result'] or [] + ret = {} + for player in info: + player['playerid'] = int(player['playerid']) + ret[player['type']] = player + return ret + + +def get_player_ids(): + """ + Returns a list of all the active Kodi player ids (usually 3) as int + """ + ret = [] + for player in get_players().values(): + ret.append(player['playerid']) + return ret + + +def get_playlist_id(typus): + """ + Returns the corresponding Kodi playlist id as an int + typus: Kodi playlist types: 'video', 'audio' or 'picture' + + Returns None if nothing was found + """ + for playlist in get_playlists(): + if playlist.get('type') == typus: + return playlist.get('playlistid') + + +def get_playlists(): + """ + Returns a list of all the Kodi playlists, e.g. + [ + {u'playlistid': 0, u'type': u'audio'}, + {u'playlistid': 1, u'type': u'video'}, + {u'playlistid': 2, u'type': u'picture'} + ] + """ + return JSONRPC('Playlist.GetPlaylists').execute() + + +def set_volume(volume): + """ + Set's the volume (for Kodi overall, not only a player). + Feed with an int + """ + return JSONRPC('Application.SetVolume').execute({"volume": volume}) + + +def play(): + """ + Toggles all Kodi players to play + """ + for playerid in get_player_ids(): + JSONRPC("Player.PlayPause").execute({"playerid": playerid, + "play": True}) + + +def pause(): + """ + Pauses playback for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.PlayPause").execute({"playerid": playerid, + "play": False}) + + +def stop(): + """ + Stops playback for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.Stop").execute({"playerid": playerid}) + + +def seek_to(offset): + """ + Seeks all Kodi players to offset [int] + """ + for playerid in get_player_ids(): + JSONRPC("Player.Seek").execute( + {"playerid": playerid, + "value": milliseconds_to_kodi_time(offset)}) + + +def smallforward(): + """ + Small step forward for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.Seek").execute({"playerid": playerid, + "value": "smallforward"}) + + +def smallbackward(): + """ + Small step backward for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.Seek").execute({"playerid": playerid, + "value": "smallbackward"}) + + +def skipnext(): + """ + Skips to the next item to play for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.GoTo").execute({"playerid": playerid, + "to": "next"}) + + +def skipprevious(): + """ + Skips to the previous item to play for all Kodi players + """ + for playerid in get_player_ids(): + JSONRPC("Player.GoTo").execute({"playerid": playerid, + "to": "previous"}) + + +def input_up(): + """ + Tells Kodi the users pushed up + """ + JSONRPC("Input.Up").execute() + + +def input_down(): + """ + Tells Kodi the users pushed down + """ + JSONRPC("Input.Down").execute() + + +def input_left(): + """ + Tells Kodi the users pushed left + """ + JSONRPC("Input.Left").execute() + + +def input_right(): + """ + Tells Kodi the users pushed left + """ + JSONRPC("Input.Right").execute() + + +def input_select(): + """ + Tells Kodi the users pushed select + """ + JSONRPC("Input.Select").execute() + + +def input_home(): + """ + Tells Kodi the users pushed home + """ + JSONRPC("Input.Home").execute() + + +def input_back(): + """ + Tells Kodi the users pushed back + """ + JSONRPC("Input.Back").execute() diff --git a/resources/lib/utils.py b/resources/lib/utils.py index eddc8e7f..3b5f9f8f 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -180,6 +180,23 @@ def dialog(typus, *args, **kwargs): return types[typus](*args, **kwargs) +def milliseconds_to_kodi_time(milliseconds): + """ + Converts time in milliseconds to the time dict used by the Kodi JSON RPC + Pass in the time in milliseconds as an int + """ + seconds = milliseconds / 1000 + minutes = seconds / 60 + hours = minutes / 60 + seconds = seconds % 60 + minutes = minutes % 60 + milliseconds = milliseconds % 1000 + return {'hours': hours, + 'minutes': minutes, + 'seconds': seconds, + 'milliseconds': milliseconds} + + def tryEncode(uniString, encoding='utf-8'): """ Will try to encode uniString (in unicode) to encoding. This possibly From f2bc95813a324d0a4ea62dc1cf071f6a7b1892b5 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 8 Dec 2017 19:43:06 +0100 Subject: [PATCH 117/509] Centralize Kodi json rpc --- resources/lib/artwork.py | 121 +++------------ resources/lib/entrypoint.py | 248 +++++++++++------------------- resources/lib/json_rpc.py | 221 +++++++++++++++++++++++--- resources/lib/playback_starter.py | 6 +- resources/lib/playbackutils.py | 4 +- resources/lib/player.py | 101 ++++-------- resources/lib/playlist_func.py | 205 ++++++++++-------------- resources/lib/playqueue.py | 60 ++++---- resources/lib/utils.py | 6 +- 9 files changed, 472 insertions(+), 500 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index ce2edc34..b6306c70 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging -from json import dumps, loads +from logging import getLogger import requests from shutil import rmtree from urllib import quote_plus, unquote from threading import Thread from Queue import Queue, Empty +import json_rpc as js -from xbmc import executeJSONRPC, sleep, translatePath +from xbmc import sleep, translatePath from xbmcvfs import exists from utils import window, settings, language as lang, kodiSQL, tryEncode, \ @@ -20,7 +20,7 @@ import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -34,87 +34,16 @@ def setKodiWebServerDetails(): xbmc_port = None xbmc_username = None xbmc_password = None - web_query = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - "setting": "services.webserver" - } - } - result = executeJSONRPC(dumps(web_query)) - result = loads(result) - try: - xbmc_webserver_enabled = result['result']['value'] - except (KeyError, TypeError): - xbmc_webserver_enabled = False - if not xbmc_webserver_enabled: + if js.get_setting('services.webserver') in (None, False): # Enable the webserver, it is disabled - web_port = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - "setting": "services.webserverport", - "value": 8080 - } - } - result = executeJSONRPC(dumps(web_port)) xbmc_port = 8080 - web_user = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - "setting": "services.webserver", - "value": True - } - } - result = executeJSONRPC(dumps(web_user)) xbmc_username = "kodi" + js.set_setting('services.webserverport', xbmc_port) + js.set_setting('services.webserver', True) # Webserver already enabled - web_port = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - "setting": "services.webserverport" - } - } - result = executeJSONRPC(dumps(web_port)) - result = loads(result) - try: - xbmc_port = result['result']['value'] - except (TypeError, KeyError): - pass - web_user = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - "setting": "services.webserverusername" - } - } - result = executeJSONRPC(dumps(web_user)) - result = loads(result) - try: - xbmc_username = result['result']['value'] - except (TypeError, KeyError): - pass - web_pass = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - "setting": "services.webserverpassword" - } - } - result = executeJSONRPC(dumps(web_pass)) - result = loads(result) - try: - xbmc_password = result['result']['value'] - except TypeError: - pass + xbmc_port = js.get_setting('services.webserverport') + xbmc_username = js.get_setting('services.webserverusername') + xbmc_password = js.get_setting('services.webserverpassword') return (xbmc_port, xbmc_username, xbmc_password) @@ -152,7 +81,7 @@ class Image_Cache_Thread(Thread): # Set in service.py if thread_stopped(): # Abort was requested while waiting. We should exit - log.info("---===### Stopped Image_Cache_Thread ###===---") + LOG.info("---===### Stopped Image_Cache_Thread ###===---") return sleep(1000) try: @@ -179,10 +108,10 @@ class Image_Cache_Thread(Thread): # Server thinks its a DOS attack, ('error 10053') # Wait before trying again if sleeptime > 5: - log.error('Repeatedly got ConnectionError for url %s' + LOG.error('Repeatedly got ConnectionError for url %s' % double_urldecode(url)) break - log.debug('Were trying too hard to download art, server ' + LOG.debug('Were trying too hard to download art, server ' 'over-loaded. Sleep %s seconds before trying ' 'again to download %s' % (2**sleeptime, double_urldecode(url))) @@ -190,18 +119,18 @@ class Image_Cache_Thread(Thread): sleeptime += 1 continue except Exception as e: - log.error('Unknown exception for url %s: %s' + LOG.error('Unknown exception for url %s: %s' % (double_urldecode(url), e)) import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + LOG.error("Traceback:\n%s" % traceback.format_exc()) break # We did not even get a timeout break queue.task_done() - log.debug('Cached art: %s' % double_urldecode(url)) + LOG.debug('Cached art: %s' % double_urldecode(url)) # Sleep for a bit to reduce CPU strain sleep(sleep_between) - log.info("---===### Stopped Image_Cache_Thread ###===---") + LOG.info("---===### Stopped Image_Cache_Thread ###===---") class Artwork(): @@ -217,11 +146,11 @@ class Artwork(): if not dialog('yesno', "Image Texture Cache", lang(39250)): return - log.info("Doing Image Cache Sync") + LOG.info("Doing Image Cache Sync") # ask to rest all existing or not if dialog('yesno', "Image Texture Cache", lang(39251)): - log.info("Resetting all cache data first") + LOG.info("Resetting all cache data first") # Remove all existing textures first path = tryDecode(translatePath("special://thumbnails/")) if exists_dir(path): @@ -248,7 +177,7 @@ class Artwork(): cursor.execute(query, ('actor', )) result = cursor.fetchall() total = len(result) - log.info("Image cache sync about to process %s video images" % total) + LOG.info("Image cache sync about to process %s video images" % total) connection.close() for url in result: @@ -259,7 +188,7 @@ class Artwork(): cursor.execute("SELECT url FROM art") result = cursor.fetchall() total = len(result) - log.info("Image cache sync about to process %s music images" % total) + LOG.info("Image cache sync about to process %s music images" % total) connection.close() for url in result: self.cacheTexture(url[0]) @@ -364,7 +293,7 @@ class Artwork(): url = cursor.fetchone()[0] except TypeError: # Add the artwork - log.debug("Adding Art Link for kodiId: %s (%s)" + LOG.debug("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl)) query = ( ''' @@ -382,7 +311,7 @@ class Artwork(): imageType in ("fanart", "poster")): # Delete current entry before updating with the new one self.deleteCachedArtwork(url) - log.debug("Updating Art url for %s kodiId %s %s -> (%s)" + LOG.debug("Updating Art url for %s kodiId %s %s -> (%s)" % (imageType, kodiId, url, imageUrl)) query = ' '.join(( "UPDATE art", @@ -418,11 +347,11 @@ class Artwork(): (url,)) cachedurl = cursor.fetchone()[0] except TypeError: - log.info("Could not find cached url.") + LOG.info("Could not find cached url.") else: # Delete thumbnail as well as the entry path = translatePath("special://thumbnails/%s" % cachedurl) - log.debug("Deleting cached thumbnail: %s" % path) + LOG.debug("Deleting cached thumbnail: %s" % path) if exists(path): rmtree(tryDecode(path), ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 6ac3366d..c96b9935 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -12,12 +12,13 @@ from xbmc import sleep, executebuiltin, translatePath from xbmcgui import ListItem from utils import window, settings, language as lang, dialog, tryEncode, \ - CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode + CatchExceptions, exists_dir, plex_command, tryDecode import downloadutils from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ GetMachineIdentifier from PlexAPI import API +import json_rpc as js import variables as v ############################################################################### @@ -268,7 +269,6 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False): ##### GET NEXTUP EPISODES FOR TAGNAME ##### def getNextUpEpisodes(tagname, limit): - count = 0 # if the addon is called with nextup parameter, # we return the nextepisodes list of the given tagname @@ -283,68 +283,50 @@ def getNextUpEpisodes(tagname, limit): ]}, 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] } - result = JSONRPC('VideoLibrary.GetTVShows').execute(params) - - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - if settings('ignoreSpecialsNextEpisodes') == "true": - params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", - 'field': "playcount", - 'value': "1"}, - {'operator': "greaterthan", - 'field': "season", - 'value': "0"}]}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - else: - params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'operator': "lessthan", - 'field': "playcount", - 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - - result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem(handle=HANDLE, - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - + for item in js.get_tv_shows(params): + if settings('ignoreSpecialsNextEpisodes') == "true": + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'and': [ + {'operator': "lessthan", + 'field': "playcount", + 'value': "1"}, + {'operator': "greaterthan", + 'field': "season", + 'value': "0"}]}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + else: + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'operator': "lessthan", + 'field': "playcount", + 'value': "1"}, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", + "plot", "file", "rating", "resume", "tvshowid", "art", + "streamdetails", "firstaired", "runtime", "writer", + "dateadded", "lastplayed" + ], + 'limits': {"end": 1} + } + for episode in js.get_episodes(params): + xbmcplugin.addDirectoryItem(handle=HANDLE, + url=episode['file'], + listitem=createListItem(episode)) + count += 1 + if count == limit: + break xbmcplugin.endOfDirectory(handle=HANDLE) @@ -364,42 +346,26 @@ def getInProgressEpisodes(tagname, limit): ]}, 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] } - result = JSONRPC('VideoLibrary.GetTVShows').execute(params) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - pass - else: - for item in items: - params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'operator': "true", - 'field': "inprogress", - 'value': ""}, - 'properties': ["title", "playcount", "season", "episode", - "showtitle", "plot", "file", "rating", "resume", - "tvshowid", "art", "cast", "streamdetails", "firstaired", - "runtime", "writer", "dateadded", "lastplayed"] - } - result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - li = createListItem(episode) - xbmcplugin.addDirectoryItem(handle=HANDLE, - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - + for item in js.get_tv_shows(params): + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'operator': "true", + 'field': "inprogress", + 'value': ""}, + 'properties': ["title", "playcount", "season", "episode", + "showtitle", "plot", "file", "rating", "resume", + "tvshowid", "art", "cast", "streamdetails", "firstaired", + "runtime", "writer", "dateadded", "lastplayed"] + } + for episode in js.get_episodes(params): + xbmcplugin.addDirectoryItem(handle=HANDLE, + url=episode['file'], + listitem=createListItem(episode)) + count += 1 + if count == limit: + break xbmcplugin.endOfDirectory(handle=HANDLE) ##### GET RECENT EPISODES FOR TAGNAME ##### @@ -412,22 +378,13 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit): appendShowTitle = settings('RecentTvAppendShow') == 'true' appendSxxExx = settings('RecentTvAppendSeason') == 'true' # First we get a list of all the TV shows - filtered by tag + allshowsIds = set() params = { 'sort': {'order': "descending", 'method': "dateadded"}, 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname}, } - result = JSONRPC('VideoLibrary.GetTVShows').execute(params) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result'][mediatype] - except (KeyError, TypeError): - # No items, empty folder - xbmcplugin.endOfDirectory(handle=HANDLE) - return - - allshowsIds = set() - for item in items: - allshowsIds.add(item['tvshowid']) + for tv_show in js.get_tv_shows(params): + allshowsIds.add(tv_show['tvshowid']) params = { 'sort': {'order': "descending", 'method': "dateadded"}, 'properties': ["title", "playcount", "season", "episode", "showtitle", @@ -442,26 +399,18 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit): 'field': "playcount", 'value': "1" } - result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass - else: - for episode in episodes: - if episode['tvshowid'] in allshowsIds: - li = createListItem(episode, - appendShowTitle=appendShowTitle, - appendSxxExx=appendSxxExx) - xbmcplugin.addDirectoryItem( - handle=HANDLE, - url=episode['file'], - listitem=li) - count += 1 - - if count == limit: - break - + for episode in js.get_episodes(params): + if episode['tvshowid'] in allshowsIds: + listitem = createListItem(episode, + appendShowTitle=appendShowTitle, + appendSxxExx=appendSxxExx) + xbmcplugin.addDirectoryItem( + handle=HANDLE, + url=episode['file'], + listitem=listitem) + count += 1 + if count == limit: + break xbmcplugin.endOfDirectory(handle=HANDLE) @@ -644,15 +593,6 @@ def getOnDeck(viewid, mediatype, tagname, limit): {'operator': "is", 'field': "tag", 'value': "%s" % tagname} ]} } - result = JSONRPC('VideoLibrary.GetTVShows').execute(params) - # If we found any, find the oldest unwatched show for each one. - try: - items = result['result'][mediatype] - except (KeyError, TypeError): - # Now items retrieved - empty directory - xbmcplugin.endOfDirectory(handle=HANDLE) - return - params = { 'sort': {'method': "episode"}, 'limits': {"end": 1}, @@ -677,7 +617,6 @@ def getOnDeck(viewid, mediatype, tagname, limit): {'operator': "true", 'field': "inprogress", 'value': ""} ] } - # Are there any episodes still in progress/not yet finished watching?!? # Then we should show this episode, NOT the "next up" inprog_params = { @@ -687,35 +626,26 @@ def getOnDeck(viewid, mediatype, tagname, limit): } count = 0 - for item in items: + for item in js.get_tv_shows(params): inprog_params['tvshowid'] = item['tvshowid'] - result = JSONRPC('VideoLibrary.GetEpisodes').execute(inprog_params) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): + episodes = js.get_episodes(inprog_params) + if not episodes: # No, there are no episodes not yet finished. Get "next up" params['tvshowid'] = item['tvshowid'] - result = JSONRPC('VideoLibrary.GetEpisodes').execute(params) - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - # Also no episodes currently coming up - continue + episodes = js.get_episodes(params) for episode in episodes: # There will always be only 1 episode ('limit=1') - li = createListItem(episode, - appendShowTitle=appendShowTitle, - appendSxxExx=appendSxxExx) + listitem = createListItem(episode, + appendShowTitle=appendShowTitle, + appendSxxExx=appendSxxExx) xbmcplugin.addDirectoryItem( handle=HANDLE, url=episode['file'], - listitem=li, + listitem=listitem, isFolder=False) - count += 1 if count >= limit: break - xbmcplugin.endOfDirectory(handle=HANDLE) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 971a5679..f42499f1 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -2,7 +2,7 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ -from utils import JSONRPC, milliseconds_to_kodi_time +from utils import jsonrpc, milliseconds_to_kodi_time def get_players(): @@ -14,7 +14,7 @@ def get_players(): 'picture': ... } """ - info = JSONRPC("Player.GetActivePlayers").execute()['result'] or [] + info = jsonrpc("Player.GetActivePlayers").execute()['result'] or [] ret = {} for player in info: player['playerid'] = int(player['playerid']) @@ -53,7 +53,19 @@ def get_playlists(): {u'playlistid': 2, u'type': u'picture'} ] """ - return JSONRPC('Playlist.GetPlaylists').execute() + try: + ret = jsonrpc('Playlist.GetPlaylists').execute()['result'] + except KeyError: + ret = [] + return ret + + +def get_volume(): + """ + Returns the Kodi volume as an int between 0 (min) and 100 (max) + """ + return jsonrpc('Application.GetProperties').execute( + {"properties": ['volume']})['result']['volume'] def set_volume(volume): @@ -61,7 +73,15 @@ def set_volume(volume): Set's the volume (for Kodi overall, not only a player). Feed with an int """ - return JSONRPC('Application.SetVolume').execute({"volume": volume}) + return jsonrpc('Application.SetVolume').execute({"volume": volume}) + + +def get_muted(): + """ + Returns True if Kodi is muted, False otherwise + """ + return jsonrpc('Application.GetProperties').execute( + {"properties": ['muted']})['result']['muted'] def play(): @@ -69,7 +89,7 @@ def play(): Toggles all Kodi players to play """ for playerid in get_player_ids(): - JSONRPC("Player.PlayPause").execute({"playerid": playerid, + jsonrpc("Player.PlayPause").execute({"playerid": playerid, "play": True}) @@ -78,7 +98,7 @@ def pause(): Pauses playback for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.PlayPause").execute({"playerid": playerid, + jsonrpc("Player.PlayPause").execute({"playerid": playerid, "play": False}) @@ -87,7 +107,7 @@ def stop(): Stops playback for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.Stop").execute({"playerid": playerid}) + jsonrpc("Player.Stop").execute({"playerid": playerid}) def seek_to(offset): @@ -95,7 +115,7 @@ def seek_to(offset): Seeks all Kodi players to offset [int] """ for playerid in get_player_ids(): - JSONRPC("Player.Seek").execute( + jsonrpc("Player.Seek").execute( {"playerid": playerid, "value": milliseconds_to_kodi_time(offset)}) @@ -105,7 +125,7 @@ def smallforward(): Small step forward for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.Seek").execute({"playerid": playerid, + jsonrpc("Player.Seek").execute({"playerid": playerid, "value": "smallforward"}) @@ -114,7 +134,7 @@ def smallbackward(): Small step backward for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.Seek").execute({"playerid": playerid, + jsonrpc("Player.Seek").execute({"playerid": playerid, "value": "smallbackward"}) @@ -123,7 +143,7 @@ def skipnext(): Skips to the next item to play for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.GoTo").execute({"playerid": playerid, + jsonrpc("Player.GoTo").execute({"playerid": playerid, "to": "next"}) @@ -132,7 +152,7 @@ def skipprevious(): Skips to the previous item to play for all Kodi players """ for playerid in get_player_ids(): - JSONRPC("Player.GoTo").execute({"playerid": playerid, + jsonrpc("Player.GoTo").execute({"playerid": playerid, "to": "previous"}) @@ -140,46 +160,209 @@ def input_up(): """ Tells Kodi the users pushed up """ - JSONRPC("Input.Up").execute() + jsonrpc("Input.Up").execute() def input_down(): """ Tells Kodi the users pushed down """ - JSONRPC("Input.Down").execute() + jsonrpc("Input.Down").execute() def input_left(): """ Tells Kodi the users pushed left """ - JSONRPC("Input.Left").execute() + jsonrpc("Input.Left").execute() def input_right(): """ Tells Kodi the users pushed left """ - JSONRPC("Input.Right").execute() + jsonrpc("Input.Right").execute() def input_select(): """ Tells Kodi the users pushed select """ - JSONRPC("Input.Select").execute() + jsonrpc("Input.Select").execute() def input_home(): """ Tells Kodi the users pushed home """ - JSONRPC("Input.Home").execute() + jsonrpc("Input.Home").execute() def input_back(): """ Tells Kodi the users pushed back """ - JSONRPC("Input.Back").execute() + jsonrpc("Input.Back").execute() + + +def playlist_get_items(playlistid, properties): + """ + playlistid: [int] id of the Kodi playlist + properties: [list] of strings for the properties to return + e.g. 'title', 'file' + + Returns a list of Kodi playlist items as dicts with the keys specified in + properties. Or an empty list if unsuccessful. Example: + [{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'}] + """ + reply = jsonrpc('Playlist.GetItems').execute({ + 'playlistid': playlistid, + 'properties': properties + }) + try: + reply = reply['result']['items'] + except KeyError: + reply = [] + return reply + + +def playlist_add(playlistid, item): + """ + Adds an item to the Kodi playlist with id playlistid. item is either the + dict + {'file': filepath as string} + or + {kodi_type: kodi_id} + + Returns a dict with the key 'error' if unsuccessful. + """ + return jsonrpc('Playlist.Add').execute({'playlistid': playlistid, + 'item': item}) + + +def playlist_insert(params): + """ + Insert item(s) into playlist. Does not work for picture playlists (aka + slideshows). params is the dict + { + 'playlistid': [int] + 'position': [int] + 'item': + } + item is either the dict + {'file': filepath as string} + or + {kodi_type: kodi_id} + Returns a dict with the key 'error' if something went wrong. + """ + return jsonrpc('Playlist.Insert').execute(params) + + +def playlist_remove(playlistid, position): + """ + Removes the playlist item at position from the playlist + position: [int] + + Returns a dict with the key 'error' if something went wrong. + """ + return jsonrpc('Playlist.Remove').execute({'playlistid': playlistid, + 'position': position}) + + +def get_setting(setting): + """ + Returns the Kodi setting, a [str], or None if not possible + """ + try: + ret = jsonrpc('Settings.GetSettingValue').execute( + {'setting': setting})['result']['value'] + except (KeyError, TypeError): + ret = None + return ret + + +def set_setting(setting, value): + """ + Sets the Kodi setting, a [str], to value + """ + return jsonrpc('Settings.SetSettingValue').execute( + {'setting': setting, 'value': value}) + + +def get_tv_shows(params): + """ + Returns a list of tv shows for params (check the Kodi wiki) + """ + ret = jsonrpc('VideoLibrary.GetTVShows').execute(params) + try: + ret['result']['tvshows'] + except (KeyError, TypeError): + ret = [] + return ret + + +def get_episodes(params): + """ + Returns a list of tv show episodes for params (check the Kodi wiki) + """ + ret = jsonrpc('VideoLibrary.GetEpisodes').execute(params) + try: + ret['result']['episodes'] + except (KeyError, TypeError): + ret = [] + return ret + + +def current_audiostream(playerid): + """ + Returns a dict of the active audiostream for playerid [int]: + { + 'index': [int], audiostream index + 'language': [str] + 'name': [str] + 'codec': [str] + 'bitrate': [int] + 'channels': [int] + } + or an empty dict if unsuccessful + """ + ret = jsonrpc('Player.GetProperties').execute( + {'properties': ['currentaudiostream'], 'playerid': playerid}) + try: + ret = ret['result']['currentaudiostream'] + except (KeyError, TypeError): + ret = {} + return ret + + +def current_subtitle(playerid): + """ + Returns a dict of the active subtitle for playerid [int]: + { + 'index': [int], subtitle index + 'language': [str] + 'name': [str] + } + or an empty dict if unsuccessful + """ + ret = jsonrpc('Player.GetProperties').execute( + {'properties': ['currentsubtitle'], 'playerid': playerid}) + try: + ret = ret['result']['currentsubtitle'] + except (KeyError, TypeError): + ret = {} + return ret + + +def subtitle_enabled(playerid): + """ + Returns True if a subtitle is enabled, False otherwise + """ + ret = jsonrpc('Player.GetProperties').execute( + {'properties': ['subtitleenabled'], 'playerid': playerid}) + try: + ret = ret['result']['subtitleenabled'] + except (KeyError, TypeError): + ret = False + return ret diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index aabfc3ac..5abba986 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -12,7 +12,7 @@ from playbackutils import PlaybackUtils from utils import window from PlexFunctions import GetPlexMetadata from PlexAPI import API -from playqueue import lock +from playqueue import LOCK import variables as v from downloadutils import DownloadUtils from PKC_listitem import convert_PKC_to_listitem @@ -62,7 +62,7 @@ class Playback_Starter(Thread): # Video and Music playqueue = self.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - with lock: + with LOCK: result = PlaybackUtils(xml, playqueue).play( plex_id, kodi_id, @@ -113,7 +113,7 @@ class Playback_Starter(Thread): log.info('Couldnt find item %s in Kodi db' % api.getRatingKey()) playqueue = self.playqueue.get_playqueue_from_type(typus) - with lock: + with LOCK: result = PlaybackUtils(xml, playqueue).play( plex_id, kodi_id=kodi_id, diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 388b0dc5..b302a4a2 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -18,7 +18,7 @@ from PlexFunctions import init_plex_playqueue from PKC_listitem import PKC_ListItem as ListItem, convert_PKC_to_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 + add_listitem_to_playlist, remove_from_kodi_playlist from pickler import Playback_Successful from plexdb_functions import Get_Plex_DB import variables as v @@ -155,7 +155,7 @@ class PlaybackUtils(): playurl, xml[0]) # Remove the original item from playlist - remove_from_Kodi_playlist( + remove_from_kodi_playlist( playqueue, startPos+1) # Readd the original item to playlist - via jsonrpc so we have diff --git a/resources/lib/player.py b/resources/lib/player.py index 3d6944e9..4bb51a83 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -10,12 +10,13 @@ from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode import downloadutils import plexdb_functions as plexdb import kodidb_functions as kodidb +import json_rpc as js import variables as v import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = logging.getLogger("PLEX." + __name__) ############################################################################### @@ -29,7 +30,7 @@ class Player(xbmc.Player): def __init__(self): self.doUtils = downloadutils.DownloadUtils xbmc.Player.__init__(self) - log.info("Started playback monitor.") + LOG.info("Started playback monitor.") def onPlayBackStarted(self): """ @@ -56,7 +57,7 @@ class Player(xbmc.Player): else: count += 1 if not currentFile: - log.warn('Error getting currently playing file; abort reporting') + LOG.warn('Error getting currently playing file; abort reporting') return # Save currentFile for cleanup later and for references @@ -69,11 +70,11 @@ class Player(xbmc.Player): xbmc.sleep(200) itemId = window("plex_%s.itemid" % tryEncode(currentFile)) if count == 5: - log.warn("Could not find itemId, cancelling playback report!") + LOG.warn("Could not find itemId, cancelling playback report!") return count += 1 - log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) + LOG.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) plexitem = "plex_%s" % tryEncode(currentFile) runtime = window("%s.runtime" % plexitem) @@ -86,40 +87,26 @@ class Player(xbmc.Player): playcount = 0 window('plex_skipWatched%s' % itemId, value="true") - log.debug("Playing itemtype is: %s" % itemType) + LOG.debug("Playing itemtype is: %s" % itemType) customseek = window('plex_customplaylist.seektime') if customseek: # Start at, when using custom playlist (play to Kodi from # webclient) - log.info("Seeking to: %s" % customseek) + LOG.info("Seeking to: %s" % customseek) try: self.seekTime(int(customseek)) except: - log.error('Could not seek!') + LOG.error('Could not seek!') window('plex_customplaylist.seektime', clear=True) try: seekTime = self.getTime() except RuntimeError: - log.error('Could not get current seektime from xbmc player') + LOG.error('Could not get current seektime from xbmc player') seekTime = 0 - - # Get playback volume - volume_query = { - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) - result = result.get('result') - - volume = result.get('volume') - muted = result.get('muted') + volume = js.get_volume() + muted = js.get_muted() # Postdata structure to send to plex server url = "{server}/:/timeline?" @@ -144,35 +131,13 @@ class Player(xbmc.Player): % tryEncode(currentFile)) else: # Get the current kodi audio and subtitles and convert to plex equivalent - tracks_query = { - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) - result = result.get('result') - - try: # Audio tracks - indexAudio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - indexAudio = 0 - - try: # Subtitles tracks - indexSubs = result['currentsubtitle']['index'] - except (KeyError, TypeError): + indexAudio = js.current_audiostream(1).get('index', 0) + subsEnabled = js.subtitle_enabled(1) + if subsEnabled: + indexSubs = js.current_subtitle(1).get('index', 0) + else: indexSubs = 0 - try: # If subtitles are enabled - subsEnabled = result['subtitleenabled'] - except (KeyError, TypeError): - subsEnabled = "" - # Postdata for the audio postdata['AudioStreamIndex'] = indexAudio + 1 @@ -185,7 +150,7 @@ class Player(xbmc.Player): if mapping: # Set in playbackutils.py - log.debug("Mapping for external subtitles index: %s" + LOG.debug("Mapping for external subtitles index: %s" % mapping) externalIndex = json.loads(mapping) @@ -213,9 +178,9 @@ class Player(xbmc.Player): except ValueError: try: runtime = self.getTotalTime() - log.error("Runtime is missing, Kodi runtime: %s" % runtime) + LOG.error("Runtime is missing, Kodi runtime: %s" % runtime) except: - log.error('Could not get kodi runtime, setting to zero') + LOG.error('Could not get kodi runtime, setting to zero') runtime = 0 with plexdb.Get_Plex_DB() as plex_db: @@ -223,7 +188,7 @@ class Player(xbmc.Player): try: fileid = plex_dbitem[1] except TypeError: - log.info("Could not find fileid in plex db.") + LOG.info("Could not find fileid in plex db.") fileid = None # Save data map for updates and position calls data = { @@ -242,7 +207,7 @@ class Player(xbmc.Player): } self.played_info[currentFile] = data - log.info("ADDING_FILE: %s" % data) + LOG.info("ADDING_FILE: %s" % data) # log some playback stats '''if(itemType != None): @@ -262,7 +227,7 @@ class Player(xbmc.Player): def onPlayBackPaused(self): currentFile = self.currentFile - log.info("PLAYBACK_PAUSED: %s" % currentFile) + LOG.info("PLAYBACK_PAUSED: %s" % currentFile) if self.played_info.get(currentFile): self.played_info[currentFile]['paused'] = True @@ -270,7 +235,7 @@ class Player(xbmc.Player): def onPlayBackResumed(self): currentFile = self.currentFile - log.info("PLAYBACK_RESUMED: %s" % currentFile) + LOG.info("PLAYBACK_RESUMED: %s" % currentFile) if self.played_info.get(currentFile): self.played_info[currentFile]['paused'] = False @@ -278,7 +243,7 @@ class Player(xbmc.Player): def onPlayBackSeek(self, time, seekOffset): # Make position when seeking a bit more accurate currentFile = self.currentFile - log.info("PLAYBACK_SEEK: %s" % currentFile) + LOG.info("PLAYBACK_SEEK: %s" % currentFile) if self.played_info.get(currentFile): try: @@ -290,7 +255,7 @@ class Player(xbmc.Player): def onPlayBackStopped(self): # Will be called when user stops xbmc playing a file - log.info("ONPLAYBACK_STOPPED") + LOG.info("ONPLAYBACK_STOPPED") self.stopAll() @@ -303,24 +268,24 @@ class Player(xbmc.Player): # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) state.PLEX_TRANSIENT_TOKEN = None - log.debug("Cleared playlist properties.") + LOG.debug("Cleared playlist properties.") def onPlayBackEnded(self): # Will be called when xbmc stops playing a file, because the file ended - log.info("ONPLAYBACK_ENDED") + LOG.info("ONPLAYBACK_ENDED") self.onPlayBackStopped() def stopAll(self): if not self.played_info: return - log.info("Played_information: %s" % self.played_info) + LOG.info("Played_information: %s" % self.played_info) # Process each items for item in self.played_info: data = self.played_info.get(item) if not data: continue - log.debug("Item path: %s" % item) - log.debug("Item data: %s" % data) + LOG.debug("Item path: %s" % item) + LOG.debug("Item data: %s" % data) runtime = data['runtime'] currentPosition = data['currentPosition'] @@ -340,7 +305,7 @@ class Player(xbmc.Player): except ZeroDivisionError: # Runtime is 0. percentComplete = 0 - log.info("Percent complete: %s Mark played at: %s" + LOG.info("Percent complete: %s Mark played at: %s" % (percentComplete, v.MARK_PLAYED_AT)) if percentComplete >= v.MARK_PLAYED_AT: # Tell Kodi that we've finished watching (Plex knows) @@ -374,7 +339,7 @@ class Player(xbmc.Player): # Stop transcoding if playMethod == "Transcode": - log.info("Transcoding for %s terminating" % itemid) + LOG.info("Transcoding for %s terminating" % itemid) self.doUtils().downloadUrl( "{server}/video/:/transcode/universal/stop", parameters={'session': window('plex_client_Id')}) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 054f7bfd..cb4a1883 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -1,3 +1,6 @@ +""" +Collection of functions associated with Kodi and Plex playlists and playqueues +""" import logging from urllib import quote from urlparse import parse_qsl, urlsplit @@ -5,13 +8,14 @@ from re import compile as re_compile import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU -from utils import JSONRPC, tryEncode, escape_html +from utils import tryEncode, escape_html from PlexAPI import API from PlexFunctions import GetPlexMetadata +import json_rpc as js ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = logging.getLogger("PLEX." + __name__) REGEX = re_compile(r'''metadata%2F(\d+)''') ############################################################################### @@ -29,7 +33,7 @@ class Playlist_Object_Baseclase(object): kodi_pl = None items = [] old_kodi_pl = [] - ID = None + id = None version = None selectedItemID = None selectedItemOffset = None @@ -43,10 +47,10 @@ class Playlist_Object_Baseclase(object): """ answ = "<%s: " % (self.__class__.__name__) # For some reason, can't use dir directly - answ += "ID: %s, " % self.ID + answ += "id: %s, " % self.id answ += "items: %s, " % self.items for key in self.__dict__: - if key not in ("ID", 'items'): + if key not in ("id", 'items'): if type(getattr(self, key)) in (str, unicode): answ += '%s: %s, ' % (key, tryEncode(getattr(self, key))) else: @@ -61,14 +65,14 @@ class Playlist_Object_Baseclase(object): self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] self.old_kodi_pl = [] - self.ID = None + self.id = None self.version = None self.selectedItemID = None self.selectedItemOffset = None self.shuffled = 0 self.repeat = 0 self.plex_transient_token = None - log.debug('Playlist cleared: %s' % self) + LOG.debug('Playlist cleared: %s', self) class Playlist_Object(Playlist_Object_Baseclase): @@ -82,12 +86,12 @@ class Playqueue_Object(Playlist_Object_Baseclase): """ PKC object to represent PMS playQueues and Kodi playlist for queueing - playlistid = None [int] Kodi playlist ID (0, 1, 2) + playlistid = None [int] Kodi playlist id (0, 1, 2) type = None [str] Kodi type: 'audio', 'video', 'picture' kodi_pl = None Kodi xbmc.PlayList object items = [] [list] of Playlist_Items old_kodi_pl = [] [list] store old Kodi JSON result with all pl items - ID = None [str] Plex playQueueID, unique Plex identifier + id = None [str] Plex playQueueID, unique Plex identifier version = None [int] Plex version of the playQueue selectedItemID = None [str] Plex selectedItemID, playing element in queue @@ -106,7 +110,7 @@ class Playlist_Item(object): """ Object to fill our playqueues and playlists with. - ID = None [str] Plex playlist/playqueue id, e.g. playQueueItemID + id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID plex_id = None [str] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' plex_UUID = None [str] Plex librarySectionUUID @@ -117,7 +121,7 @@ class Playlist_Item(object): guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below """ - ID = None + id = None plex_id = None plex_type = None plex_UUID = None @@ -176,7 +180,7 @@ def playlist_item_from_kodi(kodi_item): # 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)) - log.debug('Made playlist item from Kodi: %s' % item) + LOG.debug('Made playlist item from Kodi: %s', item) return item @@ -199,7 +203,7 @@ def playlist_item_from_plex(plex_id): item.plex_UUID = plex_id item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % (item.plex_UUID, plex_id)) - log.debug('Made playlist item from plex: %s' % item) + LOG.debug('Made playlist item from plex: %s', item) return item @@ -213,7 +217,7 @@ def playlist_item_from_xml(playlist, xml_video_element): api = API(xml_video_element) item.plex_id = api.getRatingKey() item.plex_type = api.getType() - item.ID = xml_video_element.attrib['%sItemID' % playlist.kind] + item.id = xml_video_element.attrib['%sItemID' % playlist.kind] item.guid = xml_video_element.attrib.get('guid') if item.guid is not None: item.guid = escape_html(item.guid) @@ -225,7 +229,7 @@ def playlist_item_from_xml(playlist, xml_video_element): except TypeError: pass item.xml = xml_video_element - log.debug('Created new playlist item from xml: %s' % item) + LOG.debug('Created new playlist item from xml: %s', item) return item @@ -237,8 +241,8 @@ def _get_playListVersion_from_xml(playlist, xml): 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.error('Could not get new playlist Version for playlist %s', + playlist) return False return True @@ -249,7 +253,7 @@ def get_playlist_details_from_xml(playlist, xml): playlist.ID with the XML's playQueueID """ try: - playlist.ID = xml.attrib['%sID' % playlist.kind] + playlist.id = xml.attrib['%sID' % playlist.kind] playlist.version = xml.attrib['%sVersion' % playlist.kind] playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] playlist.selectedItemID = xml.attrib.get( @@ -257,12 +261,12 @@ def get_playlist_details_from_xml(playlist, xml): playlist.selectedItemOffset = xml.attrib.get( '%sSelectedItemOffset' % playlist.kind) except: - log.error('Could not parse xml answer from PMS for playlist %s' - % playlist) + LOG.error('Could not parse xml answer from PMS for playlist %s', + playlist) import traceback - log.error(traceback.format_exc()) + LOG.error(traceback.format_exc()) raise KeyError - log.debug('Updated playlist from xml: %s' % playlist) + LOG.debug('Updated playlist from xml: %s', playlist) def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): @@ -280,7 +284,7 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): try: get_playlist_details_from_xml(playlist, xml) except KeyError: - log.error('Could not update playlist from PMS') + LOG.error('Could not update playlist from PMS') return for plex_item in xml: playlist_item = add_to_Kodi_playlist(playlist, plex_item) @@ -295,7 +299,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Returns True if successful, False otherwise """ - log.debug('Initializing the playlist %s on the Plex side' % playlist) + LOG.debug('Initializing the playlist %s on the Plex side', playlist) try: if plex_id: item = playlist_item_from_plex(plex_id) @@ -312,11 +316,11 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): get_playlist_details_from_xml(playlist, xml) item.xml = xml[0] except (KeyError, IndexError, TypeError): - log.error('Could not init Plex playlist with plex_id %s and ' + LOG.error('Could not init Plex playlist with plex_id %s and ' 'kodi_item %s', plex_id, kodi_item) return False playlist.items.append(item) - log.debug('Initialized the playlist on the Plex side: %s' % playlist) + LOG.debug('Initialized the playlist on the Plex side: %s' % playlist) return True @@ -329,10 +333,10 @@ def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, file: str!! """ - log.debug('add_listitem_to_playlist at position %s. Playlist before add: ' - '%s' % (pos, playlist)) + LOG.debug('add_listitem_to_playlist at position %s. Playlist before add: ' + '%s', pos, playlist) kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file} - if playlist.ID is None: + 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) @@ -358,9 +362,9 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, file: str! """ - log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist) + 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: + if playlist.id is None: success = init_Plex_playlist(playlist, plex_id, kodi_item) else: success = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) @@ -376,9 +380,9 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)} else: params['item'] = {'file': item.file} - reply = JSONRPC('Playlist.Insert').execute(params) + reply = js.playlist_insert(params) if reply.get('error') is not None: - log.error('Could not add item to playlist. Kodi reply. %s', reply) + LOG.error('Could not add item to playlist. Kodi reply. %s', reply) return False return True @@ -395,25 +399,24 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): try: item = playlist_item_from_plex(plex_id) except KeyError: - log.error('Could not add new item to the PMS playlist') + LOG.error('Could not add new item to the PMS playlist') return False else: item = playlist_item_from_kodi(kodi_item) - url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri) + url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri) # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: item.xml = xml[-1] item.ID = xml[-1].attrib['%sItemID' % playlist.kind] except IndexError: - log.info('Could not get playlist children. Adding a dummy') + LOG.info('Could not get playlist children. Adding a dummy') except (TypeError, AttributeError, KeyError): - log.error('Could not add item %s to playlist %s' - % (kodi_item, playlist)) + LOG.error('Could not add item %s to playlist %s', kodi_item, playlist) return False # Get the guid for this item for plex_item in xml: - if plex_item.attrib['%sItemID' % playlist.kind] == item.ID: + if plex_item.attrib['%sItemID' % playlist.kind] == item.id: item.guid = escape_html(plex_item.attrib['guid']) playlist.items.append(item) if pos == len(playlist.items) - 1: @@ -424,7 +427,7 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): move_playlist_item(playlist, len(playlist.items) - 1, pos) - log.debug('Successfully added item on the Plex side: %s' % playlist) + LOG.debug('Successfully added item on the Plex side: %s', playlist) return True @@ -437,9 +440,9 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, file: str! """ - 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)) + 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 @@ -448,18 +451,18 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % kodi_type: int(kodi_id)} else: params['item'] = {'file': file} - reply = JSONRPC('Playlist.Insert').execute(params) + reply = js.playlist_insert(params) if reply.get('error') is not None: - log.error('Could not add item to playlist. Kodi reply. %s' % reply) + LOG.error('Could not add item to playlist. Kodi reply. %s', reply) return False item = playlist_item_from_kodi( - {'id': kodi_id, 'type': kodi_type, 'file': file}, playlist) + {'id': kodi_id, 'type': kodi_type, 'file': file}) if item.plex_id is not None: xml = GetPlexMetadata(item.plex_id) try: item.xml = xml[-1] except (TypeError, IndexError): - log.error('Could not get metadata for playlist item %s', item) + LOG.error('Could not get metadata for playlist item %s', item) playlist.items.insert(pos, item) return True @@ -470,26 +473,26 @@ def move_playlist_item(playlist, before_pos, after_pos): WILL ALSO CHANGE OUR PLAYLISTS. Returns True if successful """ - log.debug('Moving item from %s to %s on the Plex side for %s' - % (before_pos, after_pos, playlist)) + LOG.debug('Moving item from %s to %s on the Plex side for %s', + before_pos, after_pos, playlist) if after_pos == 0: url = "{server}/%ss/%s/items/%s/move?after=0" % \ (playlist.kind, - playlist.ID, - playlist.items[before_pos].ID) + 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) + playlist.id, + playlist.items[before_pos].id, + playlist.items[after_pos - 1].id) # We need to increment the playlistVersion if _get_playListVersion_from_xml( playlist, DU().downloadUrl(url, action_type="PUT")) is False: return False # 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) + LOG.debug('Done moving for %s' % playlist) return True @@ -500,7 +503,7 @@ def get_PMS_playlist(playlist, playlist_id=None): Returns None if something went wrong """ - playlist_id = playlist_id if playlist_id else playlist.ID + playlist_id = playlist_id if playlist_id else playlist.id xml = DU().downloadUrl( "{server}/%ss/%s" % (playlist.kind, playlist_id), headerOptions={'Accept': 'application/xml'}) @@ -520,59 +523,24 @@ def refresh_playlist_from_PMS(playlist): try: get_playlist_details_from_xml(playlist, xml) except KeyError: - log.error('Could not refresh playlist from PMS') + LOG.error('Could not refresh playlist from PMS') def delete_playlist_item_from_PMS(playlist, pos): """ Delete the item at position pos [int] on the Plex side and our playlists """ - log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist)) + LOG.debug('Deleting position %s for %s on the Plex side', pos, playlist) xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" % (playlist.kind, - playlist.ID, - playlist.items[pos].ID, + playlist.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: - log.error('Could not get Kodi playqueues. JSON Result was: %s' - % queues) - queues = [] - return queues - - # Functions operating on the Kodi playlist objects ########## def add_to_Kodi_playlist(playlist, xml_video_element): @@ -583,17 +551,14 @@ def add_to_Kodi_playlist(playlist, xml_video_element): Returns a Playlist_Item or None if it did not work """ item = playlist_item_from_xml(playlist, xml_video_element) - params = { - 'playlistid': playlist.playlistid - } if item.kodi_id: - params['item'] = {'%sid' % item.kodi_type: item.kodi_id} + json_item = {'%sid' % item.kodi_type: item.kodi_id} else: - params['item'] = {'file': item.file} - reply = JSONRPC('Playlist.Add').execute(params) + json_item = {'file': item.file} + reply = js.playlist_add(playlist.playlistid, json_item) if reply.get('error') is not None: - log.error('Could not add item %s to Kodi playlist. Error: %s' - % (xml_video_element, reply)) + LOG.error('Could not add item %s to Kodi playlist. Error: %s', + xml_video_element, reply) return None return item @@ -607,8 +572,8 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, file: string! """ - log.debug('Insert listitem at position %s for Kodi only for %s' - % (pos, playlist)) + 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 @@ -619,29 +584,25 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, if file is not None: item.file = file playlist.items.insert(pos, item) - log.debug('Done inserting for %s' % playlist) + LOG.debug('Done inserting for %s', playlist) -def remove_from_Kodi_playlist(playlist, pos): +def remove_from_kodi_playlist(playlist, pos): """ Removes the item at position pos from the Kodi playlist using JSON. WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS """ - log.debug('Removing position %s from Kodi only from %s' % (pos, playlist)) - reply = JSONRPC('Playlist.Remove').execute({ - 'playlistid': playlist.playlistid, - 'position': pos - }) + LOG.debug('Removing position %s from Kodi only from %s', pos, playlist) + reply = js.playlist_remove(playlist.playlistid, pos) if reply.get('error') is not None: - log.error('Could not delete the item from the playlist. Error: %s' - % reply) + LOG.error('Could not delete the item from the playlist. Error: %s', + reply) return - else: - try: - del playlist.items[pos] - except IndexError: - log.error('Cannot delete position %s for %s' % (pos, playlist)) + try: + del playlist.items[pos] + except IndexError: + LOG.error('Cannot delete position %s for %s', pos, playlist) def get_pms_playqueue(playqueue_id): @@ -654,7 +615,7 @@ def get_pms_playqueue(playqueue_id): try: xml.attrib except AttributeError: - log.error('Could not download Plex playqueue %s' % playqueue_id) + LOG.error('Could not download Plex playqueue %s', playqueue_id) xml = None return xml @@ -669,12 +630,12 @@ def get_plextype_from_xml(xml): try: plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0] except IndexError: - log.error('Could not get plex_id from xml: %s' % xml.attrib) + LOG.error('Could not get plex_id from xml: %s', xml.attrib) return new_xml = GetPlexMetadata(plex_id) try: new_xml[0].attrib except (TypeError, IndexError, AttributeError): - log.error('Could not get plex metadata for plex id %s' % plex_id) + LOG.error('Could not get plex metadata for plex id %s', plex_id) return return new_xml[0].attrib.get('type') diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 2420ca0a..4203349d 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -1,5 +1,6 @@ -# -*- coding: utf-8 -*- -############################################################################### +""" +Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly +""" import logging from threading import RLock, Thread @@ -10,13 +11,14 @@ import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API from playbackutils import PlaybackUtils +import json_rpc as js import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = logging.getLogger("PLEX." + __name__) -# Lock used for playqueue manipulations -lock = RLock() +# lock used for playqueue manipulations +LOCK = RLock() PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### @@ -33,15 +35,15 @@ class Playqueue(Thread): def __init__(self, callback=None): self.__dict__ = self.__shared_state if self.playqueues is not None: - log.debug('Playqueue thread has already been initialized') + LOG.debug('Playqueue thread has already been initialized') Thread.__init__(self) return self.mgr = callback # Initialize Kodi playqueues - with lock: + with LOCK: self.playqueues = [] - for queue in PL.get_kodi_playqueues(): + for queue in js.get_playlists(): playqueue = PL.Playqueue_Object() playqueue.playlistid = queue['playlistid'] playqueue.type = queue['type'] @@ -59,7 +61,7 @@ class Playqueue(Thread): # 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) + LOG.debug('Initialized the Kodi play queues: %s' % self.playqueues) Thread.__init__(self) def get_playqueue_from_type(self, typus): @@ -67,7 +69,7 @@ class Playqueue(Thread): Returns the playqueue according to the typus ('video', 'audio', 'picture') passed in """ - with lock: + with LOCK: for playqueue in self.playqueues: if playqueue.type == typus: break @@ -85,7 +87,7 @@ class Playqueue(Thread): try: xml[0].attrib except (TypeError, IndexError, AttributeError): - log.error('Could not download the PMS xml for %s' % plex_id) + LOG.error('Could not download the PMS xml for %s' % plex_id) return playqueue = self.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) @@ -93,7 +95,7 @@ class Playqueue(Thread): for i, child in enumerate(xml): api = API(child) PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) - log.debug('Firing up Kodi player') + LOG.debug('Firing up Kodi player') Player().play(playqueue.kodi_pl, None, False, 0) return playqueue @@ -109,15 +111,15 @@ class Playqueue(Thread): repeat = 0, 1, 2 offset = time offset in Plextime (milliseconds) """ - log.info('New playqueue %s received from Plex companion with offset ' + LOG.info('New playqueue %s received from Plex companion with offset ' '%s, repeat %s' % (playqueue_id, offset, repeat)) - with lock: + with LOCK: xml = PL.get_PMS_playlist(playqueue, playqueue_id) playqueue.clear() try: PL.get_playlist_details_from_xml(playqueue, xml) except KeyError: - log.error('Could not get playqueue ID %s' % playqueue_id) + LOG.error('Could not get playqueue ID %s' % playqueue_id) return PlaybackUtils(xml, playqueue).play_all() playqueue.repeat = 0 if not repeat else int(repeat) @@ -126,12 +128,12 @@ class Playqueue(Thread): window('plex_customplaylist.seektime', str(ConvertPlexToKodiTime(offset))) for startpos, item in enumerate(playqueue.items): - if item.ID == playqueue.selectedItemID: + if item.id == playqueue.selectedItemID: break else: startpos = 0 # Start playback. Player does not return in time - log.debug('Playqueues after Plex Companion update are now: %s' + LOG.debug('Playqueues after Plex Companion update are now: %s' % self.playqueues) thread = Thread(target=Player().play, args=(playqueue.kodi_pl, @@ -147,7 +149,7 @@ class Playqueue(Thread): """ old = list(playqueue.items) index = list(range(0, len(old))) - log.debug('Comparing new Kodi playqueue %s with our play queue %s' + LOG.debug('Comparing new Kodi playqueue %s with our play queue %s' % (new, old)) if self.thread_stopped(): # Chances are that we got an empty Kodi playlist due to @@ -176,15 +178,15 @@ class Playqueue(Thread): del old[j], index[j] break elif identical: - log.debug('Detected playqueue item %s moved to position %s' + LOG.debug('Detected playqueue item %s moved to position %s' % (i+j, i)) PL.move_playlist_item(playqueue, i + j, i) del old[j], index[j] break else: - log.debug('Detected new Kodi element at position %s: %s ' + LOG.debug('Detected new Kodi element at position %s: %s ' % (i, new_item)) - if playqueue.ID is None: + if playqueue.id is None: PL.init_Plex_playlist(playqueue, kodi_item=new_item) else: @@ -194,17 +196,18 @@ class Playqueue(Thread): for j in range(i, len(index)): index[j] += 1 for i in reversed(index): - log.debug('Detected deletion of playqueue element at pos %s' % i) + LOG.debug('Detected deletion of playqueue element at pos %s' % i) PL.delete_playlist_item_from_PMS(playqueue, i) - log.debug('Done comparing playqueues') + LOG.debug('Done comparing playqueues') def run(self): thread_stopped = self.thread_stopped thread_suspended = self.thread_suspended - log.info("----===## Starting PlayQueue client ##===----") + 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)): + for i, item in enumerate(js.playlist_get_items( + playqueue.id, ["title", "file"])): if i == 0: PL.init_Plex_playlist(playqueue, kodi_item=item) else: @@ -214,9 +217,10 @@ class Playqueue(Thread): if thread_stopped(): break sleep(1000) - with lock: + with LOCK: for playqueue in self.playqueues: - kodi_playqueue = PL.get_kodi_playlist_items(playqueue) + kodi_playqueue = js.playlist_get_items(playqueue.id, + ["title", "file"]) if playqueue.old_kodi_pl != kodi_playqueue: # compare old and new playqueue self._compare_playqueues(playqueue, kodi_playqueue) @@ -226,4 +230,4 @@ class Playqueue(Thread): sleep(10) continue sleep(200) - log.info("----===## PlayQueue client stopped ##===----") + LOG.info("----===## PlayQueue client stopped ##===----") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 3b5f9f8f..123ebd0d 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -333,14 +333,14 @@ def create_actor_db_index(): def getScreensaver(): # Get the current screensaver value params = {'setting': "screensaver.mode"} - return JSONRPC('Settings.getSettingValue').execute(params)['result']['value'] + return jsonrpc('Settings.getSettingValue').execute(params)['result']['value'] def setScreensaver(value): # Toggle the screensaver params = {'setting': "screensaver.mode", 'value': value} log.debug('Toggling screensaver to "%s": %s' - % (value, JSONRPC('Settings.setSettingValue').execute(params))) + % (value, jsonrpc('Settings.setSettingValue').execute(params))) def reset(): @@ -1141,7 +1141,7 @@ def changePlayState(itemType, kodiId, playCount, lastplayed): log.debug("JSON result was: %s" % result) -class JSONRPC(object): +class jsonrpc(object): id_ = 1 jsonrpc = "2.0" From 34cd0fadb456bddcb560caa301566c36e856ca61 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 8 Dec 2017 20:24:36 +0100 Subject: [PATCH 118/509] Delete obsolete screensaver function --- resources/lib/json_rpc.py | 3 ++- resources/lib/librarysync.py | 20 ++++++++++---------- resources/lib/utils.py | 13 ------------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index f42499f1..5782dd4a 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -272,7 +272,8 @@ def playlist_remove(playlistid, position): def get_setting(setting): """ - Returns the Kodi setting, a [str], or None if not possible + Returns the Kodi setting (GetSettingValue), a [str], or None if not + possible """ try: ret = jsonrpc('Settings.GetSettingValue').execute( diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index c10e39ba..6943ec65 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -9,15 +9,16 @@ import xbmc from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ - thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\ - setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ - tryDecode, deletePlaylists, deleteNodes, tryEncode, compare_version + thread_methods, create_actor_db_index, dialog, LogTime, playlistXSP,\ + language as lang, DateToKodi, reset, tryDecode, deletePlaylists, \ + deleteNodes, tryEncode, compare_version import downloadutils import itemtypes import plexdb_functions as plexdb import kodidb_functions as kodidb import userclient import videonodes +import json_rpc as js import variables as v from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \ @@ -264,9 +265,8 @@ class LibrarySync(Thread): def _fullSync(self): xbmc.executebuiltin('InhibitIdleShutdown(true)') - screensaver = getScreensaver() - setScreensaver(value="") - + screensaver = js.get_setting('screensaver.mode') + js.set_setting('screensaver.mode', '') if self.new_items_only is True: # Only do the following once for new items # Add sources @@ -275,7 +275,7 @@ class LibrarySync(Thread): # Set views. Abort if unsuccessful if not self.maintainViews(): xbmc.executebuiltin('InhibitIdleShutdown(false)') - setScreensaver(value=screensaver) + js.set_setting('screensaver.mode', screensaver) return False process = { @@ -291,7 +291,7 @@ class LibrarySync(Thread): self.thread_suspended() or not process[itemtype]()): xbmc.executebuiltin('InhibitIdleShutdown(false)') - setScreensaver(value=screensaver) + js.set_setting('screensaver.mode', screensaver) return False # Let kodi update the views in any case, since we're doing a full sync @@ -301,7 +301,7 @@ class LibrarySync(Thread): window('plex_initialScan', clear=True) xbmc.executebuiltin('InhibitIdleShutdown(false)') - setScreensaver(value=screensaver) + js.set_setting('screensaver.mode', screensaver) if window('plex_scancrashed') == 'true': # Show warning if itemtypes.py crashed at some point dialog('ok', heading='{plex}', line1=lang(39408)) @@ -320,7 +320,7 @@ class LibrarySync(Thread): except Exception as e: # Empty movies, tv shows? log.error('Path hack failed with error message: %s' % str(e)) - setScreensaver(value=screensaver) + js.set_setting('screensaver.mode', screensaver) return True def processView(self, folderItem, kodi_db, plex_db, totalnodes): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 123ebd0d..52a39991 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -330,19 +330,6 @@ def create_actor_db_index(): conn.close() -def getScreensaver(): - # Get the current screensaver value - params = {'setting': "screensaver.mode"} - return jsonrpc('Settings.getSettingValue').execute(params)['result']['value'] - - -def setScreensaver(value): - # Toggle the screensaver - params = {'setting': "screensaver.mode", 'value': value} - log.debug('Toggling screensaver to "%s": %s' - % (value, jsonrpc('Settings.setSettingValue').execute(params))) - - def reset(): # Are you sure you want to reset your local Kodi database? if not dialog('yesno', From dfdc6eefd0b4703c1eeb6724ebb2ca76733e2281 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 8 Dec 2017 20:32:10 +0100 Subject: [PATCH 119/509] Move jsonrpc function --- resources/lib/json_rpc.py | 37 ++++++++++++++- resources/lib/utils.py | 99 +++++++++++++-------------------------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 5782dd4a..a965881d 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -2,7 +2,42 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ -from utils import jsonrpc, milliseconds_to_kodi_time +from json import loads, dumps +from utils import milliseconds_to_kodi_time +from xbmc import executeJSONRPC + + +class jsonrpc(object): + """ + Used for all Kodi JSON RPC calls. + """ + id_ = 1 + jsonrpc = "2.0" + + def __init__(self, method, **kwargs): + """ + Initialize with the Kodi method + """ + 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 dumps(query) + + def execute(self, params=None): + """ + Pass any params as a dict. Will return Kodi's answer as a dict. + """ + self.params = params + return loads(executeJSONRPC(self._query())) def get_players(): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 52a39991..5184b4d2 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -3,7 +3,6 @@ ############################################################################### import logging from cProfile import Profile -from json import loads, dumps from pstats import Stats from sqlite3 import connect, OperationalError from datetime import datetime, timedelta @@ -1083,70 +1082,38 @@ class Lock_Function: # UNUSED METHODS -def changePlayState(itemType, kodiId, playCount, lastplayed): - """ - YET UNUSED +# def changePlayState(itemType, kodiId, playCount, lastplayed): +# """ +# YET UNUSED - kodiId: int or str - playCount: int or str - lastplayed: str or int unix timestamp - """ - lastplayed = DateToKodi(lastplayed) +# kodiId: int or str +# playCount: int or str +# lastplayed: str or int unix timestamp +# """ +# lastplayed = DateToKodi(lastplayed) - kodiId = int(kodiId) - playCount = int(playCount) - method = { - 'movie': ' VideoLibrary.SetMovieDetails', - 'episode': 'VideoLibrary.SetEpisodeDetails', - 'musicvideo': ' VideoLibrary.SetMusicVideoDetails', # TODO - 'show': 'VideoLibrary.SetTVShowDetails', # TODO - '': 'AudioLibrary.SetAlbumDetails', # TODO - '': 'AudioLibrary.SetArtistDetails', # TODO - 'track': 'AudioLibrary.SetSongDetails' - } - params = { - 'movie': { - 'movieid': kodiId, - 'playcount': playCount, - 'lastplayed': lastplayed - }, - 'episode': { - 'episodeid': kodiId, - 'playcount': playCount, - 'lastplayed': lastplayed - } - } - query = { - "jsonrpc": "2.0", - "id": 1, - } - query['method'] = method[itemType] - query['params'] = params[itemType] - result = xbmc.executeJSONRPC(dumps(query)) - result = 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 dumps(query) - - def execute(self, params=None): - self.params = params - return loads(xbmc.executeJSONRPC(self._query())) +# kodiId = int(kodiId) +# playCount = int(playCount) +# method = { +# 'movie': ' VideoLibrary.SetMovieDetails', +# 'episode': 'VideoLibrary.SetEpisodeDetails', +# 'musicvideo': ' VideoLibrary.SetMusicVideoDetails', # TODO +# 'show': 'VideoLibrary.SetTVShowDetails', # TODO +# '': 'AudioLibrary.SetAlbumDetails', # TODO +# '': 'AudioLibrary.SetArtistDetails', # TODO +# 'track': 'AudioLibrary.SetSongDetails' +# } +# params = { +# 'movie': { +# 'movieid': kodiId, +# 'playcount': playCount, +# 'lastplayed': lastplayed +# }, +# 'episode': { +# 'episodeid': kodiId, +# 'playcount': playCount, +# 'lastplayed': lastplayed +# } +# } +# result = jsonrpc(method[itemType]).execute(params[itemType]) +# log.debug("JSON result was: %s" % result) From 9380a2386707a36035810096e425f7d589b61915 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 8 Dec 2017 20:35:32 +0100 Subject: [PATCH 120/509] Fix typo --- resources/lib/json_rpc.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index a965881d..19ae0ef1 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -193,49 +193,49 @@ def skipprevious(): def input_up(): """ - Tells Kodi the users pushed up + Tells Kodi the user pushed up """ jsonrpc("Input.Up").execute() def input_down(): """ - Tells Kodi the users pushed down + Tells Kodi the user pushed down """ jsonrpc("Input.Down").execute() def input_left(): """ - Tells Kodi the users pushed left + Tells Kodi the user pushed left """ jsonrpc("Input.Left").execute() def input_right(): """ - Tells Kodi the users pushed left + Tells Kodi the user pushed left """ jsonrpc("Input.Right").execute() def input_select(): """ - Tells Kodi the users pushed select + Tells Kodi the user pushed select """ jsonrpc("Input.Select").execute() def input_home(): """ - Tells Kodi the users pushed home + Tells Kodi the user pushed home """ jsonrpc("Input.Home").execute() def input_back(): """ - Tells Kodi the users pushed back + Tells Kodi the user pushed back """ jsonrpc("Input.Back").execute() From cceb110354a7aab3b26aab6f3bb4a7bffbb05760 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 8 Dec 2017 20:41:11 +0100 Subject: [PATCH 121/509] Always return JSON RPC answer --- resources/lib/json_rpc.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 19ae0ef1..46da124f 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -195,49 +195,49 @@ def input_up(): """ Tells Kodi the user pushed up """ - jsonrpc("Input.Up").execute() + return jsonrpc("Input.Up").execute() def input_down(): """ Tells Kodi the user pushed down """ - jsonrpc("Input.Down").execute() + return jsonrpc("Input.Down").execute() def input_left(): """ Tells Kodi the user pushed left """ - jsonrpc("Input.Left").execute() + return jsonrpc("Input.Left").execute() def input_right(): """ Tells Kodi the user pushed left """ - jsonrpc("Input.Right").execute() + return jsonrpc("Input.Right").execute() def input_select(): """ Tells Kodi the user pushed select """ - jsonrpc("Input.Select").execute() + return jsonrpc("Input.Select").execute() def input_home(): """ Tells Kodi the user pushed home """ - jsonrpc("Input.Home").execute() + return jsonrpc("Input.Home").execute() def input_back(): """ Tells Kodi the user pushed back """ - jsonrpc("Input.Back").execute() + return jsonrpc("Input.Back").execute() def playlist_get_items(playlistid, properties): From 843bedbee6f06e7bc1c4adc5360383d0c8d9f8c3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 13:47:19 +0100 Subject: [PATCH 122/509] Switch Companion to use json_rpc.py --- resources/lib/PlexCompanion.py | 8 +- resources/lib/json_rpc.py | 47 +++- resources/lib/plexbmchelper/functions.py | 244 --------------------- resources/lib/plexbmchelper/listener.py | 26 +-- resources/lib/plexbmchelper/subscribers.py | 72 +++--- resources/lib/utils.py | 26 ++- resources/lib/variables.py | 5 + 7 files changed, 119 insertions(+), 309 deletions(-) delete mode 100644 resources/lib/plexbmchelper/functions.py diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 9cf71bf5..88e472b4 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -8,8 +8,8 @@ from urllib import urlencode from xbmc import sleep, executebuiltin from utils import settings, thread_methods -from plexbmchelper import listener, plexgdm, subscribers, functions, \ - httppersist, plexsettings +from plexbmchelper import listener, plexgdm, subscribers, httppersist, \ + plexsettings from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API from playlist_func import get_pms_playqueue, get_plextype_from_xml @@ -196,9 +196,8 @@ class PlexCompanion(Thread): # Start up instances requestMgr = httppersist.RequestMgr() - jsonClass = functions.jsonClass(requestMgr, self.settings) subscriptionManager = subscribers.SubscriptionManager( - jsonClass, requestMgr, self.player, self.mgr) + requestMgr, self.player, self.mgr) queue = Queue.Queue(maxsize=100) self.queue = queue @@ -211,7 +210,6 @@ class PlexCompanion(Thread): httpd = listener.ThreadedHTTPServer( client, subscriptionManager, - jsonClass, self.settings, queue, ('', self.settings['myport']), diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 46da124f..9cb0d679 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -3,7 +3,7 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ from json import loads, dumps -from utils import milliseconds_to_kodi_time +from utils import millis_to_kodi_time from xbmc import executeJSONRPC @@ -49,7 +49,7 @@ def get_players(): 'picture': ... } """ - info = jsonrpc("Player.GetActivePlayers").execute()['result'] or [] + info = jsonrpc("Player.GetActivePlayers").execute()['result'] ret = {} for player in info: player['playerid'] = int(player['playerid']) @@ -152,7 +152,7 @@ def seek_to(offset): for playerid in get_player_ids(): jsonrpc("Player.Seek").execute( {"playerid": playerid, - "value": milliseconds_to_kodi_time(offset)}) + "value": millis_to_kodi_time(offset)}) def smallforward(): @@ -240,6 +240,13 @@ def input_back(): return jsonrpc("Input.Back").execute() +def input_sendtext(text): + """ + Tells Kodi the user sent text [unicode] + """ + return jsonrpc("Input.SendText").execute({'test': text, 'done': False}) + + def playlist_get_items(playlistid, properties): """ playlistid: [int] id of the Kodi playlist @@ -350,6 +357,33 @@ def get_episodes(params): return ret +def get_player_props(playerid): + """ + Returns a dict for the active Kodi player with the following values: + { + 'type' [str] the Kodi player type, e.g. 'video' + 'time' The current item's time in Kodi time + 'totaltime' The current item's total length in Kodi time + 'speed' [int] playback speed, defaults to 0 + 'shuffled' [bool] True if shuffled + 'repeat' [str] 'off', 'one', 'all' + 'position' [int] position in playlist (or -1) + 'playlistid' [int] the Kodi playlist id (or -1) + } + """ + ret = jsonrpc('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['type', + 'time', + 'totaltime', + 'speed', + 'shuffled', + 'repeat', + 'position', + 'playlistid']}) + return ret['result'] + + def current_audiostream(playerid): """ Returns a dict of the active audiostream for playerid [int]: @@ -402,3 +436,10 @@ def subtitle_enabled(playerid): except (KeyError, TypeError): ret = False return ret + + +def ping(): + """ + Pings the JSON RPC interface + """ + return jsonrpc('JSONRPC.Ping').execute() diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py deleted file mode 100644 index 784a1e77..00000000 --- a/resources/lib/plexbmchelper/functions.py +++ /dev/null @@ -1,244 +0,0 @@ -import logging -import base64 -import json -import string - -import xbmc - -import plexdb_functions as plexdb - -############################################################################### - -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -def xbmc_photo(): - return "photo" - - -def xbmc_video(): - return "video" - - -def xbmc_audio(): - return "audio" - - -def plex_photo(): - return "photo" - - -def plex_video(): - return "video" - - -def plex_audio(): - return "music" - - -def xbmc_type(plex_type): - if plex_type == plex_photo(): - return xbmc_photo() - elif plex_type == plex_video(): - return xbmc_video() - elif plex_type == plex_audio(): - return xbmc_audio() - - -def plex_type(xbmc_type): - if xbmc_type == xbmc_photo(): - return plex_photo() - elif xbmc_type == xbmc_video(): - return plex_video() - elif xbmc_type == xbmc_audio(): - return plex_audio() - - -def getXMLHeader(): - return '\n' - - -def getOKMsg(): - return getXMLHeader() + '' - - -def timeToMillis(time): - return (time['hours']*3600 + - time['minutes']*60 + - time['seconds'])*1000 + time['milliseconds'] - - -def millisToTime(t): - millis = int(t) - seconds = millis / 1000 - minutes = seconds / 60 - hours = minutes / 60 - seconds = seconds % 60 - minutes = minutes % 60 - millis = millis % 1000 - return {'hours': hours, - 'minutes': minutes, - 'seconds': seconds, - 'milliseconds': millis} - - -def textFromXml(element): - return element.firstChild.data - - -class jsonClass(): - - def __init__(self, requestMgr, settings): - self.settings = settings - self.requestMgr = requestMgr - - def jsonrpc(self, action, arguments={}): - """ put some JSON together for the JSON-RPC APIv6 """ - if action.lower() == "sendkey": - request = json.dumps({ - "jsonrpc": "2.0", - "method": "Input.SendText", - "params": { - "text": arguments[0], - "done": False - } - }) - elif action.lower() == "ping": - request = json.dumps({ - "jsonrpc": "2.0", - "id": 1, - "method": "JSONRPC.Ping" - }) - elif arguments: - request = json.dumps({ - "id": 1, - "jsonrpc": "2.0", - "method": action, - "params": arguments}) - else: - request = json.dumps({ - "id": 1, - "jsonrpc": "2.0", - "method": action - }) - - result = self.parseJSONRPC(xbmc.executeJSONRPC(request)) - - if not result and self.settings['webserver_enabled']: - # xbmc.executeJSONRPC appears to fail on the login screen, but - # going through the network stack works, so let's try the request - # again - result = self.parseJSONRPC(self.requestMgr.post( - "127.0.0.1", - self.settings['port'], - "/jsonrpc", - request, - {'Content-Type': 'application/json', - 'Authorization': 'Basic %s' % string.strip( - base64.encodestring('%s:%s' - % (self.settings['user'], - self.settings['passwd']))) - })) - return result - - def skipTo(self, plexId, typus): - # playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus))) - # playerId = self. - with plexdb.Get_Plex_DB() as plex_db: - plexdb_item = plex_db.getItem_byId(plexId) - try: - dbid = plexdb_item[0] - mediatype = plexdb_item[4] - except TypeError: - log.info('Couldnt find item %s in Kodi db' % plexId) - return - log.debug('plexid: %s, kodi id: %s, type: %s' - % (plexId, dbid, mediatype)) - - def getPlexHeaders(self): - h = { - "Content-type": "text/xml", - "Access-Control-Allow-Origin": "*", - "X-Plex-Version": self.settings['version'], - "X-Plex-Client-Identifier": self.settings['uuid'], - "X-Plex-Provides": "client,controller,player", - "X-Plex-Product": "PlexKodiConnect", - "X-Plex-Device-Name": self.settings['client_name'], - "X-Plex-Platform": "Kodi", - "X-Plex-Model": self.settings['platform'], - "X-Plex-Device": "PC", - } - if self.settings['myplex_user']: - h["X-Plex-Username"] = self.settings['myplex_user'] - return h - - def parseJSONRPC(self, jsonraw): - if not jsonraw: - log.debug("Empty response from Kodi") - return {} - else: - parsed = json.loads(jsonraw) - if parsed.get('error', False): - log.error("Kodi returned an error: %s" % parsed.get('error')) - return parsed.get('result', {}) - - def getPlayers(self): - info = self.jsonrpc("Player.GetActivePlayers") or [] - ret = {} - for player in info: - player['playerid'] = int(player['playerid']) - ret[player['type']] = player - return ret - - def getPlaylistId(self, typus): - """ - typus: one of the Kodi types, e.g. audio or video - - Returns None if nothing was found - """ - for playlist in self.getPlaylists(): - if playlist.get('type') == typus: - return playlist.get('playlistid') - - def getPlaylists(self): - """ - Returns a list, e.g. - [ - {u'playlistid': 0, u'type': u'audio'}, - {u'playlistid': 1, u'type': u'video'}, - {u'playlistid': 2, u'type': u'picture'} - ] - """ - return self.jsonrpc('Playlist.GetPlaylists') - - def getPlayerIds(self): - ret = [] - for player in self.getPlayers().values(): - ret.append(player['playerid']) - return ret - - def getVideoPlayerId(self, players=False): - if players is None: - players = self.getPlayers() - return players.get(xbmc_video(), {}).get('playerid', None) - - def getAudioPlayerId(self, players=False): - if players is None: - players = self.getPlayers() - return players.get(xbmc_audio(), {}).get('playerid', None) - - def getPhotoPlayerId(self, players=False): - if players is None: - players = self.getPlayers() - return players.get(xbmc_photo(), {}).get('playerid', None) - - def getVolume(self): - answ = self.jsonrpc('Application.GetProperties', - { - "properties": ["volume", 'muted'] - }) - vol = str(answ.get('volume', 100)) - mute = ("0", "1")[answ.get('muted', False)] - return (vol, mute) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index c07e9c00..e177212f 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -8,9 +8,9 @@ from urlparse import urlparse, parse_qs from xbmc import sleep from companion import process_command from utils import window - -from functions import * - +import json_rpc as js +from clientinfo import getXArgsDeviceInfo +import variables as v ############################################################################### @@ -82,7 +82,6 @@ class MyHandler(BaseHTTPRequestHandler): def answer_request(self, sendData): self.serverlist = self.server.client.getServerList() subMgr = self.server.subscriptionManager - js = self.server.jsonClass settings = self.server.settings try: @@ -105,7 +104,7 @@ class MyHandler(BaseHTTPRequestHandler): % settings['version']) elif request_path == "verify": self.response("XBMC JSON connection test:\n" + - js.jsonrpc("ping")) + js.ping()) elif "resources" == request_path: resp = ('%s' '' @@ -121,15 +120,15 @@ class MyHandler(BaseHTTPRequestHandler): ' deviceClass="pc"' '/>' '' - % (getXMLHeader(), + % (v.XML_HEADER, settings['client_name'], settings['uuid'], settings['platform'], settings['plexbmc_version'])) log.debug("crafted resources response: %s" % resp) - self.response(resp, js.getPlexHeaders()) + self.response(resp, getXArgsDeviceInfo()) elif "/subscribe" in request_path: - self.response(getOKMsg(), js.getPlexHeaders()) + self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo()) protocol = params.get('protocol', False) host = self.client_address[0] port = params.get('port', False) @@ -147,7 +146,7 @@ class MyHandler(BaseHTTPRequestHandler): self.response( sub(r"INSERTCOMMANDID", str(commandID), - subMgr.msg(js.getPlayers())), + subMgr.msg(js.get_players())), { 'X-Plex-Client-Identifier': settings['uuid'], 'Access-Control-Expose-Headers': @@ -156,14 +155,14 @@ class MyHandler(BaseHTTPRequestHandler): 'Content-Type': 'text/xml' }) elif "/unsubscribe" in request_path: - self.response(getOKMsg(), js.getPlexHeaders()) + self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo()) uuid = self.headers.get('X-Plex-Client-Identifier', False) \ or self.client_address[0] subMgr.removeSubscriber(uuid) else: # Throw it to companion.py process_command(request_path, params, self.server.queue) - self.response('', js.getPlexHeaders()) + self.response('', getXArgsDeviceInfo()) subMgr.notify() except: log.error('Error encountered. Traceback:') @@ -174,17 +173,16 @@ class MyHandler(BaseHTTPRequestHandler): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True - def __init__(self, client, subscriptionManager, jsonClass, settings, + def __init__(self, client, subscriptionManager, settings, queue, *args, **kwargs): """ client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to- date serverlist without instantiating anything - same for SubscriptionManager and jsonClass + same for SubscriptionManager """ self.client = client self.subscriptionManager = subscriptionManager - self.jsonClass = jsonClass self.settings = settings self.queue = queue HTTPServer.__init__(self, *args, **kwargs) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 82d4d833..5345fea4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -2,12 +2,15 @@ import logging import re import threading +from xbmc import sleep + import downloadutils from clientinfo import getXArgsDeviceInfo -from utils import window +from utils import window, kodi_time_to_millis import PlexFunctions as pf import state -from functions import * +import variables as v +import json_rpc as js ############################################################################### @@ -17,7 +20,7 @@ log = logging.getLogger("PLEX."+__name__) class SubscriptionManager: - def __init__(self, jsonClass, RequestMgr, player, mgr): + def __init__(self, RequestMgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} @@ -30,8 +33,6 @@ class SubscriptionManager: 'audio': {}, 'picture': {} } - self.volume = 0 - self.mute = '0' self.server = "" self.protocol = "http" self.port = "" @@ -40,7 +41,6 @@ class SubscriptionManager: self.xbmcplayer = player self.playqueue = mgr.playqueue - self.js = jsonClass self.RequestMgr = RequestMgr def getServerByHost(self, host): @@ -52,32 +52,34 @@ class SubscriptionManager: return server return {} - def getVolume(self): - self.volume, self.mute = self.js.getVolume() - def msg(self, players): - msg = getXMLHeader() + log.debug('players: %s', players) + msg = v.XML_HEADER msg += ' 300: + if count > 30: break keyid = window('plex_currently_playing_itemid') - xbmc.sleep(100) + sleep(100) count += 1 if keyid: self.lastkey = "/library/metadata/%s" % keyid @@ -119,7 +121,7 @@ class SubscriptionManager: ret += ' port="%s"' % serv.get('port', self.port) ret += ' volume="%s"' % info['volume'] ret += ' shuffle="%s"' % info['shuffle'] - ret += ' mute="%s"' % self.mute + ret += ' mute="%s"' % info['mute'] ret += ' repeat="%s"' % info['repeat'] ret += ' itemType="%s"' % ptype if state.PLEX_TRANSIENT_TOKEN: @@ -145,7 +147,7 @@ class SubscriptionManager: if (not window('plex_currently_playing_itemid') and not self.lastplayers): return True - players = self.js.getPlayers() + players = js.get_players() # fetch the message, subscribers or not, since the server # will need the info anyway msg = self.msg(players) @@ -233,27 +235,15 @@ class SubscriptionManager: # Get the playqueue playqueue = self.playqueue.playqueues[playerid] # get info from the player - props = self.js.jsonrpc( - "Player.GetProperties", - {"playerid": playerid, - "properties": ["type", - "time", - "totaltime", - "speed", - "shuffled", - "repeat"]}) + props = js.get_player_props(playerid) info = { - 'time': timeToMillis(props['time']), - 'duration': timeToMillis(props['totaltime']), + 'time': kodi_time_to_millis(props['time']), + 'duration': kodi_time_to_millis(props['totaltime']), 'state': ("paused", "playing")[int(props['speed'])], 'shuffle': ("0", "1")[props.get('shuffled', False)], 'repeat': pf.getPlexRepeat(props.get('repeat')), } - # Get the playlist position - pos = self.js.jsonrpc( - "Player.GetProperties", - {"playerid": playerid, - "properties": ["position"]})['position'] + pos = props['position'] try: info['playQueueItemID'] = playqueue.items[pos].ID or 'null' info['guid'] = playqueue.items[pos].guid or 'null' @@ -274,8 +264,8 @@ class SubscriptionManager: } # get the volume from the application - info['volume'] = self.volume - info['mute'] = self.mute + info['volume'] = js.get_volume() + info['mute'] = js.get_muted() info['plex_transient_token'] = playqueue.plex_transient_token diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 5184b4d2..9ccc80b9 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -179,9 +179,15 @@ def dialog(typus, *args, **kwargs): return types[typus](*args, **kwargs) -def milliseconds_to_kodi_time(milliseconds): +def millis_to_kodi_time(milliseconds): """ - Converts time in milliseconds to the time dict used by the Kodi JSON RPC + Converts time in milliseconds to the time dict used by the Kodi JSON RPC: + { + 'hours': [int], + 'minutes': [int], + 'seconds'[int], + 'milliseconds': [int] + } Pass in the time in milliseconds as an int """ seconds = milliseconds / 1000 @@ -196,6 +202,22 @@ def milliseconds_to_kodi_time(milliseconds): 'milliseconds': milliseconds} +def kodi_time_to_millis(time): + """ + Converts the Kodi time dict + { + 'hours': [int], + 'minutes': [int], + 'seconds'[int], + 'milliseconds': [int] + } + to milliseconds [int] + """ + return (time['hours']*3600 + + time['minutes']*60 + + time['seconds'])*1000 + time['milliseconds'] + + def tryEncode(uniString, encoding='utf-8'): """ Will try to encode uniString (in unicode) to encoding. This possibly diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 856197d3..525bb29c 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -361,3 +361,8 @@ SORT_METHODS_ALBUMS = ( 'SORT_METHOD_ARTIST', 'SORT_METHOD_ALBUM', ) + + +XML_HEADER = '\n' + +COMPANION_OK_MESSAGE = XML_HEADER + '' From 73c7f866e6d48864ba4fb8e3537dd2357ebdb690 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 13:54:30 +0100 Subject: [PATCH 123/509] Security fix: Companion shall not send Plex token --- resources/lib/clientinfo.py | 6 ++++-- resources/lib/plexbmchelper/listener.py | 10 ++++++---- resources/lib/plexbmchelper/subscribers.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index dfddae5f..4a1da15d 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -13,7 +13,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -def getXArgsDeviceInfo(options=None): +def getXArgsDeviceInfo(options=None, include_token=True): """ Returns a dictionary that can be used as headers for GET and POST requests. An authentication option is NOT yet added. @@ -21,6 +21,8 @@ def getXArgsDeviceInfo(options=None): Inputs: options: dictionary of options that will override the standard header options otherwise set. + include_token: set to False if you don't want to include the Plex token + (e.g. for Companion communication) Output: header dictionary """ @@ -41,7 +43,7 @@ def getXArgsDeviceInfo(options=None): 'X-Plex-Client-Identifier': getDeviceId(), 'X-Plex-Provides': 'client,controller,player,pubsub-player', } - if window('pms_token'): + if include_token and window('pms_token'): xargs['X-Plex-Token'] = window('pms_token') if options is not None: xargs.update(options) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index e177212f..e93f07de 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -126,9 +126,10 @@ class MyHandler(BaseHTTPRequestHandler): settings['platform'], settings['plexbmc_version'])) log.debug("crafted resources response: %s" % resp) - self.response(resp, getXArgsDeviceInfo()) + self.response(resp, getXArgsDeviceInfo(include_token=False)) elif "/subscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo()) + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) protocol = params.get('protocol', False) host = self.client_address[0] port = params.get('port', False) @@ -155,14 +156,15 @@ class MyHandler(BaseHTTPRequestHandler): 'Content-Type': 'text/xml' }) elif "/unsubscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo()) + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) uuid = self.headers.get('X-Plex-Client-Identifier', False) \ or self.client_address[0] subMgr.removeSubscriber(uuid) else: # Throw it to companion.py process_command(request_path, params, self.server.queue) - self.response('', getXArgsDeviceInfo()) + self.response('', getXArgsDeviceInfo(include_token=False)) subMgr.notify() except: log.error('Error encountered. Traceback:') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 5345fea4..f6e40ef1 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -176,7 +176,7 @@ class SubscriptionManager: def _sendNotification(self, info, playerid): playqueue = self.playqueue.playqueues[playerid] - xargs = getXArgsDeviceInfo() + xargs = getXArgsDeviceInfo(include_token=False) params = { 'containerKey': self.containerKey or "/library/metadata/900000", 'key': self.lastkey or "/library/metadata/900000", From cdd38c6ef77e5d8295fa765c629c89c4e512c1de Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 14:35:08 +0100 Subject: [PATCH 124/509] Optimize some imports --- resources/lib/PlexAPI.py | 4 ++-- resources/lib/PlexCompanion.py | 12 ++++++------ resources/lib/artwork.py | 4 ++-- resources/lib/clientinfo.py | 4 ++-- resources/lib/context_entry.py | 2 -- resources/lib/downloadutils.py | 7 +++---- resources/lib/entrypoint.py | 4 ++-- resources/lib/initialsetup.py | 4 ++-- resources/lib/itemtypes.py | 5 ++--- resources/lib/json_rpc.py | 3 +++ resources/lib/kodidb_functions.py | 5 ++--- resources/lib/kodimonitor.py | 1 - resources/lib/playback_starter.py | 4 ++-- resources/lib/playbackutils.py | 5 ++--- resources/lib/player.py | 24 ++++++++++++------------ resources/lib/playlist_func.py | 4 ++-- resources/lib/playqueue.py | 4 ++-- resources/lib/playutils.py | 4 ++-- resources/lib/plexdb_functions.py | 4 ++-- resources/lib/userclient.py | 10 +++++----- resources/lib/utils.py | 4 ++-- resources/lib/videonodes.py | 4 ++-- resources/lib/websocket_client.py | 4 ++-- 23 files changed, 61 insertions(+), 65 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index da08ea42..3dc6887d 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -30,7 +30,7 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt (and others...) """ -import logging +from logging import getLogger from time import time import urllib2 import socket @@ -57,7 +57,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX." + __name__) +log = getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 88e472b4..237515af 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -import logging +from logging import getLogger from threading import Thread -import Queue +from Queue import Queue, Empty from socket import SHUT_RDWR from urllib import urlencode @@ -19,7 +19,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### @@ -39,7 +39,7 @@ class PlexCompanion(Thread): log.debug("Registration string is:\n%s" % self.client.getClientDetails()) # kodi player instance - self.player = player.Player() + self.player = player.PKC_Player() Thread.__init__(self) @@ -199,7 +199,7 @@ class PlexCompanion(Thread): subscriptionManager = subscribers.SubscriptionManager( requestMgr, self.player, self.mgr) - queue = Queue.Queue(maxsize=100) + queue = Queue(maxsize=100) self.queue = queue if settings('plexCompanion') == 'true': @@ -276,7 +276,7 @@ class PlexCompanion(Thread): # See if there's anything we need to process try: task = queue.get(block=False) - except Queue.Empty: + except Empty: pass else: # Got instructions, process them diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index b6306c70..f2dffc7b 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -2,11 +2,11 @@ ############################################################################### from logging import getLogger -import requests +from Queue import Queue, Empty from shutil import rmtree from urllib import quote_plus, unquote from threading import Thread -from Queue import Queue, Empty +import requests import json_rpc as js from xbmc import sleep, translatePath diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 4a1da15d..694b0e02 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from utils import window, settings import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index b818b851..c3aa0598 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- - ############################################################################### - import logging import xbmc diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 7178a09d..d7078fe0 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- ############################################################################### - -import logging -import requests +from logging import getLogger import xml.etree.ElementTree as etree +import requests from utils import settings, window, language as lang, dialog import clientinfo as client @@ -17,7 +16,7 @@ import state import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index c96b9935..a4913489 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from shutil import copyfile from os import walk, makedirs from os.path import basename, join @@ -22,7 +22,7 @@ import json_rpc as js import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) try: HANDLE = int(argv[1]) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 730517e9..d1eb38f7 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################### +from logging import getLogger -import logging import xbmc import xbmcgui @@ -18,7 +18,7 @@ from migration import check_migration ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 4347be0f..d48612ce 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### - -import logging +from logging import getLogger from urllib import urlencode from ntpath import dirname from datetime import datetime @@ -20,7 +19,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 9cb0d679..7667fc89 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -2,10 +2,13 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ +from logging import getLogger from json import loads, dumps from utils import millis_to_kodi_time from xbmc import executeJSONRPC +log = getLogger("PLEX."+__name__) + class jsonrpc(object): """ diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 5f289dac..6d31faa0 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### - -import logging +from logging import getLogger from ntpath import dirname import artwork @@ -11,7 +10,7 @@ import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index a9933924..6bc0639a 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### - from logging import getLogger from json import loads diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 5abba986..c097c4ca 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from threading import Thread from urlparse import parse_qsl @@ -21,7 +21,7 @@ from context_entry import ContextMenu import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index b302a4a2..ea8e9e27 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### - -import logging +from logging import getLogger from urllib import urlencode from threading import Thread @@ -26,7 +25,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/player.py b/resources/lib/player.py index 4bb51a83..85d971d2 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging -import json +from logging import getLogger +from json import loads -import xbmc +from xbmc import Player, sleep from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode import downloadutils @@ -16,12 +16,12 @@ import state ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### -class Player(xbmc.Player): +class PKC_Player(Player): played_info = state.PLAYED_INFO playStats = state.PLAYER_STATES @@ -29,7 +29,7 @@ class Player(xbmc.Player): def __init__(self): self.doUtils = downloadutils.DownloadUtils - xbmc.Player.__init__(self) + Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): @@ -42,12 +42,12 @@ class Player(xbmc.Player): # Get current file (in utf-8!) try: currentFile = tryDecode(self.getPlayingFile()) - xbmc.sleep(300) + sleep(300) except: currentFile = "" count = 0 while not currentFile: - xbmc.sleep(100) + sleep(100) try: currentFile = tryDecode(self.getPlayingFile()) except: @@ -67,7 +67,7 @@ class Player(xbmc.Player): itemId = window("plex_%s.itemid" % tryEncode(currentFile)) count = 0 while not itemId: - xbmc.sleep(200) + sleep(200) itemId = window("plex_%s.itemid" % tryEncode(currentFile)) if count == 5: LOG.warn("Could not find itemId, cancelling playback report!") @@ -142,17 +142,17 @@ class Player(xbmc.Player): postdata['AudioStreamIndex'] = indexAudio + 1 # Postdata for the subtitles - if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0: + if subsEnabled and len(Player().getAvailableSubtitleStreams()) > 0: # Number of audiotracks to help get plex Index - audioTracks = len(xbmc.Player().getAvailableAudioStreams()) + audioTracks = len(Player().getAvailableAudioStreams()) mapping = window("%s.indexMapping" % plexitem) if mapping: # Set in playbackutils.py LOG.debug("Mapping for external subtitles index: %s" % mapping) - externalIndex = json.loads(mapping) + externalIndex = loads(mapping) if externalIndex.get(str(indexSubs)): # If the current subtitle is in the mapping diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index cb4a1883..e13a6a07 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -1,7 +1,7 @@ """ Collection of functions associated with Kodi and Plex playlists and playqueues """ -import logging +from logging import getLogger from urllib import quote from urlparse import parse_qsl, urlsplit from re import compile as re_compile @@ -15,7 +15,7 @@ import json_rpc as js ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = getLogger("PLEX." + __name__) REGEX = re_compile(r'''metadata%2F(\d+)''') ############################################################################### diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 4203349d..cf6a5250 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -1,7 +1,7 @@ """ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly """ -import logging +from logging import getLogger from threading import RLock, Thread from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO @@ -15,7 +15,7 @@ import json_rpc as js import variables as v ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = getLogger("PLEX." + __name__) # lock used for playqueue manipulations LOCK = RLock() diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 3e268957..97ce2322 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from downloadutils import DownloadUtils from utils import window, settings, tryEncode, language as lang, dialog @@ -10,7 +10,7 @@ import PlexAPI ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index 1fdbe07d..a08e0d40 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- ############################################################################### +from logging import getLogger from utils import kodiSQL -import logging import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index f9671263..17104d63 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging -import threading +from logging import getLogger +from threading import Thread import xbmc import xbmcgui @@ -19,13 +19,13 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### @thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) -class UserClient(threading.Thread): +class UserClient(Thread): # Borg - multiple instances, shared state __shared_state = {} @@ -49,7 +49,7 @@ class UserClient(threading.Thread): self.addon = xbmcaddon.Addon() self.doUtils = downloadutils.DownloadUtils() - threading.Thread.__init__(self) + Thread.__init__(self) def getUsername(self): """ diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 9ccc80b9..fb24f0bb 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from cProfile import Profile from pstats import Stats from sqlite3 import connect, OperationalError @@ -28,7 +28,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 9f526175..659894e5 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger from shutil import copytree import xml.etree.ElementTree as etree from os import makedirs @@ -14,7 +14,7 @@ import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### # Paths are strings, NOT unicode! diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 5a4466d9..aeb3385c 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger import websocket from json import loads import xml.etree.ElementTree as etree @@ -17,7 +17,7 @@ import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### From 39014fe7f4d02547a713168b153ad05b2253bda3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 15:41:07 +0100 Subject: [PATCH 125/509] Move kodi webserver details to state.py --- resources/lib/artwork.py | 31 ++++++------------------------- resources/lib/state.py | 6 ++++++ service.py | 26 +++++++++++++++++++------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index f2dffc7b..300a6577 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -7,13 +7,13 @@ from shutil import rmtree from urllib import quote_plus, unquote from threading import Thread import requests -import json_rpc as js from xbmc import sleep, translatePath from xbmcvfs import exists from utils import window, settings, language as lang, kodiSQL, tryEncode, \ thread_methods, dialog, exists_dir, tryDecode +import state # Disable annoying requests warnings import requests.packages.urllib3 @@ -27,26 +27,6 @@ LOG = getLogger("PLEX." + __name__) ARTWORK_QUEUE = Queue() -def setKodiWebServerDetails(): - """ - Get the Kodi webserver details - used to set the texture cache - """ - xbmc_port = None - xbmc_username = None - xbmc_password = None - if js.get_setting('services.webserver') in (None, False): - # Enable the webserver, it is disabled - xbmc_port = 8080 - xbmc_username = "kodi" - js.set_setting('services.webserverport', xbmc_port) - js.set_setting('services.webserver', True) - # Webserver already enabled - xbmc_port = js.get_setting('services.webserverport') - xbmc_username = js.get_setting('services.webserverusername') - xbmc_password = js.get_setting('services.webserverpassword') - return (xbmc_port, xbmc_username, xbmc_password) - - def double_urlencode(text): return quote_plus(quote_plus(text)) @@ -59,8 +39,6 @@ def double_urldecode(text): 'DB_SCAN', 'STOP_SYNC']) class Image_Cache_Thread(Thread): - xbmc_host = 'localhost' - xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() sleep_between = 50 # Potentially issues with limited number of threads # Hence let Kodi wait till download is successful @@ -94,8 +72,11 @@ class Image_Cache_Thread(Thread): try: requests.head( url="http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, url), - auth=(self.xbmc_username, self.xbmc_password), + % (state.WEBSERVER_HOST, + state.WEBSERVER_PORT, + url), + auth=(state.WEBSERVER_USERNAME, + state.WEBSERVER_PASSWORD), timeout=self.timeout) except requests.Timeout: # We don't need the result, only trigger Kodi to start the diff --git a/resources/lib/state.py b/resources/lib/state.py index 26ff403c..2020fc71 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -78,3 +78,9 @@ PLEX_TRANSIENT_TOKEN = None # Kodi player states PLAYER_STATES = {} PLAYED_INFO = {} + +# Kodi webserver details +WEBSERVER_PORT = 8080 +WEBSERVER_USERNAME = 'kodi' +WEBSERVER_PASSWORD = '' +WEBSERVER_HOST = 'localhost' diff --git a/service.py b/service.py index 676b0b19..6a3e4c84 100644 --- a/service.py +++ b/service.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- - ############################################################################### - -import logging +from logging import getLogger from os import path as os_path from sys import path as sys_path, argv @@ -45,18 +43,32 @@ from PlexCompanion import PlexCompanion from command_pipeline import Monitor_Window from playback_starter import Playback_Starter from artwork import Image_Cache_Thread +from json_rpc import get_setting, set_setting import variables as v import state ############################################################################### - import loghandler loghandler.config() -log = logging.getLogger("PLEX.service") - +log = getLogger("PLEX.service") ############################################################################### +def set_webserver(): + """ + Set the Kodi webserver details - used to set the texture cache + """ + if get_setting('services.webserver') in (None, False): + # Enable the webserver, it is disabled + set_setting('services.webserver', True) + # Set standard port and username + set_setting('services.webserverport', 8080) + set_setting('services.webserverusername', 'kodi') + # Webserver already enabled + state.WEBSERVER_PORT = get_setting('services.webserverport') + state.WEBSERVER_USERNAME = get_setting('services.webserverusername') + state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword') + class Service(): @@ -80,7 +92,7 @@ class Service(): image_cache_thread_running = False def __init__(self): - + set_webserver() self.monitor = Monitor() window('plex_kodiProfile', From 5223f7620cf730840d99c45170500d21e85785f3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 16:01:03 +0100 Subject: [PATCH 126/509] Don't force webserver port & username --- service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service.py b/service.py index 6a3e4c84..ad9f70bb 100644 --- a/service.py +++ b/service.py @@ -62,8 +62,8 @@ def set_webserver(): # Enable the webserver, it is disabled set_setting('services.webserver', True) # Set standard port and username - set_setting('services.webserverport', 8080) - set_setting('services.webserverusername', 'kodi') + # set_setting('services.webserverport', 8080) + # set_setting('services.webserverusername', 'kodi') # Webserver already enabled state.WEBSERVER_PORT = get_setting('services.webserverport') state.WEBSERVER_USERNAME = get_setting('services.webserverusername') From 90c76aa9971087a4eb6cde1d9a5adab2eb2bb873 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 16:18:46 +0100 Subject: [PATCH 127/509] Init unique machine identifier earlier --- resources/lib/clientinfo.py | 29 +++++++++++--------- resources/lib/variables.py | 3 +++ service.py | 53 ++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 694b0e02..05294313 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -59,24 +59,27 @@ def getDeviceId(reset=False): If id does not exist, create one and save in Kodi settings file. """ if reset is True: + v.PKC_MACHINE_IDENTIFIER = None window('plex_client_Id', clear=True) settings('plex_client_Id', value="") - clientId = window('plex_client_Id') - if clientId: - return clientId + client_id = v.PKC_MACHINE_IDENTIFIER + if client_id: + return client_id - clientId = settings('plex_client_Id') + client_id = settings('plex_client_Id') # Because Kodi appears to cache file settings!! - if clientId != "" and reset is False: - window('plex_client_Id', value=clientId) - log.info("Unique device Id plex_client_Id loaded: %s" % clientId) - return clientId + if client_id != "" and reset is False: + v.PKC_MACHINE_IDENTIFIER = client_id + window('plex_client_Id', value=client_id) + log.info("Unique device Id plex_client_Id loaded: %s", client_id) + return client_id log.info("Generating a new deviceid.") from uuid import uuid4 - clientId = str(uuid4()) - settings('plex_client_Id', value=clientId) - window('plex_client_Id', value=clientId) - log.info("Unique device Id plex_client_Id loaded: %s" % clientId) - return clientId + client_id = str(uuid4()) + settings('plex_client_Id', value=client_id) + v.PKC_MACHINE_IDENTIFIER = client_id + window('plex_client_Id', value=client_id) + log.info("Unique device Id plex_client_Id generated: %s", client_id) + return client_id diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 525bb29c..46254428 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -63,6 +63,9 @@ DEVICENAME = DEVICENAME.replace('(', "") DEVICENAME = DEVICENAME.replace(')', "") DEVICENAME = DEVICENAME.strip() +# Unique ID for this Plex client; also see clientinfo.py +PKC_MACHINE_IDENTIFIER = None + # Database paths _DB_VIDEO_VERSION = { 13: 78, # Gotham diff --git a/service.py b/service.py index ad9f70bb..06dd1ab9 100644 --- a/service.py +++ b/service.py @@ -37,6 +37,7 @@ import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket import downloadutils from playqueue import Playqueue +import clientinfo import PlexAPI from PlexCompanion import PlexCompanion @@ -51,7 +52,7 @@ import state import loghandler loghandler.config() -log = getLogger("PLEX.service") +LOG = getLogger("PLEX.service") ############################################################################### def set_webserver(): @@ -92,24 +93,15 @@ class Service(): image_cache_thread_running = False def __init__(self): - set_webserver() - self.monitor = Monitor() - - window('plex_kodiProfile', - value=tryDecode(translatePath("special://profile"))) - window('fetch_pms_item_number', - value=settings('fetch_pms_item_number')) - # Initial logging - log.info("======== START %s ========" % v.ADDON_NAME) - log.info("Platform: %s" % v.PLATFORM) - log.info("KODI Version: %s" % v.KODILONGVERSION) - log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION)) - log.info("Using plugin paths: %s" - % (settings('useDirectPaths') != "true")) - log.info("Number of sync threads: %s" - % settings('syncThreadNumber')) - log.info("Full sys.argv received: %s" % argv) + LOG.info("======== START %s ========", v.ADDON_NAME) + LOG.info("Platform: %s", v.PLATFORM) + LOG.info("KODI Version: %s", v.KODILONGVERSION) + LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) + LOG.info("Using plugin paths: %s", + settings('useDirectPaths') != "true") + LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) + LOG.info("Full sys.argv received: %s", argv) # Reset window props for profile switch properties = [ @@ -130,8 +122,15 @@ class Service(): # Clear video nodes properties videonodes.VideoNodes().clearProperties() - # Set the minimum database version + # Init some stuff window('plex_minDBVersion', value="1.5.10") + set_webserver() + self.monitor = Monitor() + window('plex_kodiProfile', + value=tryDecode(translatePath("special://profile"))) + window('fetch_pms_item_number', + value=settings('fetch_pms_item_number')) + clientinfo.getDeviceId() def __stop_PKC(self): """ @@ -172,7 +171,7 @@ class Service(): if window('plex_kodiProfile') != kodiProfile: # Profile change happened, terminate this thread and others - log.info("Kodi profile was: %s and changed to: %s. " + LOG.info("Kodi profile was: %s and changed to: %s. " "Terminating old PlexKodiConnect thread." % (kodiProfile, window('plex_kodiProfile'))) @@ -235,7 +234,7 @@ class Service(): # Alert user is not authenticated and suppress future # warning self.warn_auth = False - log.warn("Not authenticated yet.") + LOG.warn("Not authenticated yet.") # User access is restricted. # Keep verifying until access is granted @@ -267,7 +266,7 @@ class Service(): window('plex_online', value="false") # Suspend threads state.SUSPEND_LIBRARY_THREAD = True - log.error("Plex Media Server went offline") + LOG.error("Plex Media Server went offline") if settings('show_pms_offline') == 'true': dialog('notification', lang(33001), @@ -301,7 +300,7 @@ class Service(): icon='{plex}', time=5000, sound=False) - log.info("Server %s is online and ready." % server) + LOG.info("Server %s is online and ready." % server) window('plex_online', value="true") if state.AUTHENTICATED: # Server got offline when we were authenticated. @@ -331,7 +330,7 @@ class Service(): except: pass window('plex_service_started', clear=True) - log.info("======== STOP %s ========" % v.ADDON_NAME) + LOG.info("======== STOP %s ========" % v.ADDON_NAME) # Safety net - Kody starts PKC twice upon first installation! @@ -344,11 +343,11 @@ else: # Delay option delay = int(settings('startupDelay')) -log.info("Delaying Plex startup by: %s sec..." % delay) +LOG.info("Delaying Plex startup by: %s sec..." % delay) if exit: - log.error('PKC service.py already started - exiting this instance') + LOG.error('PKC service.py already started - exiting this instance') elif delay and Monitor().waitForAbort(delay): # Start the service - log.info("Abort requested while waiting. PKC not started.") + LOG.info("Abort requested while waiting. PKC not started.") else: Service().ServiceEntryPoint() From 41abcc8d2cac669b0331accf270c6f774334eec7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 16:30:52 +0100 Subject: [PATCH 128/509] Remove plexbmc plexsettings.py --- resources/lib/PlexCompanion.py | 11 ++-- resources/lib/plexbmchelper/listener.py | 20 +++---- resources/lib/plexbmchelper/plexgdm.py | 17 +++--- resources/lib/plexbmchelper/plexsettings.py | 62 --------------------- resources/lib/variables.py | 2 + 5 files changed, 22 insertions(+), 90 deletions(-) delete mode 100644 resources/lib/plexbmchelper/plexsettings.py diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 237515af..fa4ef4d7 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -8,8 +8,7 @@ from urllib import urlencode from xbmc import sleep, executebuiltin from utils import settings, thread_methods -from plexbmchelper import listener, plexgdm, subscribers, httppersist, \ - plexsettings +from plexbmchelper import listener, plexgdm, subscribers, httppersist from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API from playlist_func import get_pms_playqueue, get_plextype_from_xml @@ -32,10 +31,9 @@ class PlexCompanion(Thread): log.info("----===## Starting PlexCompanion ##===----") if callback is not None: self.mgr = callback - self.settings = plexsettings.getSettings() # Start GDM for server/client discovery self.client = plexgdm.plexgdm() - self.client.clientDetails(self.settings) + self.client.clientDetails() log.debug("Registration string is:\n%s" % self.client.getClientDetails()) # kodi player instance @@ -210,9 +208,8 @@ class PlexCompanion(Thread): httpd = listener.ThreadedHTTPServer( client, subscriptionManager, - self.settings, queue, - ('', self.settings['myport']), + ('', v.COMPANION_PORT), listener.MyHandler) httpd.timeout = 0.95 break @@ -261,7 +258,7 @@ class PlexCompanion(Thread): else: log.debug("Client is no longer registered. " "Plex Companion still running on port %s" - % self.settings['myport']) + % v.COMPANION_PORT) client.register_as_client() # Get and set servers if message_count % 30 == 0: diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index e93f07de..55103a35 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -46,8 +46,7 @@ class MyHandler(BaseHTTPRequestHandler): def do_OPTIONS(self): self.send_response(200) self.send_header('Content-Length', '0') - self.send_header('X-Plex-Client-Identifier', - self.server.settings['uuid']) + self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER) self.send_header('Content-Type', 'text/plain') self.send_header('Connection', 'close') self.send_header('Access-Control-Max-Age', '1209600') @@ -82,7 +81,6 @@ class MyHandler(BaseHTTPRequestHandler): def answer_request(self, sendData): self.serverlist = self.server.client.getServerList() subMgr = self.server.subscriptionManager - settings = self.server.settings try: request_path = self.path[1:] @@ -101,7 +99,7 @@ class MyHandler(BaseHTTPRequestHandler): if request_path == "version": self.response( "PlexKodiConnect Plex Companion: Running\nVersion: %s" - % settings['version']) + % v.ADDON_VERSION) elif request_path == "verify": self.response("XBMC JSON connection test:\n" + js.ping()) @@ -121,10 +119,10 @@ class MyHandler(BaseHTTPRequestHandler): '/>' '' % (v.XML_HEADER, - settings['client_name'], - settings['uuid'], - settings['platform'], - settings['plexbmc_version'])) + v.DEVICENAME, + v.PKC_MACHINE_IDENTIFIER, + v.PLATFORM, + v.ADDON_VERSION)) log.debug("crafted resources response: %s" % resp) self.response(resp, getXArgsDeviceInfo(include_token=False)) elif "/subscribe" in request_path: @@ -149,7 +147,7 @@ class MyHandler(BaseHTTPRequestHandler): str(commandID), subMgr.msg(js.get_players())), { - 'X-Plex-Client-Identifier': settings['uuid'], + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', 'Access-Control-Allow-Origin': '*', @@ -175,8 +173,7 @@ class MyHandler(BaseHTTPRequestHandler): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True - def __init__(self, client, subscriptionManager, settings, - queue, *args, **kwargs): + def __init__(self, client, subscriptionManager, queue, *args, **kwargs): """ client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to- date serverlist without instantiating anything @@ -185,6 +182,5 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ self.client = client self.subscriptionManager = subscriptionManager - self.settings = settings self.queue = queue HTTPServer.__init__(self, *args, **kwargs) diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 488dbf54..4c88998c 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -30,6 +30,7 @@ from xbmc import sleep import downloadutils from utils import window, settings, dialog, language +import variables as v ############################################################################### @@ -44,7 +45,6 @@ class plexgdm: self.discover_message = 'M-SEARCH * HTTP/1.0' self.client_header = '* HTTP/1.0' self.client_data = None - self.client_id = None self._multicast_address = '239.0.0.250' self.discover_group = (self._multicast_address, 32414) @@ -60,7 +60,7 @@ class plexgdm: self.client_registered = False self.download = downloadutils.DownloadUtils().downloadUrl - def clientDetails(self, options): + def clientDetails(self): self.client_data = ( "Content-Type: plex/media-player\n" "Resource-Identifier: %s\n" @@ -74,13 +74,12 @@ class plexgdm: "playqueues\n" "Device-Class: HTPC\n" ) % ( - options['uuid'], - options['client_name'], - options['myport'], - options['addonName'], - options['version'] + v.PKC_MACHINE_IDENTIFIER, + v.DEVICENAME, + v.COMPANION_PORT, + v.ADDON_NAME, + v.ADDON_VERSION ) - self.client_id = options['uuid'] def getClientDetails(self): return self.client_data @@ -211,7 +210,7 @@ class plexgdm: registered = False for client in xml: if (client.attrib.get('machineIdentifier') == - self.client_id): + v.PKC_MACHINE_IDENTIFIER): registered = True if registered: return True diff --git a/resources/lib/plexbmchelper/plexsettings.py b/resources/lib/plexbmchelper/plexsettings.py deleted file mode 100644 index 3e93b01a..00000000 --- a/resources/lib/plexbmchelper/plexsettings.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -from utils import guisettingsXML, settings -import variables as v - -############################################################################### - -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -def getGUI(name): - xml = guisettingsXML() - try: - ans = list(xml.iter(name))[0].text - if ans is None: - ans = '' - except: - ans = '' - return ans - - -def getSettings(): - options = {} - - options['gdm_debug'] = settings('companionGDMDebugging') - options['gdm_debug'] = True if options['gdm_debug'] == 'true' else False - - options['client_name'] = v.DEVICENAME - - # XBMC web server options - options['webserver_enabled'] = (getGUI('webserver') == "true") - log.info('Webserver is set to %s' % options['webserver_enabled']) - webserverport = getGUI('webserverport') - try: - webserverport = int(webserverport) - log.info('Using webserver port %s' % str(webserverport)) - except: - log.info('No setting for webserver port found in guisettings.xml.' - 'Using default fallback port 8080') - webserverport = 8080 - options['port'] = webserverport - - options['user'] = getGUI('webserverusername') - options['passwd'] = getGUI('webserverpassword') - log.info('Webserver username: %s, password: %s' - % (options['user'], options['passwd'])) - - options['addonName'] = v.ADDON_NAME - options['uuid'] = settings('plex_client_Id') - options['platform'] = v.PLATFORM - options['version'] = v.ADDON_VERSION - options['plexbmc_version'] = options['version'] - options['myplex_user'] = settings('username') - try: - options['myport'] = int(settings('companionPort')) - log.info('Using Plex Companion Port %s' % str(options['myport'])) - except: - log.error('Error getting Plex Companion Port from file settings. ' - 'Using fallback port 39005') - options['myport'] = 39005 - return options diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 46254428..9dcc4560 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -63,6 +63,8 @@ DEVICENAME = DEVICENAME.replace('(', "") DEVICENAME = DEVICENAME.replace(')', "") DEVICENAME = DEVICENAME.strip() +COMPANION_PORT = int(_ADDON.getSetting('companionPort')) + # Unique ID for this Plex client; also see clientinfo.py PKC_MACHINE_IDENTIFIER = None From c3b5054477cd33c9b13c261cdf89a03593d9784c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 9 Dec 2017 17:23:50 +0100 Subject: [PATCH 129/509] Fixes to Companion /poll replies --- resources/lib/plexbmchelper/listener.py | 6 ++++-- resources/lib/plexbmchelper/subscribers.py | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 55103a35..fb5c4268 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -148,10 +148,12 @@ class MyHandler(BaseHTTPRequestHandler): subMgr.msg(js.get_players())), { 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'text/xml' + 'Content-Type': 'text/xml;charset=utf-8' }) elif "/unsubscribe" in request_path: self.response(v.COMPANION_OK_MESSAGE, diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index f6e40ef1..ebdb8fe5 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -18,6 +18,14 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### +# What is Companion controllable? +CONTROLLABLE = { + v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', + v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' \ + 'skipPrevious,skipNext,stepBack,stepForward', + v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,audioStream,subtitleStream,' \ + 'seekTo,skipPrevious,skipNext,stepBack,stepForward' +} class SubscriptionManager: def __init__(self, RequestMgr, player, mgr): @@ -56,7 +64,7 @@ class SubscriptionManager: log.debug('players: %s', players) msg = v.XML_HEADER msg += ' Date: Sun, 10 Dec 2017 19:01:22 +0100 Subject: [PATCH 130/509] Major Plex Companion overhaul, part 1 --- resources/lib/PlexFunctions.py | 9 -- resources/lib/json_rpc.py | 27 ++++- resources/lib/kodimonitor.py | 97 +++++++--------- resources/lib/player.py | 1 + resources/lib/plexbmchelper/subscribers.py | 128 +++++++++------------ resources/lib/state.py | 29 ++++- resources/lib/variables.py | 20 ++++ 7 files changed, 165 insertions(+), 146 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 94cfcda5..ce8d9624 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -291,15 +291,6 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie', return xml -def getPlexRepeat(kodiRepeat): - plexRepeat = { - 'off': '0', - 'one': '1', - 'all': '2' # does this work?!? - } - return plexRepeat.get(kodiRepeat) - - def PMSHttpsEnabled(url): """ Returns True if the PMS can talk https, False otherwise. diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 7667fc89..90dc7af5 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -360,6 +360,22 @@ def get_episodes(params): return ret +def get_item(playerid): + """ + Returns the following for the currently playing item: + { + u'title': u'Okja', + u'type': u'movie', + u'id': 258, + u'file': u'smb://...movie.mkv', + u'label': u'Okja' + } + """ + return jsonrpc('Player.GetItem').execute({ + 'playerid': playerid, + 'properties': ['title', 'file']})['result']['item'] + + def get_player_props(playerid): """ Returns a dict for the active Kodi player with the following values: @@ -367,14 +383,14 @@ def get_player_props(playerid): 'type' [str] the Kodi player type, e.g. 'video' 'time' The current item's time in Kodi time 'totaltime' The current item's total length in Kodi time - 'speed' [int] playback speed, defaults to 0 + 'speed' [int] playback speed, 0 is paused, 1 is playing 'shuffled' [bool] True if shuffled 'repeat' [str] 'off', 'one', 'all' 'position' [int] position in playlist (or -1) 'playlistid' [int] the Kodi playlist id (or -1) } """ - ret = jsonrpc('Player.GetProperties').execute({ + return jsonrpc('Player.GetProperties').execute({ 'playerid': playerid, 'properties': ['type', 'time', @@ -383,8 +399,11 @@ def get_player_props(playerid): 'shuffled', 'repeat', 'position', - 'playlistid']}) - return ret['result'] + 'playlistid', + 'currentvideostream', + 'currentaudiostream', + 'subtitleenabled', + 'currentsubtitle']})['result'] def current_audiostream(playerid): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6bc0639a..3cb82380 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -13,7 +13,9 @@ from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ from PlexFunctions import scrobble from kodidb_functions import get_kodiid_from_filename from PlexAPI import API +import json_rpc as js import state +import variables as v ############################################################################### @@ -178,69 +180,48 @@ class KodiMonitor(Monitor): def PlayBackStart(self, data): """ - Called whenever a playback is started + Called whenever a playback is started. Example data: + { + u'item': {u'type': u'movie', u'title': u''}, + u'player': {u'playerid': 1, u'speed': 1} + } """ - # Get currently playing file - can take a while. Will be utf-8! - try: - currentFile = self.xbmcplayer.getPlayingFile() - except: - currentFile = None - count = 0 - while currentFile is None: - sleep(100) - try: - currentFile = self.xbmcplayer.getPlayingFile() - except: - pass - if count == 50: - log.info("No current File, cancel OnPlayBackStart...") - return - else: - count += 1 - # Just to be on the safe side - currentFile = tryDecode(currentFile) - log.debug("Currently playing file is: %s" % currentFile) - + log.debug('PlayBackStart called with: %s', data) # Get the type of media we're playing try: - typus = data['item']['type'] + kodi_type = data['item']['type'] + playerid = data['player']['playerid'] + json_data = js.get_item(playerid) except (TypeError, KeyError): - log.info("Item is invalid for PMS playstate update.") + log.info('Aborting playback report - item is invalid for updates') return - log.debug("Playing itemtype is (or appears to be): %s" % typus) - - # Try to get a Kodi ID - # If PKC was used - native paths, not direct paths - plex_id = window('plex_%s.itemid' % tryEncode(currentFile)) - # Get rid of the '' if the window property was not set - plex_id = None if not plex_id else plex_id - kodiid = None - if plex_id is None: - log.debug('Did not get Plex id from window properties') - try: - kodiid = data['item']['id'] - except (TypeError, KeyError): - log.debug('Did not get a Kodi id from Kodi, darn') - # For direct paths, if we're not streaming something - # When using Widgets, Kodi doesn't tell us shit so we need this hack - if (kodiid is None and plex_id is None and typus != 'song' - and not currentFile.startswith('http')): - (kodiid, typus) = get_kodiid_from_filename(currentFile) - if kodiid is None: - return - - if plex_id is None: - # Get Plex' item id - with plexdb.Get_Plex_DB() as plexcursor: - plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus) - try: - plex_id = plex_dbitem[0] - except TypeError: - log.info("No Plex id returned for kodiid %s. Aborting playback" - " report" % kodiid) - return - log.debug("Found Plex id %s for Kodi id %s for type %s" - % (plex_id, kodiid, typus)) + try: + kodi_id = json_data['id'] + kodi_type = json_data['type'] + except KeyError: + log.info('Aborting playback report - no Kodi id for %s', json_data) + return + # Get Plex' item id + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) + try: + plex_id = plex_dbitem[0] + plex_type = plex_dbitem[2] + except TypeError: + # No plex id, hence item not in the library. E.g. clips + plex_id = None + plex_type = None + state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) + state.PLAYER_STATES[playerid]['file'] = json_data['file'] + state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id + state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type + state.PLAYER_STATES[playerid]['plex_id'] = plex_id + state.PLAYER_STATES[playerid]['plex_type'] = plex_type + log.debug('Set the player state %s', state.PLAYER_STATES[playerid]) + # Set other stuff like volume + state.PLAYER_STATES[playerid]['volume'] = js.get_volume() + state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + return # Switch subtitle tracks if applicable subtitle = window('plex_%s.subtitle' % tryEncode(currentFile)) diff --git a/resources/lib/player.py b/resources/lib/player.py index 85d971d2..e1d8e2ac 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -37,6 +37,7 @@ class PKC_Player(Player): Will be called when xbmc starts playing a file. Window values need to have been set in Kodimonitor.py """ + return self.stopAll() # Get current file (in utf-8!) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index ebdb8fe5..4183428d 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -72,70 +72,62 @@ class SubscriptionManager: msg += self.getTimelineXML(players.get(v.KODI_TYPE_VIDEO), v.PLEX_TYPE_VIDEO) msg += "\n" + log.debug('msg is: %s', msg) return msg def getTimelineXML(self, player, ptype): if player is None: status = 'stopped' - time = 0 else: playerid = player['playerid'] - info = self.getPlayerProperties(playerid) + info = state.PLAYER_STATES[playerid] # save this info off so the server update can use it too - self.playerprops[playerid] = info - status = info['state'] - time = info['time'] + # self.playerprops[playerid] = info + status = ("paused", "playing")[info['speed']] ret = ('\n 30: - break - keyid = window('plex_currently_playing_itemid') - sleep(100) - count += 1 - if keyid: - self.lastkey = "/library/metadata/%s" % keyid - self.ratingkey = keyid - ret += ' key="%s"' % self.lastkey - ret += ' ratingKey="%s"' % self.ratingkey - serv = self.getServerByHost(self.server) - if info.get('playQueueID'): - self.containerKey = "/playQueues/%s" % info.get('playQueueID') - ret += ' playQueueID="%s"' % info.get('playQueueID') - ret += ' playQueueVersion="%s"' % info.get('playQueueVersion') - ret += ' playQueueItemID="%s"' % info.get('playQueueItemID') + if info['plex_id']: + self.lastkey = "/library/metadata/%s" % info['plex_id'] + self.ratingkey = info['plex_id'] + ret += ' key="/library/metadata/%s"' % info['plex_id'] + ret += ' ratingKey="%s"' % info['plex_id'] + # PlayQueue stuff + playqueue = self.playqueue.playqueues[playerid] + pos = info['position'] + try: + ret += ' playQueueItemID="%s"' % playqueue.items[pos].ID or 'null' + self.containerKey = "/playQueues/%s" % playqueue.ID or 'null' + ret += ' playQueueID="%s"' % playqueue.ID or 'null' + ret += ' playQueueVersion="%s"' % playqueue.version or 'null' ret += ' containerKey="%s"' % self.containerKey - ret += ' guid="%s"' % info['guid'] - elif keyid: - self.containerKey = self.lastkey - ret += ' containerKey="%s"' % self.containerKey - - ret += ' duration="%s"' % info['duration'] - ret += ' machineIdentifier="%s"' % serv.get('uuid', "") - ret += ' protocol="%s"' % serv.get('protocol', "http") - ret += ' address="%s"' % serv.get('server', self.server) - ret += ' port="%s"' % serv.get('port', self.port) - ret += ' volume="%s"' % info['volume'] - ret += ' shuffle="%s"' % info['shuffle'] - ret += ' mute="%s"' % info['mute'] - ret += ' repeat="%s"' % info['repeat'] - ret += ' itemType="%s"' % ptype + ret += ' guid="%s"' % playqueue.items[pos].guid or 'null' + except IndexError: + pass + ret += ' machineIdentifier="%s"' % server.get('uuid', "") + ret += ' protocol="%s"' % server.get('protocol', 'http') + ret += ' address="%s"' % server.get('server', self.server) + ret += ' port="%s"' % server.get('port', self.port) + # Temp. token set? if state.PLEX_TRANSIENT_TOKEN: ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN - elif info['plex_transient_token']: - ret += ' token="%s"' % info['plex_transient_token'] + elif playqueue.plex_transient_token: + ret += ' token="%s"' % playqueue.plex_transient_token # Might need an update in the future if ptype == 'video': ret += ' subtitleStreamID="-1"' @@ -236,37 +228,26 @@ class SubscriptionManager: del self.subscribers[sub.uuid] def getPlayerProperties(self, playerid): + # Get the playqueue + playqueue = self.playqueue.playqueues[playerid] + # get info from the player + props = state.PLAYER_STATES[playerid] + info = { + 'time': kodi_time_to_millis(props['time']), + 'duration': kodi_time_to_millis(props['totaltime']), + 'state': ("paused", "playing")[int(props['speed'])], + 'shuffle': ("0", "1")[props.get('shuffled', False)], + 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[props.get('repeat')] + } + pos = props['position'] try: - # Get the playqueue - playqueue = self.playqueue.playqueues[playerid] - # get info from the player - props = js.get_player_props(playerid) - info = { - 'time': kodi_time_to_millis(props['time']), - 'duration': kodi_time_to_millis(props['totaltime']), - 'state': ("paused", "playing")[int(props['speed'])], - 'shuffle': ("0", "1")[props.get('shuffled', False)], - 'repeat': pf.getPlexRepeat(props.get('repeat')), - } - pos = props['position'] - try: - info['playQueueItemID'] = playqueue.items[pos].ID or 'null' - info['guid'] = playqueue.items[pos].guid or 'null' - info['playQueueID'] = playqueue.ID or 'null' - info['playQueueVersion'] = playqueue.version or 'null' - info['itemType'] = playqueue.items[pos].plex_type or 'null' - except: - info['itemType'] = props.get('type') or 'null' + info['playQueueItemID'] = playqueue.items[pos].ID or 'null' + info['guid'] = playqueue.items[pos].guid or 'null' + info['playQueueID'] = playqueue.ID or 'null' + info['playQueueVersion'] = playqueue.version or 'null' + info['itemType'] = playqueue.items[pos].plex_type or 'null' except: - import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) - info = { - 'time': 0, - 'duration': 0, - 'state': 'stopped', - 'shuffle': False, - 'repeat': 0 - } + info['itemType'] = props.get('type') or 'null' # get the volume from the application info['volume'] = js.get_volume() @@ -323,5 +304,6 @@ class Subscriber: response = self.doUtils(url, postBody=msg, action_type="POST") + log.debug('response is: %s', response) if response in [False, None, 401]: self.subMgr.removeSubscriber(self.uuid) diff --git a/resources/lib/state.py b/resources/lib/state.py index 2020fc71..c88a8224 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -75,8 +75,33 @@ PLEX_USER_ID = None # another user playing something! Token identifies user PLEX_TRANSIENT_TOKEN = None -# Kodi player states -PLAYER_STATES = {} +# Kodi player states - here, initial values are set +PLAYER_STATES = { + 1: { + 'type': 'movie', + 'time': 0, + 'totaltime': 0, + 'speed': 0, + 'shuffled': False, + 'repeat': '0', + 'position': -1, + 'playlistid': -1, + 'currentvideostream': -1, + 'currentaudiostream': -1, + 'subtitleenabled': False, + 'currentsubtitle': -1, + ###### + 'file': '', + 'kodi_id': None, + 'kodi_type': None, + 'plex_id': None, + 'plex_type': None, + 'volume': 100, + 'muted': False + }, + 2: {}, + 3: {} +} PLAYED_INFO = {} # Kodi webserver details diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 9dcc4560..d660f22f 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -214,6 +214,20 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { } +KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = { + KODI_TYPE_VIDEO: KODI_TYPE_VIDEO, + KODI_TYPE_MOVIE: KODI_TYPE_VIDEO, + KODI_TYPE_EPISODE: KODI_TYPE_VIDEO, + KODI_TYPE_SEASON: KODI_TYPE_VIDEO, + KODI_TYPE_SHOW: KODI_TYPE_VIDEO, + KODI_TYPE_CLIP: KODI_TYPE_VIDEO, + KODI_TYPE_ARTIST: KODI_TYPE_AUDIO, + KODI_TYPE_ALBUM: KODI_TYPE_AUDIO, + KODI_TYPE_SONG: KODI_TYPE_AUDIO, + KODI_TYPE_AUDIO: KODI_TYPE_AUDIO, + KODI_TYPE_PHOTO: KODI_TYPE_PHOTO +} + REMAP_TYPE_FROM_PLEXTYPE = { PLEX_TYPE_MOVIE: 'movie', PLEX_TYPE_CLIP: 'clip', @@ -371,3 +385,9 @@ SORT_METHODS_ALBUMS = ( XML_HEADER = '\n' COMPANION_OK_MESSAGE = XML_HEADER + '' + +PLEX_REPEAT_FROM_KODI_REPEAT = { + 'off': '0', + 'one': '1', + 'all': '2' # does this work?!? +} From cc347d56544494e4f2a49bf910619745af8b6334 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 11 Dec 2017 19:24:21 +0100 Subject: [PATCH 131/509] Fix some KeyErrors --- resources/lib/state.py | 56 +++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/resources/lib/state.py b/resources/lib/state.py index c88a8224..5d08c32f 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -77,30 +77,40 @@ PLEX_TRANSIENT_TOKEN = None # Kodi player states - here, initial values are set PLAYER_STATES = { - 1: { - 'type': 'movie', - 'time': 0, - 'totaltime': 0, - 'speed': 0, - 'shuffled': False, - 'repeat': '0', - 'position': -1, - 'playlistid': -1, - 'currentvideostream': -1, - 'currentaudiostream': -1, - 'subtitleenabled': False, - 'currentsubtitle': -1, - ###### - 'file': '', - 'kodi_id': None, - 'kodi_type': None, - 'plex_id': None, - 'plex_type': None, - 'volume': 100, - 'muted': False + 1: { + 'type': 'movie', + 'time': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0 }, - 2: {}, - 3: {} + 'totaltime': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0 + }, + 'speed': 0, + 'shuffled': False, + 'repeat': 'off', + 'position': -1, + 'playlistid': -1, + 'currentvideostream': -1, + 'currentaudiostream': -1, + 'subtitleenabled': False, + 'currentsubtitle': -1, + ###### + 'file': '', + 'kodi_id': None, + 'kodi_type': None, + 'plex_id': None, + 'plex_type': None, + 'volume': 100, + 'muted': False + }, + 2: {}, + 3: {} } PLAYED_INFO = {} From 9cac51d5c9446583afc1f76b73a8cb02ff812262 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Dec 2017 20:14:27 +0100 Subject: [PATCH 132/509] Major Plex Companion overhaul, part 2 --- resources/lib/PlexAPI.py | 3 +- resources/lib/json_rpc.py | 1 + resources/lib/kodidb_functions.py | 69 +++-- resources/lib/kodimonitor.py | 81 +++--- resources/lib/plexbmchelper/subscribers.py | 304 ++++++++++----------- resources/lib/state.py | 3 + resources/lib/variables.py | 14 + 7 files changed, 258 insertions(+), 217 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 3dc6887d..e0c36a16 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2690,7 +2690,8 @@ class API(): 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()) + state.PLEX_IDS[tryDecode(playurl)] = self.getRatingKey() + # window('%s.itemid' % plexitem, value=self.getRatingKey()) window('%s.playcount' % plexitem, value=str(userdata['PlayCount'])) if itemtype == v.PLEX_TYPE_EPISODE: diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 90dc7af5..0e0dadcb 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -362,6 +362,7 @@ def get_episodes(params): def get_item(playerid): """ + UNRELIABLE on playback startup! (as other JSON and Python Kodi functions) Returns the following for the currently playing item: { u'title': u'Okja', diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 6d31faa0..653a019a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -808,7 +808,7 @@ class Kodidb_Functions(): ids.append(row[0]) return ids - def getIdFromFilename(self, filename, path): + def video_id_from_filename(self, filename, path): """ Returns the tuple (itemId, type) where itemId: Kodi DB unique Id for either movie or episode @@ -884,6 +884,34 @@ class Kodidb_Functions(): return return itemId, typus + def music_id_from_filename(self, filename, path): + """ + Returns the Kodi song_id from the Kodi music database or None if not + found OR something went wrong. + """ + query = ''' + SELECT idPath + FROM path + WHERE strPath = ? + ''' + self.cursor.execute(query, (path,)) + path_id = self.cursor.fetchall() + if len(path_id) != 1: + log.error('Found wrong number of path ids: %s for path %s, abort', + path_id, path) + return + query = ''' + SELECT idSong + FROM song + WHERE strFileName = ? AND idPath = ? + ''' + self.cursor.execute(query, (filename, path_id[0])) + song_id = self.cursor.fetchall() + if len(song_id) != 1: + log.info('Found wrong number of songs %s, abort', song_id) + return + return song_id[0] + def getUnplayedItems(self): """ VIDEOS @@ -1522,24 +1550,29 @@ class Kodidb_Functions(): self.cursor.execute(query, (kodi_id, kodi_type)) -def get_kodiid_from_filename(file): +def kodiid_from_filename(path, kodi_type): """ - Returns the tuple (kodiid, type) if we have a video in the database with - said filename, or (None, None) + Returns kodi_id if we have an item in the Kodi video or audio database with + said path. Feed with the Kodi itemtype, e.v. 'movie', 'song' + Returns None if not possible """ - kodiid = None - typus = None + kodi_id = None try: - filename = file.rsplit('/', 1)[1] - path = file.rsplit('/', 1)[0] + '/' + filename = path.rsplit('/', 1)[1] + path = path.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) + filename = path.rsplit('\\', 1)[1] + path = path.rsplit('\\', 1)[0] + '\\' + if kodi_type == v.KODI_TYPE_SONG: + with GetKodiDB('music') as kodi_db: + try: + kodi_id, _ = kodi_db.music_id_from_filename(filename, path) + except TypeError: + log.info('No Kodi audio db element found for path %s', path) + else: + with GetKodiDB('video') as kodi_db: + try: + kodi_id, _ = kodi_db.video_id_from_filename(filename, path) + except TypeError: + log.info('No kodi video db element found for path %s', path) + return kodi_id diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 3cb82380..d5d2fd00 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -11,7 +11,7 @@ import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ plex_command from PlexFunctions import scrobble -from kodidb_functions import get_kodiid_from_filename +from kodidb_functions import kodiid_from_filename from PlexAPI import API import json_rpc as js import state @@ -185,68 +185,61 @@ class KodiMonitor(Monitor): u'item': {u'type': u'movie', u'title': u''}, u'player': {u'playerid': 1, u'speed': 1} } + Unfortunately VERY random inputs! + E.g. when using Widgets, Kodi doesn't tell us shit """ - log.debug('PlayBackStart called with: %s', data) # Get the type of media we're playing try: kodi_type = data['item']['type'] playerid = data['player']['playerid'] - json_data = js.get_item(playerid) except (TypeError, KeyError): - log.info('Aborting playback report - item is invalid for updates') + log.info('Aborting playback report - item invalid for updates %s', + data) return + json_data = js.get_item(playerid) + path = json_data.get('file') + kodi_id = json_data.get('id') + if not path and not kodi_id: + log.info('Aborting playback report - no Kodi id or file for %s', + json_data) + return + # Plex id will NOT be set with direct paths + plex_id = state.PLEX_IDS.get(path) try: - kodi_id = json_data['id'] - kodi_type = json_data['type'] + plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[kodi_type] except KeyError: - log.info('Aborting playback report - no Kodi id for %s', json_data) - return - # Get Plex' item id - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) - try: - plex_id = plex_dbitem[0] - plex_type = plex_dbitem[2] - except TypeError: - # No plex id, hence item not in the library. E.g. clips - plex_id = None plex_type = None + # No Kodi id returned by Kodi, even if there is one. Ex: Widgets + if plex_id and not kodi_id: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(plex_id) + try: + kodi_id = plex_dbitem[0] + except TypeError: + kodi_id = None + # If using direct paths and starting playback from a widget + if not path.startswith('http'): + if not kodi_id: + kodi_id = kodiid_from_filename(path, kodi_type) + if not plex_id and kodi_id: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) + try: + plex_id = plex_dbitem[0] + plex_type = plex_dbitem[2] + except TypeError: + # No plex id, hence item not in the library. E.g. clips + pass state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) state.PLAYER_STATES[playerid]['file'] = json_data['file'] state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type state.PLAYER_STATES[playerid]['plex_id'] = plex_id state.PLAYER_STATES[playerid]['plex_type'] = plex_type - log.debug('Set the player state %s', state.PLAYER_STATES[playerid]) # Set other stuff like volume state.PLAYER_STATES[playerid]['volume'] = js.get_volume() state.PLAYER_STATES[playerid]['muted'] = js.get_muted() - return - - # Switch subtitle tracks if applicable - subtitle = window('plex_%s.subtitle' % tryEncode(currentFile)) - if window(tryEncode('plex_%s.playmethod' % currentFile)) \ - == 'Transcode' and subtitle: - if window('plex_%s.subtitle' % currentFile) == 'None': - self.xbmcplayer.showSubtitles(False) - else: - self.xbmcplayer.setSubtitleStream(int(subtitle)) - - # Set some stuff if Kodi initiated playback - if ((settings('useDirectPaths') == "1" and not typus == "song") - or - (typus == "song" and settings('enableMusic') == "true")): - if self.StartDirectPath(plex_id, - typus, - tryEncode(currentFile)) is False: - log.error('Could not initiate monitoring; aborting') - return - - # Save currentFile for cleanup later and to be able to access refs - window('plex_lastPlayedFiled', value=currentFile) - window('plex_currently_playing_itemid', value=plex_id) - window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id) - log.info('Finish playback startup') + log.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 4183428d..d45744d3 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -1,56 +1,68 @@ -import logging -import re -import threading - -from xbmc import sleep +""" +Manages getting playstate from Kodi and sending it to the PMS as well as +subscribed Plex Companion clients. +""" +from logging import getLogger +from re import sub +from threading import Thread, RLock import downloadutils -from clientinfo import getXArgsDeviceInfo from utils import window, kodi_time_to_millis -import PlexFunctions as pf import state import variables as v import json_rpc as js ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### # What is Companion controllable? CONTROLLABLE = { v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', - v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' \ + v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' 'skipPrevious,skipNext,stepBack,stepForward', - v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,audioStream,subtitleStream,' \ - 'seekTo,skipPrevious,skipNext,stepBack,stepForward' + v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' + 'subtitleStream,seekTo,skipPrevious,skipNext,stepBack,stepForward' } class SubscriptionManager: + """ + Manages Plex companion subscriptions + """ def __init__(self, RequestMgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} - self.lastkey = "" - self.containerKey = "" - self.ratingkey = "" - self.lastplayers = {} - self.lastinfo = { - 'video': {}, - 'audio': {}, - 'picture': {} - } + self.containerKey = None + self.ratingkey = None self.server = "" self.protocol = "http" self.port = "" - self.playerprops = {} - self.doUtils = downloadutils.DownloadUtils().downloadUrl + # In order to be able to signal a stop at the end + self.last_params = {} + self.lastplayers = {} + + self.doUtils = downloadutils.DownloadUtils self.xbmcplayer = player self.playqueue = mgr.playqueue - self.RequestMgr = RequestMgr + @staticmethod + def _headers(): + """ + Headers are different for Plex Companion! + """ + return { + 'Content-type': 'text/plain', + 'Connection': 'Keep-Alive', + 'Keep-Alive': 'timeout=20', + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', + 'X-Plex-Protocol': "1.0" + } + def getServerByHost(self, host): if len(self.serverlist) == 1: return self.serverlist[0] @@ -61,64 +73,80 @@ class SubscriptionManager: return {} def msg(self, players): - log.debug('players: %s', players) + LOG.debug('players: %s', players) msg = v.XML_HEADER msg += '\n' % (CONTROLLABLE[ptype], ptype, ptype) + playerid = player['playerid'] + info = state.PLAYER_STATES[playerid] + status = 'paused' if info['speed'] == '0' else 'playing' + ret = ' 30: - sub.cleanup() - del self.subscribers[sub.uuid] - - def getPlayerProperties(self, playerid): - # Get the playqueue - playqueue = self.playqueue.playqueues[playerid] - # get info from the player - props = state.PLAYER_STATES[playerid] - info = { - 'time': kodi_time_to_millis(props['time']), - 'duration': kodi_time_to_millis(props['totaltime']), - 'state': ("paused", "playing")[int(props['speed'])], - 'shuffle': ("0", "1")[props.get('shuffled', False)], - 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[props.get('repeat')] - } - pos = props['position'] - try: - info['playQueueItemID'] = playqueue.items[pos].ID or 'null' - info['guid'] = playqueue.items[pos].guid or 'null' - info['playQueueID'] = playqueue.ID or 'null' - info['playQueueVersion'] = playqueue.version or 'null' - info['itemType'] = playqueue.items[pos].plex_type or 'null' - except: - info['itemType'] = props.get('type') or 'null' - - # get the volume from the application - info['volume'] = js.get_volume() - info['mute'] = js.get_muted() - - info['plex_transient_token'] = playqueue.plex_transient_token - - return info + with RLock(): + for subscriber in self.subscribers.values(): + if subscriber.age > 30: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] class Subscriber: @@ -268,16 +267,13 @@ class Subscriber: self.commandID = int(commandID) or 0 self.navlocationsent = False self.age = 0 - self.doUtils = downloadutils.DownloadUtils().downloadUrl + self.doUtils = downloadutils.DownloadUtils self.subMgr = subMgr self.RequestMgr = RequestMgr def __eq__(self, other): return self.uuid == other.uuid - def tostr(self): - return "uuid=%s,commandID=%i" % (self.uuid, self.commandID) - def cleanup(self): self.RequestMgr.closeConnection(self.protocol, self.host, self.port) @@ -289,11 +285,12 @@ class Subscriber: return True else: self.navlocationsent = True - msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg) - log.debug("sending xml to subscriber %s:\n%s" % (self.tostr(), msg)) + msg = sub(r"INSERTCOMMANDID", str(self.commandID), msg) + LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", + self.uuid, self.commandID, msg) url = self.protocol + '://' + self.host + ':' + self.port \ + "/:/timeline" - t = threading.Thread(target=self.threadedSend, args=(url, msg)) + t = Thread(target=self.threadedSend, args=(url, msg)) t.start() def threadedSend(self, url, msg): @@ -301,9 +298,8 @@ class Subscriber: Threaded POST request, because they stall due to PMS response missing the Content-Length header :-( """ - response = self.doUtils(url, - postBody=msg, - action_type="POST") - log.debug('response is: %s', response) + response = self.doUtils().downloadUrl(url, + postBody=msg, + action_type="POST") if response in [False, None, 401]: self.subMgr.removeSubscriber(self.uuid) diff --git a/resources/lib/state.py b/resources/lib/state.py index 5d08c32f..a115f5bc 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -112,6 +112,9 @@ PLAYER_STATES = { 2: {}, 3: {} } +# Dict containing all filenames as keys with plex id as values - used for addon +# paths for playback (since we're not receiving a Kodi id) +PLEX_IDS = {} PLAYED_INFO = {} # Kodi webserver details diff --git a/resources/lib/variables.py b/resources/lib/variables.py index d660f22f..3dace6f4 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -199,6 +199,20 @@ KODITYPE_FROM_PLEXTYPE = { 'XXXXXXX': 'genre' } +PLEX_TYPE_FROM_KODI_TYPE = { + KODI_TYPE_VIDEO: PLEX_TYPE_VIDEO, + KODI_TYPE_MOVIE: PLEX_TYPE_MOVIE, + KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE, + KODI_TYPE_SEASON: PLEX_TYPE_SEASON, + KODI_TYPE_SHOW: PLEX_TYPE_SHOW, + KODI_TYPE_CLIP: PLEX_TYPE_CLIP, + KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST, + KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM, + KODI_TYPE_SONG: PLEX_TYPE_SONG, + KODI_TYPE_AUDIO: PLEX_TYPE_AUDIO, + KODI_TYPE_PHOTO: PLEX_TYPE_PHOTO +} + KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = { PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO, PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO, From 80c106d57fd22b8b6806d924af6a041208fae470 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 13 Dec 2017 20:41:29 +0100 Subject: [PATCH 133/509] Fix some IndexErrors and KeyErrors --- resources/lib/playbackutils.py | 3 ++- resources/lib/plexbmchelper/subscribers.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index ea8e9e27..6ff63e09 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -17,7 +17,7 @@ from PlexFunctions import init_plex_playqueue from PKC_listitem import PKC_ListItem as ListItem, convert_PKC_to_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 + add_listitem_to_playlist, remove_from_kodi_playlist, playlist_item_from_xml from pickler import Playback_Successful from plexdb_functions import Get_Plex_DB import variables as v @@ -140,6 +140,7 @@ class PlaybackUtils(): get_playlist_details_from_xml(playqueue, xml=xml) except KeyError: return + playqueue.items.append(playlist_item_from_xml(playqueue, xml[0])) if (not homeScreen and not seektime and sizePlaylist < 2 and window('plex_customplaylist') != "true" and diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index d45744d3..c0e20418 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -90,7 +90,6 @@ class SubscriptionManager: def _get_container_key(self, playerid): key = None playlistid = state.PLAYER_STATES[playerid]['playlistid'] - LOG.debug('type: %s, playlistid: %s', type(playlistid), playlistid) if playlistid != -1: # -1 is Kodi's answer if there is no playlist try: @@ -198,7 +197,8 @@ class SubscriptionManager: def _get_pms_params(self, playerid): info = state.PLAYER_STATES[playerid] status = 'paused' if info['speed'] == '0' else 'playing' - params = {'state': status, + params = { + 'state': status, 'ratingKey': self.ratingkey, 'key': '/library/metadata/%s' % self.ratingkey, 'time': kodi_time_to_millis(info['time']), @@ -208,8 +208,9 @@ class SubscriptionManager: params['containerKey'] = self.containerKey if self.containerKey is not None and \ self.containerKey.startswith('/playQueues/'): - params['playQueueVersion'] = info['playQueueVersion'] - params['playQueueItemID'] = info['playQueueItemID'] + playqueue = self.playqueue.playqueues[playerid] + params['playQueueVersion'] = playqueue.version + params['playQueueItemID'] = playqueue.id self.last_params = params return params From c0e7c78a119902d25630a67beb2a065c62d293eb Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 14 Dec 2017 08:29:38 +0100 Subject: [PATCH 134/509] Major Plex Companion overhaul, part 3 --- resources/lib/PlexCompanion.py | 115 +++++++--------- resources/lib/plexbmchelper/listener.py | 75 ++++++----- resources/lib/plexbmchelper/subscribers.py | 144 ++++++++++++--------- 3 files changed, 168 insertions(+), 166 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index fa4ef4d7..7d012ba0 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -1,4 +1,6 @@ -# -*- coding: utf-8 -*- +""" +The Plex Companion master python file +""" from logging import getLogger from threading import Thread from Queue import Queue, Empty @@ -18,7 +20,7 @@ import state ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -26,39 +28,24 @@ log = getLogger("PLEX."+__name__) @thread_methods(add_suspends=['PMS_STATUS']) class PlexCompanion(Thread): """ + Plex Companion monitoring class. Invoke only once """ def __init__(self, callback=None): - log.info("----===## Starting PlexCompanion ##===----") + LOG.info("----===## Starting PlexCompanion ##===----") if callback is not None: self.mgr = callback # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails() - log.debug("Registration string is:\n%s" - % self.client.getClientDetails()) + LOG.debug("Registration string is:\n%s", + self.client.getClientDetails()) # kodi player instance self.player = player.PKC_Player() - + self.httpd = False + self.queue = None Thread.__init__(self) - def _getStartItem(self, string): - """ - Grabs the Plex id from e.g. '/library/metadata/12987' - - and returns the tuple (typus, id) where typus is either 'queueId' or - 'plexId' and id is the corresponding id as a string - """ - typus = 'plexId' - if string.startswith('/library/metadata'): - try: - string = string.split('/')[3] - except IndexError: - string = '' - else: - log.error('Unknown string! %s' % string) - return typus, string - - def processTasks(self, task): + def _process_tasks(self, task): """ Processes tasks picked up e.g. by Companion listener, e.g. {'action': 'playlist', @@ -73,7 +60,7 @@ class PlexCompanion(Thread): 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd', 'type': 'video'}} """ - log.debug('Processing: %s' % task) + LOG.debug('Processing: %s', task) data = task['data'] # Get the token of the user flinging media (might be different one) @@ -84,11 +71,11 @@ class PlexCompanion(Thread): try: xml[0].attrib except (AttributeError, IndexError, TypeError): - log.error('Could not download Plex metadata') + LOG.error('Could not download Plex metadata') return api = API(xml[0]) if api.getType() == v.PLEX_TYPE_ALBUM: - log.debug('Plex music album detected') + LOG.debug('Plex music album detected') queue = self.mgr.playqueue.init_playqueue_from_plex_children( api.getRatingKey()) queue.plex_transient_token = token @@ -120,11 +107,11 @@ class PlexCompanion(Thread): elif task['action'] == 'playlist': # Get the playqueue ID try: - typus, ID, query = ParseContainerKey(data['containerKey']) - except Exception as e: - log.error('Exception while processing: %s' % e) + _, plex_id, query = ParseContainerKey(data['containerKey']) + except: + LOG.error('Exception while processing') import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + LOG.error("Traceback:\n%s", traceback.format_exc()) return try: playqueue = self.mgr.playqueue.get_playqueue_from_type( @@ -136,14 +123,14 @@ class PlexCompanion(Thread): try: xml[0].attrib except (AttributeError, IndexError, TypeError): - log.error('Could not download Plex metadata') + LOG.error('Could not download Plex metadata') return api = API(xml[0]) playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) self.mgr.playqueue.update_playqueue_from_PMS( playqueue, - ID, + plex_id, repeat=query.get('repeat'), offset=data.get('offset')) playqueue.plex_transient_token = token @@ -154,7 +141,7 @@ class PlexCompanion(Thread): if xml is None: return if len(xml) == 0: - log.debug('Empty playqueue received - clearing playqueue') + LOG.debug('Empty playqueue received - clearing playqueue') plex_type = get_plextype_from_xml(xml) if plex_type is None: return @@ -169,9 +156,11 @@ class PlexCompanion(Thread): data['playQueueID']) def run(self): - # Ensure that sockets will be closed no matter what + """ + Ensure that sockets will be closed no matter what + """ try: - self.__run() + self._run() finally: try: self.httpd.socket.shutdown(SHUT_RDWR) @@ -182,10 +171,9 @@ class PlexCompanion(Thread): self.httpd.socket.close() except AttributeError: pass - log.info("----===## Plex Companion stopped ##===----") + LOG.info("----===## Plex Companion stopped ##===----") - def __run(self): - self.httpd = False + def _run(self): httpd = self.httpd # Cache for quicker while loops client = self.client @@ -193,10 +181,9 @@ class PlexCompanion(Thread): thread_suspended = self.thread_suspended # Start up instances - requestMgr = httppersist.RequestMgr() - subscriptionManager = subscribers.SubscriptionManager( - requestMgr, self.player, self.mgr) - + request_mgr = httppersist.RequestMgr() + subscription_manager = subscribers.SubscriptionMgr( + request_mgr, self.player, self.mgr) queue = Queue(maxsize=100) self.queue = queue @@ -207,33 +194,28 @@ class PlexCompanion(Thread): try: httpd = listener.ThreadedHTTPServer( client, - subscriptionManager, + subscription_manager, queue, ('', v.COMPANION_PORT), listener.MyHandler) httpd.timeout = 0.95 break except: - log.error("Unable to start PlexCompanion. Traceback:") + LOG.error("Unable to start PlexCompanion. Traceback:") import traceback - log.error(traceback.print_exc()) - + LOG.error(traceback.print_exc()) sleep(3000) - if start_count == 3: - log.error("Error: Unable to start web helper.") + LOG.error("Error: Unable to start web helper.") httpd = False break - start_count += 1 else: - log.info('User deactivated Plex Companion') - + LOG.info('User deactivated Plex Companion') client.start_all() - message_count = 0 if httpd: - t = Thread(target=httpd.handle_request) + thread = Thread(target=httpd.handle_request) while not thread_stopped(): # If we are not authorized, sleep @@ -246,30 +228,30 @@ class PlexCompanion(Thread): try: message_count += 1 if httpd: - if not t.isAlive(): + if not thread.isAlive(): # Use threads cause the method will stall - t = Thread(target=httpd.handle_request) - t.start() + thread = Thread(target=httpd.handle_request) + thread.start() if message_count == 3000: message_count = 0 if client.check_client_registration(): - log.debug("Client is still registered") + LOG.debug('Client is still registered') else: - log.debug("Client is no longer registered. " - "Plex Companion still running on port %s" - % v.COMPANION_PORT) + LOG.debug('Client is no longer registered. Plex ' + 'Companion still running on port %s', + v.COMPANION_PORT) client.register_as_client() # Get and set servers if message_count % 30 == 0: - subscriptionManager.serverlist = client.getServerList() - subscriptionManager.notify() + subscription_manager.serverlist = client.getServerList() + subscription_manager.notify() if not httpd: message_count = 0 except: - log.warn("Error in loop, continuing anyway. Traceback:") + LOG.warn("Error in loop, continuing anyway. Traceback:") import traceback - log.warn(traceback.format_exc()) + LOG.warn(traceback.format_exc()) # See if there's anything we need to process try: task = queue.get(block=False) @@ -277,10 +259,9 @@ class PlexCompanion(Thread): pass else: # Got instructions, process them - self.processTasks(task) + self._process_tasks(task) queue.task_done() # Don't sleep continue sleep(50) - client.stop_all() diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index fb5c4268..5a062b93 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -1,5 +1,7 @@ -# -*- coding: utf-8 -*- -import logging +""" +Plex Companion listener +""" +from logging import getLogger from re import sub from SocketServer import ThreadingMixIn from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler @@ -7,40 +9,33 @@ from urlparse import urlparse, parse_qs from xbmc import sleep from companion import process_command -from utils import window import json_rpc as js from clientinfo import getXArgsDeviceInfo import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### class MyHandler(BaseHTTPRequestHandler): + """ + BaseHTTPRequestHandler implementation of Plex Companion listener + """ protocol_version = 'HTTP/1.1' def __init__(self, *args, **kwargs): BaseHTTPRequestHandler.__init__(self, *args, **kwargs) self.serverlist = [] - def getServerByHost(self, host): - if len(self.serverlist) == 1: - return self.serverlist[0] - for server in self.serverlist: - if (server.get('serverName') in host or - server.get('server') in host): - return server - return {} - def do_HEAD(self): - log.debug("Serving HEAD request...") + LOG.debug("Serving HEAD request...") self.answer_request(0) def do_GET(self): - log.debug("Serving GET request...") + LOG.debug("Serving GET request...") self.answer_request(1) def do_OPTIONS(self): @@ -65,7 +60,8 @@ class MyHandler(BaseHTTPRequestHandler): def sendOK(self): self.send_response(200) - def response(self, body, headers={}, code=200): + def response(self, body, headers=None, code=200): + headers = {} if headers is None else headers try: self.send_response(code) for key in headers: @@ -78,9 +74,9 @@ class MyHandler(BaseHTTPRequestHandler): except: pass - def answer_request(self, sendData): + def answer_request(self, send_data): self.serverlist = self.server.client.getServerList() - subMgr = self.server.subscriptionManager + sub_mgr = self.server.subscription_manager try: request_path = self.path[1:] @@ -90,9 +86,9 @@ class MyHandler(BaseHTTPRequestHandler): params = {} for key in paramarrays: params[key] = paramarrays[key][0] - log.debug("remote request_path: %s" % request_path) - log.debug("params received from remote: %s" % params) - subMgr.updateCommandID(self.headers.get( + LOG.debug("remote request_path: %s", request_path) + LOG.debug("params received from remote: %s", params) + sub_mgr.update_command_id(self.headers.get( 'X-Plex-Client-Identifier', self.client_address[0]), params.get('commandID', False)) @@ -123,7 +119,7 @@ class MyHandler(BaseHTTPRequestHandler): v.PKC_MACHINE_IDENTIFIER, v.PLATFORM, v.ADDON_VERSION)) - log.debug("crafted resources response: %s" % resp) + LOG.debug("crafted resources response: %s", resp) self.response(resp, getXArgsDeviceInfo(include_token=False)) elif "/subscribe" in request_path: self.response(v.COMPANION_OK_MESSAGE, @@ -132,20 +128,20 @@ class MyHandler(BaseHTTPRequestHandler): host = self.client_address[0] port = params.get('port', False) uuid = self.headers.get('X-Plex-Client-Identifier', "") - commandID = params.get('commandID', 0) - subMgr.addSubscriber(protocol, - host, - port, - uuid, - commandID) + command_id = params.get('commandID', 0) + sub_mgr.add_subscriber(protocol, + host, + port, + uuid, + command_id) elif "/poll" in request_path: if params.get('wait', False) == '1': sleep(950) - commandID = params.get('commandID', 0) + command_id = params.get('commandID', 0) self.response( sub(r"INSERTCOMMANDID", - str(commandID), - subMgr.msg(js.get_players())), + str(command_id), + sub_mgr.msg(js.get_players())), { 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'X-Plex-Protocol': '1.0', @@ -160,29 +156,32 @@ class MyHandler(BaseHTTPRequestHandler): getXArgsDeviceInfo(include_token=False)) uuid = self.headers.get('X-Plex-Client-Identifier', False) \ or self.client_address[0] - subMgr.removeSubscriber(uuid) + sub_mgr.remove_subscriber(uuid) else: # Throw it to companion.py process_command(request_path, params, self.server.queue) self.response('', getXArgsDeviceInfo(include_token=False)) - subMgr.notify() + sub_mgr.notify() except: - log.error('Error encountered. Traceback:') + LOG.error('Error encountered. Traceback:') import traceback - log.error(traceback.print_exc()) + LOG.error(traceback.print_exc()) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + """ + Using ThreadingMixIn Thread magic + """ daemon_threads = True - def __init__(self, client, subscriptionManager, queue, *args, **kwargs): + def __init__(self, client, subscription_manager, queue, *args, **kwargs): """ client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to- date serverlist without instantiating anything - same for SubscriptionManager + same for SubscriptionMgr """ self.client = client - self.subscriptionManager = subscriptionManager + self.subscription_manager = subscription_manager self.queue = queue HTTPServer.__init__(self, *args, **kwargs) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index c0e20418..2ad78dec 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -6,7 +6,7 @@ from logging import getLogger from re import sub from threading import Thread, RLock -import downloadutils +from downloadutils import DownloadUtils as DU from utils import window, kodi_time_to_millis import state import variables as v @@ -22,20 +22,22 @@ LOG = getLogger("PLEX." + __name__) CONTROLLABLE = { v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' - 'skipPrevious,skipNext,stepBack,stepForward', + 'skipPrevious,skipNext,stepBack,stepForward', v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' - 'subtitleStream,seekTo,skipPrevious,skipNext,stepBack,stepForward' + 'subtitleStream,seekTo,skipPrevious,skipNext,' + 'stepBack,stepForward' } -class SubscriptionManager: + +class SubscriptionMgr(object): """ Manages Plex companion subscriptions """ - def __init__(self, RequestMgr, player, mgr): + def __init__(self, request_mgr, player, mgr): self.serverlist = [] self.subscribers = {} self.info = {} - self.containerKey = None + self.container_key = None self.ratingkey = None self.server = "" self.protocol = "http" @@ -44,10 +46,9 @@ class SubscriptionManager: self.last_params = {} self.lastplayers = {} - self.doUtils = downloadutils.DownloadUtils self.xbmcplayer = player self.playqueue = mgr.playqueue - self.RequestMgr = RequestMgr + self.request_mgr = request_mgr @staticmethod def _headers(): @@ -63,7 +64,7 @@ class SubscriptionManager: 'X-Plex-Protocol': "1.0" } - def getServerByHost(self, host): + def _server_by_host(self, host): if len(self.serverlist) == 1: return self.serverlist[0] for server in self.serverlist: @@ -72,17 +73,17 @@ class SubscriptionManager: return server return {} - def msg(self, players): + def _msg(self, players): LOG.debug('players: %s', players) msg = v.XML_HEADER msg += '\n' % (CONTROLLABLE[ptype], ptype, ptype) @@ -124,7 +125,7 @@ class SubscriptionManager: muted = '1' if info['muted'] is True else '0' ret += ' mute="%s"' % muted pbmc_server = window('pms_server') - server = self.getServerByHost(self.server) + server = self._server_by_host(self.server) if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') self.server = self.server.replace('/', '') @@ -136,16 +137,16 @@ class SubscriptionManager: playqueue = self.playqueue.playqueues[playerid] key = self._get_container_key(playerid) if key is not None and key.startswith('/playQueues'): - self.containerKey = key - ret += ' containerKey="%s"' % self.containerKey + self.container_key = key + ret += ' containerKey="%s"' % self.container_key pos = info['position'] ret += ' playQueueItemID="%s"' % playqueue.items[pos].id or 'null' ret += ' playQueueID="%s"' % playqueue.id or 'null' ret += ' playQueueVersion="%s"' % playqueue.version or 'null' ret += ' guid="%s"' % playqueue.items[pos].guid or 'null' elif key: - self.containerKey = key - ret += ' containerKey="%s"' % self.containerKey + self.container_key = key + ret += ' containerKey="%s"' % self.container_key ret += ' machineIdentifier="%s"' % server.get('uuid', "") ret += ' protocol="%s"' % server.get('protocol', 'http') ret += ' address="%s"' % server.get('server', self.server) @@ -162,26 +163,34 @@ class SubscriptionManager: ret += '/>\n' return ret - def updateCommandID(self, uuid, commandID): - if commandID and self.subscribers.get(uuid, False): - self.subscribers[uuid].commandID = int(commandID) + def update_command_id(self, uuid, command_id): + """ + Updates the Plex Companien client with the machine identifier uuid with + command_id + """ + if command_id and self.subscribers.get(uuid): + self.subscribers[uuid].command_id = int(command_id) - def notify(self, event=False): - self.cleanup() + def notify(self): + """ + Causes PKC to tell the PMS and Plex Companion players to receive a + notification what's being played. + """ + self._cleanup() # Do we need a check to NOT tell about e.g. PVR/TV and Addon playback? players = js.get_players() # fetch the message, subscribers or not, since the server # will need the info anyway - msg = self.msg(players) + msg = self._msg(players) if self.subscribers: with RLock(): for subscriber in self.subscribers.values(): - subscriber.send_update(msg, len(players) == 0) - self.notifyServer(players) + subscriber.send_update(msg, not players) + self._notify_server(players) self.lastplayers = players return True - def notifyServer(self, players): + def _notify_server(self, players): for typus, player in players.iteritems(): self._send_pms_notification( player['playerid'], self._get_pms_params(player['playerid'])) @@ -204,10 +213,10 @@ class SubscriptionManager: 'time': kodi_time_to_millis(info['time']), 'duration': kodi_time_to_millis(info['totaltime']) } - if self.containerKey: - params['containerKey'] = self.containerKey - if self.containerKey is not None and \ - self.containerKey.startswith('/playQueues/'): + if self.container_key: + params['containerKey'] = self.container_key + if self.container_key is not None and \ + self.container_key.startswith('/playQueues/'): playqueue = self.playqueue.playqueues[playerid] params['playQueueVersion'] = playqueue.version params['playQueueItemID'] = playqueue.id @@ -215,7 +224,7 @@ class SubscriptionManager: return params def _send_pms_notification(self, playerid, params): - serv = self.getServerByHost(self.server) + serv = self._server_by_host(self.server) xargs = self._headers() playqueue = self.playqueue.playqueues[playerid] if state.PLEX_TRANSIENT_TOKEN: @@ -225,32 +234,39 @@ class SubscriptionManager: url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) - self.doUtils().downloadUrl( - url, parameters=params, headerOptions=xargs) + DU().downloadUrl(url, parameters=params, headerOptions=xargs) # Save to be able to signal a stop at the end LOG.debug("Sent server notification with parameters: %s to %s", params, url) - def addSubscriber(self, protocol, host, port, uuid, commandID): + def add_subscriber(self, protocol, host, port, uuid, command_id): + """ + Adds a new Plex Companion subscriber to PKC. + """ subscriber = Subscriber(protocol, host, port, uuid, - commandID, + command_id, self, - self.RequestMgr) + self.request_mgr) with RLock(): self.subscribers[subscriber.uuid] = subscriber return subscriber - def removeSubscriber(self, uuid): + def remove_subscriber(self, uuid): + """ + Removes a connected Plex Companion subscriber with machine identifier + uuid from PKC notifications. + (Calls the cleanup() method of the subscriber) + """ with RLock(): for subscriber in self.subscribers.values(): if subscriber.uuid == uuid or subscriber.host == uuid: subscriber.cleanup() del self.subscribers[subscriber.uuid] - def cleanup(self): + def _cleanup(self): with RLock(): for subscriber in self.subscribers.values(): if subscriber.age > 30: @@ -258,27 +274,35 @@ class SubscriptionManager: del self.subscribers[subscriber.uuid] -class Subscriber: - def __init__(self, protocol, host, port, uuid, commandID, - subMgr, RequestMgr): +class Subscriber(object): + """ + Plex Companion subscribing device + """ + def __init__(self, protocol, host, port, uuid, command_id, sub_mgr, + request_mgr): self.protocol = protocol or "http" self.host = host self.port = port or 32400 self.uuid = uuid or host - self.commandID = int(commandID) or 0 + self.command_id = int(command_id) or 0 self.navlocationsent = False self.age = 0 - self.doUtils = downloadutils.DownloadUtils - self.subMgr = subMgr - self.RequestMgr = RequestMgr + self.sub_mgr = sub_mgr + self.request_mgr = request_mgr def __eq__(self, other): return self.uuid == other.uuid def cleanup(self): - self.RequestMgr.closeConnection(self.protocol, self.host, self.port) + """ + Closes the connection to the Plex Companion client + """ + self.request_mgr.closeConnection(self.protocol, self.host, self.port) def send_update(self, msg, is_nav): + """ + Sends msg to the Plex Companion client (via .../:/timeline) + """ self.age += 1 if not is_nav: self.navlocationsent = False @@ -286,21 +310,19 @@ class Subscriber: return True else: self.navlocationsent = True - msg = sub(r"INSERTCOMMANDID", str(self.commandID), msg) + msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg) LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", - self.uuid, self.commandID, msg) + self.uuid, self.command_id, msg) url = self.protocol + '://' + self.host + ':' + self.port \ + "/:/timeline" - t = Thread(target=self.threadedSend, args=(url, msg)) - t.start() + thread = Thread(target=self._threaded_send, args=(url, msg)) + thread.start() - def threadedSend(self, url, msg): + def _threaded_send(self, url, msg): """ Threaded POST request, because they stall due to PMS response missing the Content-Length header :-( """ - response = self.doUtils().downloadUrl(url, - postBody=msg, - action_type="POST") - if response in [False, None, 401]: - self.subMgr.removeSubscriber(self.uuid) + response = DU().downloadUrl(url, postBody=msg, action_type="POST") + if response in (False, None, 401): + self.sub_mgr.remove_subscriber(self.uuid) From 0b54e24947092cb1cee0fcd3483149976231d3bb Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 14 Dec 2017 10:21:30 +0100 Subject: [PATCH 135/509] Never have negative playstates --- resources/lib/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index fb24f0bb..79ca110a 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -211,11 +211,13 @@ def kodi_time_to_millis(time): 'seconds'[int], 'milliseconds': [int] } - to milliseconds [int] + to milliseconds [int]. Will not return negative results but 0! """ - return (time['hours']*3600 + - time['minutes']*60 + - time['seconds'])*1000 + time['milliseconds'] + ret = (time['hours'] * 3600 + + time['minutes'] * 60 + + time['seconds']) * 1000 + time['milliseconds'] + ret = 0 if ret < 0 else ret + return ret def tryEncode(uniString, encoding='utf-8'): From 8189eb6b4c9baed0e30c110694f1def178b39f0a Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 14 Dec 2017 10:22:48 +0100 Subject: [PATCH 136/509] Companion: fix audio stream and subtitle stream --- resources/lib/playlist_func.py | 18 +++++++++ resources/lib/plexbmchelper/subscribers.py | 45 +++++++++++++++++++--- resources/lib/variables.py | 7 ++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index e13a6a07..826b5f1d 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -12,6 +12,7 @@ from utils import tryEncode, escape_html from PlexAPI import API from PlexFunctions import GetPlexMetadata import json_rpc as js +import variables as v ############################################################################### @@ -148,6 +149,23 @@ class Playlist_Item(object): answ += '%s: %s, ' % (key, str(getattr(self, key))) return answ[:-2] + ">" + def plex_stream_index(self, kodi_stream_index, stream_type): + """ + Pass in the kodi_stream_index [int] in order to receive the Plex stream + index. + + stream_type: 'video', 'audio', 'subtitle' + + Returns None if unsuccessful + """ + stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] + count = 0 + for stream in self.xml[0][self.part]: + if stream.attrib['streamType'] == stream_type: + if count == kodi_stream_index: + return stream.attrib['id'] + count += 1 + def playlist_item_from_kodi(kodi_item): """ diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 2ad78dec..b1b96abc 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -28,6 +28,12 @@ CONTROLLABLE = { 'stepBack,stepForward' } +STREAM_DETAILS = { + 'video': 'currentvideostream', + 'audio': 'currentaudiostream', + 'subtitle': 'currentsubtitle' +} + class SubscriptionMgr(object): """ @@ -73,7 +79,11 @@ class SubscriptionMgr(object): return server return {} - def _msg(self, players): + def msg(self, players): + """ + Returns a timeline xml as str + (xml containing video, audio, photo player state) + """ LOG.debug('players: %s', players) msg = v.XML_HEADER msg += ' Date: Thu, 14 Dec 2017 10:34:40 +0100 Subject: [PATCH 137/509] Fix playstate remaining at zero --- resources/lib/json_rpc.py | 13 +++++++++++++ resources/lib/plexbmchelper/subscribers.py | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 0e0dadcb..7edcfca9 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -407,6 +407,19 @@ def get_player_props(playerid): 'currentsubtitle']})['result'] +def current_state(playerid): + """ + Returns a dict for the active Kodi player with the following values: + (values that change from second to second with no monitoring possible) + { + 'time' The current item's time in Kodi time + } + """ + return jsonrpc('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['time']})['result'] + + def current_audiostream(playerid): """ Returns a dict of the active audiostream for playerid [int]: diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b1b96abc..6d286b37 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -115,6 +115,16 @@ class SubscriptionMgr(object): state.PLAYER_STATES[playerid]['plex_id'] return key + def _set_current_details(self, playerid): + """ + Sets the current details for the player with playerid in state.py: + + PLAYER_STATES[playerid]{ + 'time' + } + """ + state.PLAYER_STATES[playerid].update(js.current_state(playerid)) + def _kodi_stream_index(self, playerid, stream_type): """ Returns the current Kodi stream index [int] for the player playerid @@ -131,6 +141,7 @@ class SubscriptionMgr(object): return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] + self._set_current_details(playerid) info = state.PLAYER_STATES[playerid] status = 'paused' if info['speed'] == '0' else 'playing' ret = ' Date: Thu, 14 Dec 2017 15:20:41 +0100 Subject: [PATCH 138/509] Update all Kodi player properties for Companion update --- resources/lib/json_rpc.py | 13 ------------- resources/lib/plexbmchelper/subscribers.py | 12 +----------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 7edcfca9..0e0dadcb 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -407,19 +407,6 @@ def get_player_props(playerid): 'currentsubtitle']})['result'] -def current_state(playerid): - """ - Returns a dict for the active Kodi player with the following values: - (values that change from second to second with no monitoring possible) - { - 'time' The current item's time in Kodi time - } - """ - return jsonrpc('Player.GetProperties').execute({ - 'playerid': playerid, - 'properties': ['time']})['result'] - - def current_audiostream(playerid): """ Returns a dict of the active audiostream for playerid [int]: diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 6d286b37..c773dbb7 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -115,16 +115,6 @@ class SubscriptionMgr(object): state.PLAYER_STATES[playerid]['plex_id'] return key - def _set_current_details(self, playerid): - """ - Sets the current details for the player with playerid in state.py: - - PLAYER_STATES[playerid]{ - 'time' - } - """ - state.PLAYER_STATES[playerid].update(js.current_state(playerid)) - def _kodi_stream_index(self, playerid, stream_type): """ Returns the current Kodi stream index [int] for the player playerid @@ -141,7 +131,7 @@ class SubscriptionMgr(object): return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] - self._set_current_details(playerid) + state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) info = state.PLAYER_STATES[playerid] status = 'paused' if info['speed'] == '0' else 'playing' ret = ' Date: Thu, 14 Dec 2017 15:54:28 +0100 Subject: [PATCH 139/509] Ensure that PKC signals playback stop on shutdown --- resources/lib/PlexCompanion.py | 6 +++++- resources/lib/plexbmchelper/subscribers.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 7d012ba0..25a6a78a 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -157,11 +157,14 @@ class PlexCompanion(Thread): def run(self): """ - Ensure that sockets will be closed no matter what + Ensure that + - STOP sent to PMS + - sockets will be closed no matter what """ try: self._run() finally: + self.subscription_manager.signal_stop() try: self.httpd.socket.shutdown(SHUT_RDWR) except AttributeError: @@ -184,6 +187,7 @@ class PlexCompanion(Thread): request_mgr = httppersist.RequestMgr() subscription_manager = subscribers.SubscriptionMgr( request_mgr, self.player, self.mgr) + self.subscription_manager = subscription_manager queue = Queue(maxsize=100) self.queue = queue diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index c773dbb7..b37ddad6 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -98,6 +98,16 @@ class SubscriptionMgr(object): LOG.debug('msg is: %s', msg) return msg + def signal_stop(self): + """ + Externally called on PKC shutdown to ensure that PKC signals a stop to + the PMS. Otherwise, PKC might be stuck at "currently playing" + """ + LOG.info('Signaling a complete stop to PMS') + for _, player in self.lastplayers.iteritems(): + self.last_params['state'] = 'stopped' + self._send_pms_notification(player['playerid'], self.last_params) + def _get_container_key(self, playerid): key = None playlistid = state.PLAYER_STATES[playerid]['playlistid'] @@ -233,7 +243,7 @@ class SubscriptionMgr(object): except KeyError: pass # Process the players we have left (to signal a stop) - for typus, player in self.lastplayers.iteritems(): + for _, player in self.lastplayers.iteritems(): self.last_params['state'] = 'stopped' self._send_pms_notification(player['playerid'], self.last_params) From bb0ba08329f94376b570e2cf43235ffc0cbf0c1e Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 14 Dec 2017 17:19:09 +0100 Subject: [PATCH 140/509] Also update volume and mute on PMS updates --- resources/lib/plexbmchelper/subscribers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b37ddad6..06eec8c4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -141,7 +141,11 @@ class SubscriptionMgr(object): return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] + # Update our PKC state of how the player actually looks like state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) + state.PLAYER_STATES[playerid]['volume'] = js.get_volume() + state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + # Get the message together to send to Plex info = state.PLAYER_STATES[playerid] status = 'paused' if info['speed'] == '0' else 'playing' ret = ' Date: Thu, 14 Dec 2017 17:39:50 +0100 Subject: [PATCH 141/509] Clean-up --- resources/lib/kodimonitor.py | 69 ++++++++++++++----------- resources/lib/plexbmchelper/listener.py | 28 +++++----- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d5d2fd00..555740f5 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- - -############################################################################### +""" +PKC Kodi Monitoring implementation +""" from logging import getLogger from json import loads from xbmc import Monitor, Player, sleep -from downloadutils import DownloadUtils import plexdb_functions as plexdb -from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \ - plex_command +from utils import window, settings, CatchExceptions, plex_command from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from PlexAPI import API @@ -19,7 +17,7 @@ import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) # settings: window-variable WINDOW_SETTINGS = { @@ -48,22 +46,29 @@ STATE_SETTINGS = { class KodiMonitor(Monitor): - + """ + PKC implementation of the Kodi Monitor class. Invoke only once. + """ def __init__(self, callback): self.mgr = callback - self.doUtils = DownloadUtils().downloadUrl self.xbmcplayer = Player() self.playqueue = self.mgr.playqueue Monitor.__init__(self) - log.info("Kodi monitor started.") + LOG.info("Kodi monitor started.") def onScanStarted(self, library): - log.debug("Kodi library scan %s running." % library) + """ + Will be called when Kodi starts scanning the library + """ + LOG.debug("Kodi library scan %s running." % library) if library == "video": window('plex_kodiScan', value="true") def onScanFinished(self, library): - log.debug("Kodi library scan %s finished." % library) + """ + Will be called when Kodi finished scanning the library + """ + LOG.debug("Kodi library scan %s finished." % library) if library == "video": window('plex_kodiScan', clear=True) @@ -71,17 +76,17 @@ class KodiMonitor(Monitor): """ Monitor the PKC settings for changes made by the user """ - log.debug('PKC settings change detected') + LOG.debug('PKC settings change detected') changed = False # Reset the window variables from the settings variables for settings_value, window_value in WINDOW_SETTINGS.iteritems(): if window(window_value) != settings(settings_value): changed = True - log.debug('PKC window settings changed: %s is now %s' - % (settings_value, settings(settings_value))) + LOG.debug('PKC window settings changed: %s is now %s', + settings_value, settings(settings_value)) window(window_value, value=settings(settings_value)) if settings_value == 'fetch_pms_item_number': - log.info('Requesting playlist/nodes refresh') + LOG.info('Requesting playlist/nodes refresh') plex_command('RUN_LIB_SCAN', 'views') # Reset the state variables in state.py for settings_value, state_name in STATE_SETTINGS.iteritems(): @@ -92,11 +97,11 @@ class KodiMonitor(Monitor): new = False if getattr(state, state_name) != new: changed = True - log.debug('PKC state settings %s changed from %s to %s' - % (settings_value, getattr(state, state_name), new)) + LOG.debug('PKC state settings %s changed from %s to %s', + settings_value, getattr(state, state_name), new) setattr(state, state_name, new) # Special cases, overwrite all internal settings - state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60 + state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) @@ -110,10 +115,12 @@ class KodiMonitor(Monitor): @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): - + """ + Called when a bunch of different stuff happens on the Kodi side + """ if data: data = loads(data, 'utf-8') - log.debug("Method: %s Data: %s" % (method, data)) + LOG.debug("Method: %s Data: %s", method, data) if method == "Player.OnPlay": self.PlayBackStart(data) @@ -132,7 +139,7 @@ class KodiMonitor(Monitor): kodiid = item['id'] item_type = item['type'] except (KeyError, TypeError): - log.info("Item is invalid for playstate update.") + LOG.info("Item is invalid for playstate update.") else: # Send notification to the server. with plexdb.Get_Plex_DB() as plexcur: @@ -140,7 +147,7 @@ class KodiMonitor(Monitor): try: itemid = plex_dbitem[0] except TypeError: - log.error("Could not find itemid in plex 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 @@ -160,7 +167,7 @@ class KodiMonitor(Monitor): elif method == "System.OnSleep": # Connection is going to sleep - log.info("Marking the server as offline. SystemOnSleep activated.") + LOG.info("Marking the server as offline. SystemOnSleep activated.") window('plex_online', value="sleep") elif method == "System.OnWake": @@ -175,12 +182,12 @@ class KodiMonitor(Monitor): plex_command('RUN_LIB_SCAN', 'full') elif method == "System.OnQuit": - log.info('Kodi OnQuit detected - shutting down') + LOG.info('Kodi OnQuit detected - shutting down') state.STOP_PKC = True def PlayBackStart(self, data): """ - Called whenever a playback is started. Example data: + Called whenever playback is started. Example data: { u'item': {u'type': u'movie', u'title': u''}, u'player': {u'playerid': 1, u'speed': 1} @@ -193,14 +200,14 @@ class KodiMonitor(Monitor): kodi_type = data['item']['type'] playerid = data['player']['playerid'] except (TypeError, KeyError): - log.info('Aborting playback report - item invalid for updates %s', + LOG.info('Aborting playback report - item invalid for updates %s', data) return json_data = js.get_item(playerid) path = json_data.get('file') kodi_id = json_data.get('id') if not path and not kodi_id: - log.info('Aborting playback report - no Kodi id or file for %s', + LOG.info('Aborting playback report - no Kodi id or file for %s', json_data) return # Plex id will NOT be set with direct paths @@ -239,7 +246,7 @@ class KodiMonitor(Monitor): # Set other stuff like volume state.PLAYER_STATES[playerid]['volume'] = js.get_volume() state.PLAYER_STATES[playerid]['muted'] = js.get_muted() - log.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) + LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): """ @@ -249,7 +256,7 @@ class KodiMonitor(Monitor): try: xml[0].attrib except: - log.error('Did not receive a valid XML for plex_id %s.' % plex_id) + LOG.error('Did not receive a valid XML for plex_id %s.' % plex_id) return False # Setup stuff, because playback was started by Kodi, not PKC api = API(xml[0]) @@ -259,4 +266,4 @@ class KodiMonitor(Monitor): window('plex_%s.playmethod' % currentFile, value="DirectStream") else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") - log.debug('Window properties set for direct paths!') + LOG.debug('Window properties set for direct paths!') diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 5a062b93..b3735a39 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -99,7 +99,7 @@ class MyHandler(BaseHTTPRequestHandler): elif request_path == "verify": self.response("XBMC JSON connection test:\n" + js.ping()) - elif "resources" == request_path: + elif request_path == 'resources': resp = ('%s' '' ' Date: Fri, 15 Dec 2017 12:24:37 +0100 Subject: [PATCH 142/509] Update binary repository to Github --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36fa9753..721b96bf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.18-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.8.18-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) @@ -77,7 +77,7 @@ Install PKC via the PlexKodiConnect Kodi repository below (we cannot use the off | Stable version | Beta version | |----------------|--------------| -| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) | +| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) | ### Additional Artwork PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys! From 5e7250356db9d8f79ee51213eeb86a7bf2780d7e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 15 Dec 2017 13:00:40 +0100 Subject: [PATCH 143/509] Update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 721b96bf..22a955c3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly cu Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible. +### UPDATE YOUR PKC REPO TO RECEIVE UPDATES! + +Unfortunately, the PKC Kodi repository had to move because it stopped working (thanks https://bintray.com). If you installed PKC before December 15, 2017, you need to [**MANUALLY** update the repo once](https://github.com/croneter/PlexKodiConnect/wiki/Update-PKC-Repository). + + ### Please Help Translating Please help translate PlexKodiConnect into your language: [Transifex.com](https://www.transifex.com/croneter/pkc) From 39d7bfd80f2da841fca3192dee1d81860f4d95cc Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 15 Dec 2017 13:22:12 +0100 Subject: [PATCH 144/509] Clean up json_rpc --- resources/lib/json_rpc.py | 84 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 0e0dadcb..c92fa456 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -2,32 +2,30 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ -from logging import getLogger from json import loads, dumps from utils import millis_to_kodi_time from xbmc import executeJSONRPC -log = getLogger("PLEX."+__name__) - -class jsonrpc(object): +class JsonRPC(object): """ Used for all Kodi JSON RPC calls. """ id_ = 1 - jsonrpc = "2.0" + version = "2.0" def __init__(self, method, **kwargs): """ - Initialize with the Kodi method + Initialize with the Kodi method, e.g. 'Player.GetActivePlayers' """ self.method = method - for arg in kwargs: # id_(int), jsonrpc(str) + self.params = None + for arg in kwargs: self.arg = arg def _query(self): query = { - 'jsonrpc': self.jsonrpc, + 'jsonrpc': self.version, 'id': self.id_, 'method': self.method, } @@ -52,7 +50,7 @@ def get_players(): 'picture': ... } """ - info = jsonrpc("Player.GetActivePlayers").execute()['result'] + info = JsonRPC("Player.GetActivePlayers").execute()['result'] ret = {} for player in info: player['playerid'] = int(player['playerid']) @@ -92,7 +90,7 @@ def get_playlists(): ] """ try: - ret = jsonrpc('Playlist.GetPlaylists').execute()['result'] + ret = JsonRPC('Playlist.GetPlaylists').execute()['result'] except KeyError: ret = [] return ret @@ -102,7 +100,7 @@ def get_volume(): """ Returns the Kodi volume as an int between 0 (min) and 100 (max) """ - return jsonrpc('Application.GetProperties').execute( + return JsonRPC('Application.GetProperties').execute( {"properties": ['volume']})['result']['volume'] @@ -111,14 +109,14 @@ def set_volume(volume): Set's the volume (for Kodi overall, not only a player). Feed with an int """ - return jsonrpc('Application.SetVolume').execute({"volume": volume}) + return JsonRPC('Application.SetVolume').execute({"volume": volume}) def get_muted(): """ Returns True if Kodi is muted, False otherwise """ - return jsonrpc('Application.GetProperties').execute( + return JsonRPC('Application.GetProperties').execute( {"properties": ['muted']})['result']['muted'] @@ -127,7 +125,7 @@ def play(): Toggles all Kodi players to play """ for playerid in get_player_ids(): - jsonrpc("Player.PlayPause").execute({"playerid": playerid, + JsonRPC("Player.PlayPause").execute({"playerid": playerid, "play": True}) @@ -136,7 +134,7 @@ def pause(): Pauses playback for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.PlayPause").execute({"playerid": playerid, + JsonRPC("Player.PlayPause").execute({"playerid": playerid, "play": False}) @@ -145,7 +143,7 @@ def stop(): Stops playback for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.Stop").execute({"playerid": playerid}) + JsonRPC("Player.Stop").execute({"playerid": playerid}) def seek_to(offset): @@ -153,7 +151,7 @@ def seek_to(offset): Seeks all Kodi players to offset [int] """ for playerid in get_player_ids(): - jsonrpc("Player.Seek").execute( + JsonRPC("Player.Seek").execute( {"playerid": playerid, "value": millis_to_kodi_time(offset)}) @@ -163,7 +161,7 @@ def smallforward(): Small step forward for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.Seek").execute({"playerid": playerid, + JsonRPC("Player.Seek").execute({"playerid": playerid, "value": "smallforward"}) @@ -172,7 +170,7 @@ def smallbackward(): Small step backward for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.Seek").execute({"playerid": playerid, + JsonRPC("Player.Seek").execute({"playerid": playerid, "value": "smallbackward"}) @@ -181,7 +179,7 @@ def skipnext(): Skips to the next item to play for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.GoTo").execute({"playerid": playerid, + JsonRPC("Player.GoTo").execute({"playerid": playerid, "to": "next"}) @@ -190,7 +188,7 @@ def skipprevious(): Skips to the previous item to play for all Kodi players """ for playerid in get_player_ids(): - jsonrpc("Player.GoTo").execute({"playerid": playerid, + JsonRPC("Player.GoTo").execute({"playerid": playerid, "to": "previous"}) @@ -198,56 +196,56 @@ def input_up(): """ Tells Kodi the user pushed up """ - return jsonrpc("Input.Up").execute() + return JsonRPC("Input.Up").execute() def input_down(): """ Tells Kodi the user pushed down """ - return jsonrpc("Input.Down").execute() + return JsonRPC("Input.Down").execute() def input_left(): """ Tells Kodi the user pushed left """ - return jsonrpc("Input.Left").execute() + return JsonRPC("Input.Left").execute() def input_right(): """ Tells Kodi the user pushed left """ - return jsonrpc("Input.Right").execute() + return JsonRPC("Input.Right").execute() def input_select(): """ Tells Kodi the user pushed select """ - return jsonrpc("Input.Select").execute() + return JsonRPC("Input.Select").execute() def input_home(): """ Tells Kodi the user pushed home """ - return jsonrpc("Input.Home").execute() + return JsonRPC("Input.Home").execute() def input_back(): """ Tells Kodi the user pushed back """ - return jsonrpc("Input.Back").execute() + return JsonRPC("Input.Back").execute() def input_sendtext(text): """ Tells Kodi the user sent text [unicode] """ - return jsonrpc("Input.SendText").execute({'test': text, 'done': False}) + return JsonRPC("Input.SendText").execute({'test': text, 'done': False}) def playlist_get_items(playlistid, properties): @@ -261,7 +259,7 @@ def playlist_get_items(playlistid, properties): [{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'}] """ - reply = jsonrpc('Playlist.GetItems').execute({ + reply = JsonRPC('Playlist.GetItems').execute({ 'playlistid': playlistid, 'properties': properties }) @@ -282,7 +280,7 @@ def playlist_add(playlistid, item): Returns a dict with the key 'error' if unsuccessful. """ - return jsonrpc('Playlist.Add').execute({'playlistid': playlistid, + return JsonRPC('Playlist.Add').execute({'playlistid': playlistid, 'item': item}) @@ -301,7 +299,7 @@ def playlist_insert(params): {kodi_type: kodi_id} Returns a dict with the key 'error' if something went wrong. """ - return jsonrpc('Playlist.Insert').execute(params) + return JsonRPC('Playlist.Insert').execute(params) def playlist_remove(playlistid, position): @@ -311,7 +309,7 @@ def playlist_remove(playlistid, position): Returns a dict with the key 'error' if something went wrong. """ - return jsonrpc('Playlist.Remove').execute({'playlistid': playlistid, + return JsonRPC('Playlist.Remove').execute({'playlistid': playlistid, 'position': position}) @@ -321,7 +319,7 @@ def get_setting(setting): possible """ try: - ret = jsonrpc('Settings.GetSettingValue').execute( + ret = JsonRPC('Settings.GetSettingValue').execute( {'setting': setting})['result']['value'] except (KeyError, TypeError): ret = None @@ -332,7 +330,7 @@ def set_setting(setting, value): """ Sets the Kodi setting, a [str], to value """ - return jsonrpc('Settings.SetSettingValue').execute( + return JsonRPC('Settings.SetSettingValue').execute( {'setting': setting, 'value': value}) @@ -340,7 +338,7 @@ def get_tv_shows(params): """ Returns a list of tv shows for params (check the Kodi wiki) """ - ret = jsonrpc('VideoLibrary.GetTVShows').execute(params) + ret = JsonRPC('VideoLibrary.GetTVShows').execute(params) try: ret['result']['tvshows'] except (KeyError, TypeError): @@ -352,7 +350,7 @@ def get_episodes(params): """ Returns a list of tv show episodes for params (check the Kodi wiki) """ - ret = jsonrpc('VideoLibrary.GetEpisodes').execute(params) + ret = JsonRPC('VideoLibrary.GetEpisodes').execute(params) try: ret['result']['episodes'] except (KeyError, TypeError): @@ -372,7 +370,7 @@ def get_item(playerid): u'label': u'Okja' } """ - return jsonrpc('Player.GetItem').execute({ + return JsonRPC('Player.GetItem').execute({ 'playerid': playerid, 'properties': ['title', 'file']})['result']['item'] @@ -391,7 +389,7 @@ def get_player_props(playerid): 'playlistid' [int] the Kodi playlist id (or -1) } """ - return jsonrpc('Player.GetProperties').execute({ + return JsonRPC('Player.GetProperties').execute({ 'playerid': playerid, 'properties': ['type', 'time', @@ -420,7 +418,7 @@ def current_audiostream(playerid): } or an empty dict if unsuccessful """ - ret = jsonrpc('Player.GetProperties').execute( + ret = JsonRPC('Player.GetProperties').execute( {'properties': ['currentaudiostream'], 'playerid': playerid}) try: ret = ret['result']['currentaudiostream'] @@ -439,7 +437,7 @@ def current_subtitle(playerid): } or an empty dict if unsuccessful """ - ret = jsonrpc('Player.GetProperties').execute( + ret = JsonRPC('Player.GetProperties').execute( {'properties': ['currentsubtitle'], 'playerid': playerid}) try: ret = ret['result']['currentsubtitle'] @@ -452,7 +450,7 @@ def subtitle_enabled(playerid): """ Returns True if a subtitle is enabled, False otherwise """ - ret = jsonrpc('Player.GetProperties').execute( + ret = JsonRPC('Player.GetProperties').execute( {'properties': ['subtitleenabled'], 'playerid': playerid}) try: ret = ret['result']['subtitleenabled'] @@ -465,4 +463,4 @@ def ping(): """ Pings the JSON RPC interface """ - return jsonrpc('JSONRPC.Ping').execute() + return JsonRPC('JSONRPC.Ping').execute() From f0a3cd8c55d883d8fe68e775c9a63da935d6b02c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 15 Dec 2017 16:08:20 +0100 Subject: [PATCH 145/509] Avoid RuntimeError on exit --- resources/lib/plexbmchelper/subscribers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 06eec8c4..7a823f6b 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -104,9 +104,10 @@ class SubscriptionMgr(object): the PMS. Otherwise, PKC might be stuck at "currently playing" """ LOG.info('Signaling a complete stop to PMS') - for _, player in self.lastplayers.iteritems(): + # To avoid RuntimeError, don't use self.lastplayers + for playerid in (0, 1, 2): self.last_params['state'] = 'stopped' - self._send_pms_notification(player['playerid'], self.last_params) + self._send_pms_notification(playerid, self.last_params) def _get_container_key(self, playerid): key = None From 72de3b67965ea84f47d06be4f5f99ffbab924bf7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 15 Dec 2017 16:11:19 +0100 Subject: [PATCH 146/509] Companion: enable audio and subtitle stream switch --- resources/lib/PlexCompanion.py | 20 ++++++++++++++++++++ resources/lib/companion.py | 5 +++++ resources/lib/json_rpc.py | 12 ++++++++++-- resources/lib/playlist_func.py | 17 +++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 25a6a78a..89fd4b68 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -14,6 +14,7 @@ from plexbmchelper import listener, plexgdm, subscribers, httppersist from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API from playlist_func import get_pms_playqueue, get_plextype_from_xml +import json_rpc as js import player import variables as v import state @@ -155,6 +156,25 @@ class PlexCompanion(Thread): playqueue, data['playQueueID']) + elif task['action'] == 'setStreams': + # Plex Companion client adjusted audio or subtitle stream + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + pos = js.get_position(playqueue.playlistid) + if 'audioStreamID' in data: + index = playqueue.items[pos].kodi_stream_index( + data['audioStreamID'], 'audio') + self.player.setAudioStream(index) + elif 'subtitleStreamID' in data: + if data['subtitleStreamID'] == '0': + self.player.showSubtitles(False) + else: + index = playqueue.items[pos].kodi_stream_index( + data['subtitleStreamID'], 'subtitle') + self.player.setSubtitleStream(index) + else: + LOG.error('Unknown setStreams command: %s', task) + def run(self): """ Ensure that diff --git a/resources/lib/companion.py b/resources/lib/companion.py index feda49a6..08db67c6 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -107,5 +107,10 @@ def process_command(request_path, params, queue=None): js.input_home() elif request_path == "player/navigation/back": js.input_back() + elif request_path == "player/playback/setStreams": + queue.put({ + 'action': 'setStreams', + 'data': params + }) else: LOG.error('Unknown request path: %s', request_path) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index c92fa456..a8fa0f94 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -50,9 +50,8 @@ def get_players(): 'picture': ... } """ - info = JsonRPC("Player.GetActivePlayers").execute()['result'] ret = {} - for player in info: + for player in JsonRPC("Player.GetActivePlayers").execute()['result']: player['playerid'] = int(player['playerid']) ret[player['type']] = player return ret @@ -405,6 +404,15 @@ def get_player_props(playerid): 'currentsubtitle']})['result'] +def get_position(playerid): + """ + Returns the currently playing item's position [int] within the playlist + """ + return JsonRPC('Player.GetProperties').execute({ + 'playerid': playerid, + 'properties': ['position']})['result']['position'] + + def current_audiostream(playerid): """ Returns a dict of the active audiostream for playerid [int]: diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 826b5f1d..f5a39e96 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -166,6 +166,23 @@ class Playlist_Item(object): return stream.attrib['id'] count += 1 + def kodi_stream_index(self, plex_stream_index, stream_type): + """ + Pass in the kodi_stream_index [int] in order to receive the Plex stream + index. + + stream_type: 'video', 'audio', 'subtitle' + + Returns None if unsuccessful + """ + stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] + count = 0 + for stream in self.xml[0][self.part]: + if stream.attrib['streamType'] == stream_type: + if stream.attrib['id'] == plex_stream_index: + return count + count += 1 + def playlist_item_from_kodi(kodi_item): """ From 47779bbbeef9cf0a54882d1669520a08705337b9 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 15 Dec 2017 16:22:03 +0100 Subject: [PATCH 147/509] Modify logging --- resources/lib/plexbmchelper/subscribers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 7a823f6b..2592f689 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -84,7 +84,6 @@ class SubscriptionMgr(object): Returns a timeline xml as str (xml containing video, audio, photo player state) """ - LOG.debug('players: %s', players) msg = v.XML_HEADER msg += ' Date: Thu, 21 Dec 2017 09:28:06 +0100 Subject: [PATCH 148/509] Major Plex Companion overhaul, part 4 --- resources/lib/PlexCompanion.py | 236 +++++++++++---------- resources/lib/companion.py | 2 +- resources/lib/json_rpc.py | 15 +- resources/lib/kodimonitor.py | 109 ++++++++-- resources/lib/playlist_func.py | 77 +++---- resources/lib/playqueue.py | 26 ++- resources/lib/plexbmchelper/httppersist.py | 32 +-- resources/lib/plexbmchelper/subscribers.py | 103 +++++---- resources/lib/plexdb_functions.py | 3 +- resources/lib/utils.py | 2 +- 10 files changed, 365 insertions(+), 240 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 89fd4b68..159ccc2d 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -11,6 +11,7 @@ from xbmc import sleep, executebuiltin from utils import settings, thread_methods from plexbmchelper import listener, plexgdm, subscribers, httppersist +from plexbmchelper.subscribers import LOCKER from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API from playlist_func import get_pms_playqueue, get_plextype_from_xml @@ -44,8 +45,127 @@ class PlexCompanion(Thread): self.player = player.PKC_Player() self.httpd = False self.queue = None + self.subscription_manager = None Thread.__init__(self) + @LOCKER.lockthis + def _process_alexa(self, data): + xml = GetPlexMetadata(data['key']) + try: + xml[0].attrib + except (AttributeError, IndexError, TypeError): + LOG.error('Could not download Plex metadata') + return + api = API(xml[0]) + if api.getType() == v.PLEX_TYPE_ALBUM: + LOG.debug('Plex music album detected') + queue = self.mgr.playqueue.init_playqueue_from_plex_children( + api.getRatingKey()) + queue.plex_transient_token = data.get('token') + else: + state.PLEX_TRANSIENT_TOKEN = data.get('token') + params = { + 'mode': 'plex_node', + 'key': '{server}%s' % data.get('key'), + 'view_offset': data.get('offset'), + 'play_directly': 'true', + 'node': 'false' + } + executebuiltin('RunPlugin(plugin://%s?%s)' + % (v.ADDON_ID, urlencode(params))) + + @staticmethod + def _process_node(data): + """ + E.g. watch later initiated by Companion. Basically navigating Plex + """ + state.PLEX_TRANSIENT_TOKEN = data.get('key') + params = { + 'mode': 'plex_node', + 'key': '{server}%s' % data.get('key'), + 'view_offset': data.get('offset'), + 'play_directly': 'true' + } + executebuiltin('RunPlugin(plugin://%s?%s)' + % (v.ADDON_ID, urlencode(params))) + + @LOCKER.lockthis + def _process_playlist(self, data): + # Get the playqueue ID + try: + _, plex_id, query = ParseContainerKey(data['containerKey']) + except: + LOG.error('Exception while processing') + import traceback + LOG.error("Traceback:\n%s", traceback.format_exc()) + return + try: + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + except KeyError: + # E.g. Plex web does not supply the media type + # Still need to figure out the type (video vs. music vs. pix) + xml = GetPlexMetadata(data['key']) + try: + xml[0].attrib + except (AttributeError, IndexError, TypeError): + LOG.error('Could not download Plex metadata') + return + api = API(xml[0]) + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + plex_id, + repeat=query.get('repeat'), + offset=data.get('offset')) + playqueue.plex_transient_token = data.get('key') + + @LOCKER.lockthis + def _process_streams(self, data): + """ + Plex Companion client adjusted audio or subtitle stream + """ + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) + pos = js.get_position(playqueue.playlistid) + if 'audioStreamID' in data: + index = playqueue.items[pos].kodi_stream_index( + data['audioStreamID'], 'audio') + self.player.setAudioStream(index) + elif 'subtitleStreamID' in data: + if data['subtitleStreamID'] == '0': + self.player.showSubtitles(False) + else: + index = playqueue.items[pos].kodi_stream_index( + data['subtitleStreamID'], 'subtitle') + self.player.setSubtitleStream(index) + else: + LOG.error('Unknown setStreams command: %s', data) + + @LOCKER.lockthis + def _process_refresh(self, data): + """ + example data: {'playQueueID': '8475', 'commandID': '11'} + """ + xml = get_pms_playqueue(data['playQueueID']) + if xml is None: + return + if len(xml) == 0: + LOG.debug('Empty playqueue received - clearing playqueue') + plex_type = get_plextype_from_xml(xml) + if plex_type is None: + return + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) + playqueue.clear() + return + playqueue = self.mgr.playqueue.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + data['playQueueID']) + def _process_tasks(self, task): """ Processes tasks picked up e.g. by Companion listener, e.g. @@ -63,128 +183,25 @@ class PlexCompanion(Thread): """ LOG.debug('Processing: %s', task) data = task['data'] - - # Get the token of the user flinging media (might be different one) - token = data.get('token') if task['action'] == 'alexa': - # e.g. Alexa - xml = GetPlexMetadata(data['key']) - try: - xml[0].attrib - except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') - return - api = API(xml[0]) - if api.getType() == v.PLEX_TYPE_ALBUM: - LOG.debug('Plex music album detected') - queue = self.mgr.playqueue.init_playqueue_from_plex_children( - api.getRatingKey()) - queue.plex_transient_token = token - else: - state.PLEX_TRANSIENT_TOKEN = token - params = { - 'mode': 'plex_node', - 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), - 'play_directly': 'true', - 'node': 'false' - } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) - + self._process_alexa(data) elif (task['action'] == 'playlist' and data.get('address') == 'node.plexapp.com'): - # E.g. watch later initiated by Companion - state.PLEX_TRANSIENT_TOKEN = token - params = { - 'mode': 'plex_node', - 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), - 'play_directly': 'true' - } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) - + self._process_node(data) elif task['action'] == 'playlist': - # Get the playqueue ID - try: - _, plex_id, query = ParseContainerKey(data['containerKey']) - except: - LOG.error('Exception while processing') - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - return - try: - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) - except KeyError: - # E.g. Plex web does not supply the media type - # Still need to figure out the type (video vs. music vs. pix) - xml = GetPlexMetadata(data['key']) - try: - xml[0].attrib - except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') - return - api = API(xml[0]) - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - plex_id, - repeat=query.get('repeat'), - offset=data.get('offset')) - playqueue.plex_transient_token = token - + self._process_playlist(data) elif task['action'] == 'refreshPlayQueue': - # example data: {'playQueueID': '8475', 'commandID': '11'} - xml = get_pms_playqueue(data['playQueueID']) - if xml is None: - return - if len(xml) == 0: - LOG.debug('Empty playqueue received - clearing playqueue') - plex_type = get_plextype_from_xml(xml) - if plex_type is None: - return - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) - playqueue.clear() - return - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - data['playQueueID']) - + self._process_refresh(data) elif task['action'] == 'setStreams': - # Plex Companion client adjusted audio or subtitle stream - playqueue = self.mgr.playqueue.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) - pos = js.get_position(playqueue.playlistid) - if 'audioStreamID' in data: - index = playqueue.items[pos].kodi_stream_index( - data['audioStreamID'], 'audio') - self.player.setAudioStream(index) - elif 'subtitleStreamID' in data: - if data['subtitleStreamID'] == '0': - self.player.showSubtitles(False) - else: - index = playqueue.items[pos].kodi_stream_index( - data['subtitleStreamID'], 'subtitle') - self.player.setSubtitleStream(index) - else: - LOG.error('Unknown setStreams command: %s', task) + self._process_streams(data) def run(self): """ - Ensure that - - STOP sent to PMS - - sockets will be closed no matter what + Ensure that sockets will be closed no matter what """ try: self._run() finally: - self.subscription_manager.signal_stop() try: self.httpd.socket.shutdown(SHUT_RDWR) except AttributeError: @@ -288,4 +305,5 @@ class PlexCompanion(Thread): # Don't sleep continue sleep(50) + self.subscription_manager.signal_stop() client.stop_all() diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 08db67c6..19f78fa9 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -58,7 +58,7 @@ def process_command(request_path, params, queue=None): if params.get('deviceName') == 'Alexa': convert_alexa_to_companion(params) LOG.debug('Received request_path: %s, params: %s', request_path, params) - if "/playMedia" in request_path: + if request_path == 'player/playback/playMedia': # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' queue.put({ diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index a8fa0f94..727ff9c5 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -247,20 +247,23 @@ def input_sendtext(text): return JsonRPC("Input.SendText").execute({'test': text, 'done': False}) -def playlist_get_items(playlistid, properties): +def playlist_get_items(playlistid): """ playlistid: [int] id of the Kodi playlist - properties: [list] of strings for the properties to return - e.g. 'title', 'file' Returns a list of Kodi playlist items as dicts with the keys specified in properties. Or an empty list if unsuccessful. Example: - [{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'}] + [ + { + u'file':u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', + u'title': u'3 Idiots', + u'type': u'movie', # IF possible! Else key missing + u'id': 3, # IF possible! Else key missing + u'label': u'3 Idiots'}] """ reply = JsonRPC('Playlist.GetItems').execute({ 'playlistid': playlistid, - 'properties': properties + 'properties': ['title', 'file'] }) try: reply = reply['result']['items'] diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 555740f5..48974327 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,8 +10,10 @@ import plexdb_functions as plexdb from utils import window, settings, CatchExceptions, plex_command from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename +from plexbmchelper.subscribers import LOCKER from PlexAPI import API import json_rpc as js +import playlist_func as PL import state import variables as v @@ -124,17 +126,20 @@ class KodiMonitor(Monitor): if method == "Player.OnPlay": self.PlayBackStart(data) - elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') pass - + elif method == 'Playlist.OnAdd': + self._playlist_onadd(data) + elif method == 'Playlist.OnRemove': + self._playlist_onremove(data) + elif method == 'Playlist.OnClear': + self._playlist_onclear(data) elif method == "VideoLibrary.OnUpdate": # Manually marking as watched/unwatched playcount = data.get('playcount') item = data.get('item') - try: kodiid = item['id'] item_type = item['type'] @@ -161,30 +166,84 @@ class KodiMonitor(Monitor): scrobble(itemid, 'watched') else: scrobble(itemid, 'unwatched') - elif method == "VideoLibrary.OnRemove": pass - elif method == "System.OnSleep": # Connection is going to sleep LOG.info("Marking the server as offline. SystemOnSleep activated.") window('plex_online', value="sleep") - elif method == "System.OnWake": # Allow network to wake up sleep(10000) window('plex_onWake', value="true") window('plex_online', value="false") - elif method == "GUI.OnScreensaverDeactivated": if settings('dbSyncScreensaver') == "true": sleep(5000) plex_command('RUN_LIB_SCAN', 'full') - elif method == "System.OnQuit": LOG.info('Kodi OnQuit detected - shutting down') state.STOP_PKC = True + @LOCKER.lockthis + def _playlist_onadd(self, data): + """ + Called if an item is added to a Kodi playlist. Example data dict: + { + u'item': { + u'type': u'movie', + u'id': 2}, + u'playlistid': 1, + u'position': 0 + } + Will NOT be called if playback initiated by Kodi widgets + """ + playqueue = self.playqueue.playqueues[data['playlistid']] + # Check whether we even need to update our known playqueue + kodi_playqueue = js.playlist_get_items(data['playlistid']) + if playqueue.old_kodi_pl == kodi_playqueue: + # We already know the latest playqueue (e.g. because Plex + # initiated playback) + return + # Playlist has been updated; need to tell Plex about it + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, kodi_item=data['item']) + else: + PL.add_item_to_PMS_playlist(playqueue, + data['position'], + kodi_item=data['item']) + # Make sure that we won't re-add this item + playqueue.old_kodi_pl = kodi_playqueue + + @LOCKER.lockthis + def _playlist_onremove(self, data): + """ + Called if an item is removed from a Kodi playlist. Example data dict: + { + u'playlistid': 1, + u'position': 0 + } + """ + playqueue = self.playqueue.playqueues[data['playlistid']] + # Check whether we even need to update our known playqueue + kodi_playqueue = js.playlist_get_items(data['playlistid']) + if playqueue.old_kodi_pl == kodi_playqueue: + # We already know the latest playqueue - nothing to do + return + PL.delete_playlist_item_from_PMS(playqueue, data['position']) + playqueue.old_kodi_pl = kodi_playqueue + + @LOCKER.lockthis + def _playlist_onclear(self, data): + """ + Called if a Kodi playlist is cleared. Example data dict: + { + u'playlistid': 1, + } + """ + self.playqueue.playqueues[data['playlistid']].clear() + + @LOCKER.lockthis def PlayBackStart(self, data): """ Called whenever playback is started. Example data: @@ -192,8 +251,7 @@ class KodiMonitor(Monitor): u'item': {u'type': u'movie', u'title': u''}, u'player': {u'playerid': 1, u'speed': 1} } - Unfortunately VERY random inputs! - E.g. when using Widgets, Kodi doesn't tell us shit + Unfortunately when using Widgets, Kodi doesn't tell us shit """ # Get the type of media we're playing try: @@ -237,16 +295,37 @@ class KodiMonitor(Monitor): except TypeError: # No plex id, hence item not in the library. E.g. clips pass - state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) - state.PLAYER_STATES[playerid]['file'] = json_data['file'] + info = js.get_player_props(playerid) + state.PLAYER_STATES[playerid].update(info) + state.PLAYER_STATES[playerid]['file'] = path state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type state.PLAYER_STATES[playerid]['plex_id'] = plex_id state.PLAYER_STATES[playerid]['plex_type'] = plex_type - # Set other stuff like volume - state.PLAYER_STATES[playerid]['volume'] = js.get_volume() - state.PLAYER_STATES[playerid]['muted'] = js.get_muted() LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) + # Check whether we need to init our playqueues (e.g. direct play) + init = False + playqueue = self.playqueue.playqueues[playerid] + try: + playqueue.items[info['position']] + except IndexError: + init = True + if init is False and plex_id is not None: + if plex_id != playqueue.items[ + state.PLAYER_STATES[playerid]['position']].id: + init = True + elif init is False and path != playqueue.items[ + state.PLAYER_STATES[playerid]['position']].file: + init = True + if init is True: + LOG.debug('Need to initialize Plex and PKC playqueue') + if plex_id: + PL.init_Plex_playlist(playqueue, plex_id=plex_id) + else: + PL.init_Plex_playlist(playqueue, + kodi_item={'id': kodi_id, + 'type': kodi_type, + 'file': path}) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index f5a39e96..a77b4796 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -25,22 +25,23 @@ REGEX = re_compile(r'''metadata%2F(\d+)''') # {u'type': u'movie', u'id': 3, 'file': path-to-file} -class Playlist_Object_Baseclase(object): +class PlaylistObjectBaseclase(object): """ Base class """ - playlistid = None - type = None - kodi_pl = None - items = [] - old_kodi_pl = [] - id = None - version = None - selectedItemID = None - selectedItemOffset = None - shuffled = 0 - repeat = 0 - plex_transient_token = None + def __init__(self): + self.playlistid = None + self.type = None + self.kodi_pl = None + 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.plex_transient_token = None def __repr__(self): """ @@ -76,14 +77,14 @@ class Playlist_Object_Baseclase(object): LOG.debug('Playlist cleared: %s', self) -class Playlist_Object(Playlist_Object_Baseclase): +class Playlist_Object(PlaylistObjectBaseclase): """ To be done for synching Plex playlists to Kodi """ kind = 'playList' -class Playqueue_Object(Playlist_Object_Baseclase): +class Playqueue_Object(PlaylistObjectBaseclase): """ PKC object to represent PMS playQueues and Kodi playlist for queueing @@ -114,27 +115,27 @@ class Playlist_Item(object): id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID plex_id = None [str] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' - plex_UUID = None [str] Plex librarySectionUUID + plex_uuid = None [str] Plex librarySectionUUID kodi_id = None Kodi unique kodi id (unique only within type!) kodi_type = None [str] Kodi type: 'movie' file = None [str] Path to the item's file. STRING!! - uri = None [str] Weird Plex uri path involving plex_UUID. STRING! + uri = None [str] Weird Plex uri path involving plex_uuid. STRING! guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below """ - id = None - plex_id = None - plex_type = None - plex_UUID = None - kodi_id = None - kodi_type = None - file = None - uri = None - guid = None - xml = None - - # Yet to be implemented: handling of a movie with several parts - part = 0 + def __init__(self): + self.id = None + self.plex_id = None + self.plex_type = None + self.plex_uuid = None + self.kodi_id = None + self.kodi_type = None + self.file = None + self.uri = None + self.guid = None + self.xml = None + # Yet to be implemented: handling of a movie with several parts + self.part = 0 def __repr__(self): """ @@ -201,7 +202,7 @@ def playlist_item_from_kodi(kodi_item): try: item.plex_id = plex_dbitem[0] item.plex_type = plex_dbitem[2] - item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-) + item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-) except TypeError: pass item.file = kodi_item.get('file') @@ -214,7 +215,7 @@ def playlist_item_from_kodi(kodi_item): 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)) + (item.plex_uuid, item.plex_id)) LOG.debug('Made playlist item from Kodi: %s', item) return item @@ -233,11 +234,11 @@ def playlist_item_from_plex(plex_id): item.plex_type = plex_dbitem[5] item.kodi_id = plex_dbitem[0] item.kodi_type = plex_dbitem[4] - except: + except (TypeError, IndexError): raise KeyError('Could not find plex_id %s in database' % plex_id) - item.plex_UUID = plex_id + item.plex_uuid = plex_id item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (item.plex_UUID, plex_id)) + (item.plex_uuid, plex_id)) LOG.debug('Made playlist item from plex: %s', item) return item @@ -335,6 +336,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Returns True if successful, False otherwise """ LOG.debug('Initializing the playlist %s on the Plex side', playlist) + playlist.clear() try: if plex_id: item = playlist_item_from_plex(plex_id) @@ -349,13 +351,14 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): action_type="POST", parameters=params) get_playlist_details_from_xml(playlist, xml) - item.xml = xml[0] + # Need to get the details for the playlist item + item = playlist_item_from_xml(playlist, xml[0]) except (KeyError, IndexError, TypeError): LOG.error('Could not init Plex playlist with plex_id %s and ' 'kodi_item %s', plex_id, kodi_item) return False playlist.items.append(item) - LOG.debug('Initialized the playlist on the Plex side: %s' % playlist) + LOG.debug('Initialized the playlist on the Plex side: %s', playlist) return True diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index cf6a5250..8afdd3e2 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -206,8 +206,7 @@ class Playqueue(Thread): 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(js.playlist_get_items( - playqueue.id, ["title", "file"])): + for i, item in enumerate(js.playlist_get_items(playqueue.id)): if i == 0: PL.init_Plex_playlist(playqueue, kodi_item=item) else: @@ -217,17 +216,16 @@ class Playqueue(Thread): if thread_stopped(): break sleep(1000) - with LOCK: - for playqueue in self.playqueues: - kodi_playqueue = js.playlist_get_items(playqueue.id, - ["title", "file"]) - 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) - # Still sleep a bit so Kodi does not become - # unresponsive - sleep(10) - continue + # with LOCK: + # for playqueue in self.playqueues: + # kodi_playqueue = js.playlist_get_items(playqueue.id) + # 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) + # # Still sleep a bit so Kodi does not become + # # unresponsive + # sleep(10) + # continue sleep(200) LOG.info("----===## PlayQueue client stopped ##===----") diff --git a/resources/lib/plexbmchelper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py index e765ae9e..2d65bb60 100644 --- a/resources/lib/plexbmchelper/httppersist.py +++ b/resources/lib/plexbmchelper/httppersist.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger import httplib import traceback import string @@ -7,7 +7,7 @@ from socket import error as socket_error ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -17,20 +17,20 @@ class RequestMgr: self.conns = {} def getConnection(self, protocol, host, port): - conn = self.conns.get(protocol+host+str(port), False) + conn = self.conns.get(protocol + host + str(port), False) if not conn: if protocol == "https": conn = httplib.HTTPSConnection(host, port) else: conn = httplib.HTTPConnection(host, port) - self.conns[protocol+host+str(port)] = conn + self.conns[protocol + host + str(port)] = conn return conn def closeConnection(self, protocol, host, port): - conn = self.conns.get(protocol+host+str(port), False) + conn = self.conns.get(protocol + host + str(port), False) if conn: conn.close() - self.conns.pop(protocol+host+str(port), None) + self.conns.pop(protocol + host + str(port), None) def dumpConnections(self): for conn in self.conns.values(): @@ -45,7 +45,7 @@ class RequestMgr: conn.request("POST", path, body, header) data = conn.getresponse() if int(data.status) >= 400: - log.error("HTTP response error: %s" % str(data.status)) + LOG.error("HTTP response error: %s" % str(data.status)) # this should return false, but I'm hacking it since iOS # returns 404 no matter what return data.read() or True @@ -56,14 +56,14 @@ class RequestMgr: if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED): pass else: - log.error("Unable to connect to %s\nReason:" % host) - log.error(traceback.print_exc()) - self.conns.pop(protocol+host+str(port), None) + LOG.error("Unable to connect to %s\nReason:" % host) + LOG.error(traceback.print_exc()) + self.conns.pop(protocol + host + str(port), None) if conn: conn.close() return False except Exception as e: - log.error("Exception encountered: %s" % e) + LOG.error("Exception encountered: %s", e) # Close connection just in case try: conn.close() @@ -76,7 +76,7 @@ class RequestMgr: newpath = path + '?' pairs = [] for key in params: - pairs.append(str(key)+'='+str(params[key])) + pairs.append(str(key) + '=' + str(params[key])) newpath += string.join(pairs, '&') return self.get(host, port, newpath, header, protocol) @@ -87,7 +87,7 @@ class RequestMgr: conn.request("GET", path, headers=header) data = conn.getresponse() if int(data.status) >= 400: - log.error("HTTP response error: %s" % str(data.status)) + LOG.error("HTTP response error: %s", str(data.status)) return False else: return data.read() or True @@ -96,8 +96,8 @@ class RequestMgr: if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED): pass else: - log.error("Unable to connect to %s\nReason:" % host) - log.error(traceback.print_exc()) - self.conns.pop(protocol+host+str(port), None) + LOG.error("Unable to connect to %s\nReason:", host) + LOG.error(traceback.print_exc()) + self.conns.pop(protocol + host + str(port), None) conn.close() return False diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 2592f689..3876d73d 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -4,10 +4,11 @@ subscribed Plex Companion clients. """ from logging import getLogger from re import sub -from threading import Thread, RLock +from threading import Thread, Lock from downloadutils import DownloadUtils as DU -from utils import window, kodi_time_to_millis +from utils import window, kodi_time_to_millis, Lock_Function +from playlist_func import init_Plex_playlist import state import variables as v import json_rpc as js @@ -15,6 +16,9 @@ import json_rpc as js ############################################################################### LOG = getLogger("PLEX." + __name__) +# Need to lock all methods and functions messing with subscribers or state +LOCK = Lock() +LOCKER = Lock_Function(LOCK) ############################################################################### @@ -48,6 +52,7 @@ class SubscriptionMgr(object): self.server = "" self.protocol = "http" self.port = "" + self.isplaying = False # In order to be able to signal a stop at the end self.last_params = {} self.lastplayers = {} @@ -79,6 +84,7 @@ class SubscriptionMgr(object): return server return {} + @LOCKER.lockthis def msg(self, players): """ Returns a timeline xml as str @@ -94,7 +100,7 @@ class SubscriptionMgr(object): msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO), v.PLEX_TYPE_VIDEO) msg += "" - LOG.debug('msg is: %s', msg) + LOG.debug('Our PKC message is: %s', msg) return msg def signal_stop(self): @@ -125,9 +131,9 @@ class SubscriptionMgr(object): state.PLAYER_STATES[playerid]['plex_id'] return key - def _kodi_stream_index(self, playerid, stream_type): + def _plex_stream_index(self, playerid, stream_type): """ - Returns the current Kodi stream index [int] for the player playerid + Returns the current Plex stream index [str] for the player playerid stream_type: 'video', 'audio', 'subtitle' """ @@ -136,18 +142,34 @@ class SubscriptionMgr(object): return playqueue.items[info['position']].plex_stream_index( info[STREAM_DETAILS[stream_type]]['index'], stream_type) + @staticmethod + def _player_info(playerid): + """ + Grabs all player info again for playerid [int]. + Returns the dict state.PLAYER_STATES[playerid] + """ + # Update our PKC state of how the player actually looks like + state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) + state.PLAYER_STATES[playerid]['volume'] = js.get_volume() + state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + return state.PLAYER_STATES[playerid] + def _timeline_xml(self, player, ptype): if player is None: return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) playerid = player['playerid'] - # Update our PKC state of how the player actually looks like - state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) - state.PLAYER_STATES[playerid]['volume'] = js.get_volume() - state.PLAYER_STATES[playerid]['muted'] = js.get_muted() - # Get the message together to send to Plex - info = state.PLAYER_STATES[playerid] - LOG.debug('timeline player state: %s', info) + info = self._player_info(playerid) + playqueue = self.playqueue.playqueues[playerid] + pos = info['position'] + try: + playqueue.items[pos] + except IndexError: + # E.g. for direct path playback for single item + return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) + LOG.debug('INFO: %s', info) + LOG.debug('playqueue: %s', playqueue) status = 'paused' if info['speed'] == '0' else 'playing' ret = ' \n' + @LOCKER.lockthis def update_command_id(self, uuid, command_id): """ Updates the Plex Companien client with the machine identifier uuid with @@ -225,18 +246,22 @@ class SubscriptionMgr(object): Causes PKC to tell the PMS and Plex Companion players to receive a notification what's being played. """ - self._cleanup() + with LOCK: + self._cleanup() # Do we need a check to NOT tell about e.g. PVR/TV and Addon playback? players = js.get_players() - # fetch the message, subscribers or not, since the server - # will need the info anyway + # fetch the message, subscribers or not, since the server will need the + # info anyway + self.isplaying = False msg = self.msg(players) - if self.subscribers: - with RLock(): + with LOCK: + if self.isplaying is True: + # If we don't check here, Plex Companion devices will simply + # drop out of the Plex Companion playback screen for subscriber in self.subscribers.values(): subscriber.send_update(msg, not players) - self._notify_server(players) - self.lastplayers = players + self._notify_server(players) + self.lastplayers = players return True def _notify_server(self, players): @@ -280,14 +305,16 @@ class SubscriptionMgr(object): xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN elif playqueue.plex_transient_token: xargs['X-Plex-Token'] = playqueue.plex_transient_token + elif state.PLEX_TOKEN: + xargs['X-Plex-Token'] = state.PLEX_TOKEN url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) DU().downloadUrl(url, parameters=params, headerOptions=xargs) - # Save to be able to signal a stop at the end LOG.debug("Sent server notification with parameters: %s to %s", params, url) + @LOCKER.lockthis def add_subscriber(self, protocol, host, port, uuid, command_id): """ Adds a new Plex Companion subscriber to PKC. @@ -299,28 +326,26 @@ class SubscriptionMgr(object): command_id, self, self.request_mgr) - with RLock(): - self.subscribers[subscriber.uuid] = subscriber + self.subscribers[subscriber.uuid] = subscriber return subscriber + @LOCKER.lockthis def remove_subscriber(self, uuid): """ Removes a connected Plex Companion subscriber with machine identifier uuid from PKC notifications. (Calls the cleanup() method of the subscriber) """ - with RLock(): - for subscriber in self.subscribers.values(): - if subscriber.uuid == uuid or subscriber.host == uuid: - subscriber.cleanup() - del self.subscribers[subscriber.uuid] + for subscriber in self.subscribers.values(): + if subscriber.uuid == uuid or subscriber.host == uuid: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] def _cleanup(self): - with RLock(): - for subscriber in self.subscribers.values(): - if subscriber.age > 30: - subscriber.cleanup() - del self.subscribers[subscriber.uuid] + for subscriber in self.subscribers.values(): + if subscriber.age > 30: + subscriber.cleanup() + del self.subscribers[subscriber.uuid] class Subscriber(object): diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index a08e0d40..239d25df 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -227,8 +227,7 @@ class Plex_DB_Functions(): ''' try: self.plexcursor.execute(query, (plex_id,)) - item = self.plexcursor.fetchone() - return item + return self.plexcursor.fetchone() except: return None diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 79ca110a..6dfdd941 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1079,7 +1079,7 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): return cls -class Lock_Function: +class Lock_Function(object): """ Decorator for class methods and functions to lock them with lock. From 02f48dd15fc7f3f308039a7dd4c468bdb0f2d6aa Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 21 Dec 2017 09:43:16 +0100 Subject: [PATCH 149/509] Prettify --- resources/lib/kodimonitor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 48974327..b7ca143d 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -311,11 +311,9 @@ class KodiMonitor(Monitor): except IndexError: init = True if init is False and plex_id is not None: - if plex_id != playqueue.items[ - state.PLAYER_STATES[playerid]['position']].id: + if plex_id != playqueue.items[info['position']].id: init = True - elif init is False and path != playqueue.items[ - state.PLAYER_STATES[playerid]['position']].file: + elif init is False and path != playqueue.items[info['position']].file: init = True if init is True: LOG.debug('Need to initialize Plex and PKC playqueue') From 1ca8a464732af98fc6e6f8b81eaa14f9729fe1c3 Mon Sep 17 00:00:00 2001 From: Draic Date: Sat, 23 Dec 2017 16:42:40 +0100 Subject: [PATCH 150/509] Hi10p should only trigger on h264 changed h265 to h264 as this should be the intended codec. --- resources/lib/playutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 97ce2322..26fe8163 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -140,8 +140,8 @@ class PlayUtils(): return False if ((settings('transcodeHi10P') == 'true' and videoCodec['bitDepth'] == '10') and - ('h265' in codec or 'hevc' in codec)): - log.info('Option to transcode 10bit h265 video content enabled.') + ('h264' in codec)): + log.info('Option to transcode 10bit h264 video content enabled.') return True try: bitrate = int(videoCodec['bitrate']) From 6e00838ef0fa4926a1c9e78d8e45445384f8200f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 15:24:36 +0100 Subject: [PATCH 151/509] Prettify --- resources/lib/initialsetup.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index d1eb38f7..2d26f967 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -404,12 +404,9 @@ class InitialSetup(): # Get current Kodi video cache setting cache, _ = advancedsettings_xml(['cache', 'memorysize']) - if cache is None: - # Kodi default cache - cache = '20971520' - else: - cache = str(cache.text) - log.info('Current Kodi video memory cache in bytes: %s' % cache) + # Kodi default cache if no setting is set + cache = str(cache.text) if cache is not None else '20971520' + log.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) # Do we need to migrate stuff? From 244a8e308f80126edb362680b8c7fad76122ca39 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 15:30:56 +0100 Subject: [PATCH 152/509] Disable Kodi msg "Loading media info from files" --- resources/lib/initialsetup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 2d26f967..13641603 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -408,10 +408,12 @@ class InitialSetup(): cache = str(cache.text) if cache is not None else '20971520' log.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) - + # Disable foreground "Loading media information from files" + # (still used by Kodi, even though the Wiki says otherwise) + advancedsettings_xml(['musiclibrary', 'backgroundupdate'], + new_value='true') # Do we need to migrate stuff? check_migration() - # Optionally sign into plex.tv. Will not be called on very first run # as plexToken will be '' settings('plex_status', value=lang(39226)) From 11df634c911922b66bbd45bb252cb48206f14647 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 15:45:48 +0100 Subject: [PATCH 153/509] Fix TypeError --- 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 a77b4796..d3c2f6fb 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -210,7 +210,7 @@ def playlist_item_from_kodi(kodi_item): query = dict(parse_qsl(urlsplit(item.file).query)) item.plex_id = query.get('plex_id') item.plex_type = query.get('itemType') - if item.plex_id is None: + if item.plex_id is None and item.file is not None: item.uri = 'library://whatever/item/%s' % quote(item.file, safe='') else: # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER From 771520cd96bfa294d3a4fc6fccdef19db6c7e035 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 16:56:48 +0100 Subject: [PATCH 154/509] Save transient token earlier to PKC playqueue --- resources/lib/PlexCompanion.py | 6 +++--- resources/lib/playqueue.py | 30 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 159ccc2d..2183d155 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -116,10 +116,10 @@ class PlexCompanion(Thread): v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) self.mgr.playqueue.update_playqueue_from_PMS( playqueue, - plex_id, + playqueue_id=plex_id, repeat=query.get('repeat'), - offset=data.get('offset')) - playqueue.plex_transient_token = data.get('key') + offset=data.get('offset'), + transient_token=data.get('key')) @LOCKER.lockthis def _process_streams(self, data): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 8afdd3e2..c178b254 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -61,7 +61,7 @@ class Playqueue(Thread): # 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) + LOG.debug('Initialized the Kodi play queues: %s', self.playqueues) Thread.__init__(self) def get_playqueue_from_type(self, typus): @@ -87,7 +87,7 @@ class Playqueue(Thread): try: xml[0].attrib except (TypeError, IndexError, AttributeError): - LOG.error('Could not download the PMS xml for %s' % plex_id) + LOG.error('Could not download the PMS xml for %s', plex_id) return playqueue = self.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) @@ -103,7 +103,8 @@ class Playqueue(Thread): playqueue, playqueue_id=None, repeat=None, - offset=None): + offset=None, + transient_token=None): """ Completely updates the Kodi playqueue with the new Plex playqueue. Pass in playqueue_id if we need to fetch a new playqueue @@ -112,17 +113,18 @@ class Playqueue(Thread): offset = time offset in Plextime (milliseconds) """ LOG.info('New playqueue %s received from Plex companion with offset ' - '%s, repeat %s' % (playqueue_id, offset, repeat)) + '%s, repeat %s', playqueue_id, offset, repeat) with LOCK: xml = PL.get_PMS_playlist(playqueue, playqueue_id) playqueue.clear() try: PL.get_playlist_details_from_xml(playqueue, xml) except KeyError: - LOG.error('Could not get playqueue ID %s' % playqueue_id) + LOG.error('Could not get playqueue ID %s', playqueue_id) return PlaybackUtils(xml, playqueue).play_all() playqueue.repeat = 0 if not repeat else int(repeat) + playqueue.token = transient_token window('plex_customplaylist', value="true") if offset not in (None, "0"): window('plex_customplaylist.seektime', @@ -133,8 +135,8 @@ class Playqueue(Thread): else: startpos = 0 # Start playback. Player does not return in time - LOG.debug('Playqueues after Plex Companion update are now: %s' - % self.playqueues) + LOG.debug('Playqueues after Plex Companion update are now: %s', + self.playqueues) thread = Thread(target=Player().play, args=(playqueue.kodi_pl, None, @@ -149,8 +151,8 @@ class Playqueue(Thread): """ old = list(playqueue.items) index = list(range(0, len(old))) - LOG.debug('Comparing new Kodi playqueue %s with our play queue %s' - % (new, old)) + LOG.debug('Comparing new Kodi playqueue %s with our play queue %s', + new, old) if self.thread_stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit @@ -178,14 +180,14 @@ class Playqueue(Thread): del old[j], index[j] break elif identical: - LOG.debug('Detected playqueue item %s moved to position %s' - % (i+j, i)) + LOG.debug('Detected playqueue item %s moved to position %s', + i+j, i) PL.move_playlist_item(playqueue, i + j, i) del old[j], index[j] break else: - LOG.debug('Detected new Kodi element at position %s: %s ' - % (i, new_item)) + 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) @@ -196,7 +198,7 @@ class Playqueue(Thread): for j in range(i, len(index)): index[j] += 1 for i in reversed(index): - LOG.debug('Detected deletion of playqueue element at pos %s' % i) + LOG.debug('Detected deletion of playqueue element at pos %s', i) PL.delete_playlist_item_from_PMS(playqueue, i) LOG.debug('Done comparing playqueues') From e358e9b3a51902c483ec7ea8f99a35d6ee2168f0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 17:32:58 +0100 Subject: [PATCH 155/509] PKC playqueues now log as dicts for pprint --- resources/lib/playlist_func.py | 40 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index d3c2f6fb..7575d839 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -47,18 +47,19 @@ class PlaylistObjectBaseclase(object): """ Print the playlist, e.g. to log """ - answ = "<%s: " % (self.__class__.__name__) + answ = '{\'%s\': {' % (self.__class__.__name__) # For some reason, can't use dir directly - answ += "id: %s, " % self.id - answ += "items: %s, " % self.items + answ += '\'id\': %s, ' % self.id for key in self.__dict__: - if key not in ("id", 'items'): - if type(getattr(self, key)) in (str, unicode): - answ += '%s: %s, ' % (key, tryEncode(getattr(self, key))) - else: - # e.g. int - answ += '%s: %s, ' % (key, str(getattr(self, key))) - return answ[:-2] + ">" + if key in ('id', 'items', 'kodi_pl'): + continue + if isinstance(getattr(self, key), (str, unicode)): + answ += '\'%s\': \'%s\', ' % (key, + tryEncode(getattr(self, key))) + else: + # e.g. int + answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) + return answ + '\'items\': %s}}' % self.items def clear(self): """ @@ -141,14 +142,23 @@ class Playlist_Item(object): """ Print the playlist item, e.g. to log """ - answ = "<%s: " % (self.__class__.__name__) + answ = '{\'%s\': {' % (self.__class__.__name__) + answ += '\'id\': %s, ' % self.id + answ += '\'plex_id\': %s, ' % self.plex_id for key in self.__dict__: - if type(getattr(self, key)) in (str, unicode): - answ += '%s: %s, ' % (key, tryEncode(getattr(self, key))) + if key in ('id', 'plex_id', 'xml'): + continue + if isinstance(getattr(self, key), (str, unicode)): + answ += '\'%s\': \'%s\', ' % (key, + tryEncode(getattr(self, key))) else: # e.g. int - answ += '%s: %s, ' % (key, str(getattr(self, key))) - return answ[:-2] + ">" + answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) + if self.xml is None: + answ += '\'xml\': None}}' + else: + answ += '\'xml\': \'%s\'}}' % self.xml.tag + return answ def plex_stream_index(self, kodi_stream_index, stream_type): """ From f5a6531386eaf1e6b641ba4b7fefb6fb1366c650 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 18:29:51 +0100 Subject: [PATCH 156/509] Fix typos --- resources/lib/companion.py | 2 +- resources/lib/playbackutils.py | 2 +- resources/lib/playlist_func.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 19f78fa9..bb7992f0 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -32,7 +32,7 @@ def skip_to(params): for (player, _) in js.get_players().iteritems(): playqueue = playqueues.get_playqueue_from_type(player) for i, item in enumerate(playqueue.items): - if item.ID == playqueue_item_id or item.plex_id == plex_id: + if item.id == playqueue_item_id or item.plex_id == plex_id: break else: LOG.debug('Item not found to skip to') diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 6ff63e09..0fc5f618 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -304,7 +304,7 @@ class PlaybackUtils(): # Item not in Kodi DB self.add_trailer(item) if successful is True: - self.playqueue.items[self.currentPosition - 1].ID = item.get( + self.playqueue.items[self.currentPosition - 1].id = item.get( '%sItemID' % self.playqueue.kind) def add_trailer(self, item): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 7575d839..f2fe8b34 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -296,7 +296,7 @@ def _get_playListVersion_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 + playlist.id with the XML's playQueueID """ try: playlist.id = xml.attrib['%sID' % playlist.kind] @@ -456,7 +456,7 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url, action_type="PUT") try: item.xml = xml[-1] - item.ID = xml[-1].attrib['%sItemID' % playlist.kind] + item.id = xml[-1].attrib['%sItemID' % playlist.kind] except IndexError: LOG.info('Could not get playlist children. Adding a dummy') except (TypeError, AttributeError, KeyError): From 4b5f7868bbf1e6ff2751619c80dabfce1390f756 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 20:47:23 +0100 Subject: [PATCH 157/509] Fix typo --- resources/lib/kodimonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index b7ca143d..be41eaf0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -311,7 +311,7 @@ class KodiMonitor(Monitor): except IndexError: init = True if init is False and plex_id is not None: - if plex_id != playqueue.items[info['position']].id: + if plex_id != playqueue.items[info['position']].plex_id: init = True elif init is False and path != playqueue.items[info['position']].file: init = True From e4ea7692b219add98b75f81af38c473a236fdaed Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 21:23:50 +0100 Subject: [PATCH 158/509] Add json to skip to certain playqueue position --- resources/lib/json_rpc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 727ff9c5..a2ef0b03 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -191,6 +191,15 @@ def skipprevious(): "to": "previous"}) +def skipto(position): + """ + Skips to the position [int] of the current playlist + """ + for playerid in get_player_ids(): + JsonRPC("Player.GoTo").execute({"playerid": playerid, + "to": position}) + + def input_up(): """ Tells Kodi the user pushed up From 2f90674f513f328e92334b92b856e616422368e5 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 21:31:05 +0100 Subject: [PATCH 159/509] Major Plex Companion overhaul, part 5 --- resources/lib/PlexCompanion.py | 18 +++++++++++------- resources/lib/kodimonitor.py | 4 ++++ resources/lib/playlist_func.py | 27 +++++++++++++++++++++++++++ resources/lib/playqueue.py | 2 +- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 2183d155..40620ece 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -93,7 +93,7 @@ class PlexCompanion(Thread): def _process_playlist(self, data): # Get the playqueue ID try: - _, plex_id, query = ParseContainerKey(data['containerKey']) + _, container_key, query = ParseContainerKey(data['containerKey']) except: LOG.error('Exception while processing') import traceback @@ -114,12 +114,16 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - playqueue_id=plex_id, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('key')) + if playqueue.id == container_key: + # OK, really weird, this happens at least with Plex for Android + LOG.debug('Already know this Plex playQueue, ignoring this command') + else: + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('key')) @LOCKER.lockthis def _process_streams(self, data): diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index be41eaf0..1f7f5619 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -199,6 +199,10 @@ class KodiMonitor(Monitor): Will NOT be called if playback initiated by Kodi widgets """ playqueue = self.playqueue.playqueues[data['playlistid']] + # Did PKC cause this add? Then lets not do anything + if playqueue.is_kodi_onadd() is False: + LOG.debug('PKC added this item to the playqueue - ignoring') + return # Check whether we even need to update our known playqueue kodi_playqueue = js.playlist_get_items(data['playlistid']) if playqueue.old_kodi_pl == kodi_playqueue: diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index f2fe8b34..e5bab020 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -42,6 +42,9 @@ class PlaylistObjectBaseclase(object): self.shuffled = 0 self.repeat = 0 self.plex_transient_token = None + # Needed to not add an item twice (first through PKC, then the kodi + # monitor) + self._onadd_queue = [] def __repr__(self): """ @@ -61,6 +64,25 @@ class PlaylistObjectBaseclase(object): answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) return answ + '\'items\': %s}}' % self.items + def kodi_onadd(self): + """ + Call this before adding an item to the Kodi playqueue + """ + self._onadd_queue.append(None) + + def is_kodi_onadd(self): + """ + Returns False if the last kodimonitor on_add was caused by PKC - so that + we are not adding a playlist item twice. + + Calling this function will remove the item from our "checklist" + """ + try: + self._onadd_queue.pop() + except IndexError: + return True + return False + def clear(self): """ Resets the playlist object to an empty playlist @@ -428,9 +450,11 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)} else: params['item'] = {'file': item.file} + playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: LOG.error('Could not add item to playlist. Kodi reply. %s', reply) + playlist.is_kodi_onadd() return False return True @@ -499,9 +523,11 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % kodi_type: int(kodi_id)} else: params['item'] = {'file': file} + playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: LOG.error('Could not add item to playlist. Kodi reply. %s', reply) + playlist.is_kodi_onadd() return False item = playlist_item_from_kodi( {'id': kodi_id, 'type': kodi_type, 'file': file}) @@ -623,6 +649,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, LOG.debug('Insert listitem at position %s for Kodi only for %s', pos, playlist) # Add the item into Kodi playlist + playlist.kodi_onadd() 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: diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index c178b254..cdb23cf6 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -122,9 +122,9 @@ class Playqueue(Thread): except KeyError: LOG.error('Could not get playqueue ID %s', playqueue_id) return - PlaybackUtils(xml, playqueue).play_all() playqueue.repeat = 0 if not repeat else int(repeat) playqueue.token = transient_token + PlaybackUtils(xml, playqueue).play_all() window('plex_customplaylist', value="true") if offset not in (None, "0"): window('plex_customplaylist.seektime', From bfefef548e7fffe630c5ff3acfbe8aec29840733 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 21:32:12 +0100 Subject: [PATCH 160/509] Fix typo --- resources/lib/PlexCompanion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 40620ece..15a33f77 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -123,7 +123,7 @@ class PlexCompanion(Thread): playqueue_id=container_key, repeat=query.get('repeat'), offset=data.get('offset'), - transient_token=data.get('key')) + transient_token=data.get('token')) @LOCKER.lockthis def _process_streams(self, data): From ba0f22ac1e68fea38117dbbbac242a654b87c0c9 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 28 Dec 2017 21:46:48 +0100 Subject: [PATCH 161/509] Prettify --- resources/lib/companion.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/resources/lib/companion.py b/resources/lib/companion.py index bb7992f0..eccc60c2 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -23,22 +23,27 @@ def skip_to(params): Does not seem to be implemented yet by Plex! """ - playqueue_item_id = params.get('playQueueItemID', 'not available') + playqueue_item_id = params.get('playQueueItemID') _, plex_id = GetPlexKeyNumber(params.get('key')) LOG.debug('Skipping to playQueueItemID %s, plex_id %s', playqueue_item_id, plex_id) found = True playqueues = Playqueue() - for (player, _) in js.get_players().iteritems(): - playqueue = playqueues.get_playqueue_from_type(player) + for player in js.get_players().values(): + playqueue = playqueues.playqueues[player['playerid']] for i, item in enumerate(playqueue.items): - if item.id == playqueue_item_id or item.plex_id == plex_id: + if item.id == playqueue_item_id: + found = True break else: - LOG.debug('Item not found to skip to') - found = False - if found: + for i, item in enumerate(playqueue.items): + if item.plex_id == plex_id: + found = True + break + if found is True: Player().play(playqueue.kodi_pl, None, False, i) + else: + LOG.error('Item not found to skip to') def convert_alexa_to_companion(dictionary): From cf15799df22f1b70d2b8774f4157352f30783726 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 30 Dec 2017 12:57:23 +0100 Subject: [PATCH 162/509] Clear and remove-items from Kodi playqueues once --- resources/lib/kodimonitor.py | 10 +++++++- resources/lib/playlist_func.py | 46 +++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 1f7f5619..c796f74a 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -229,6 +229,10 @@ class KodiMonitor(Monitor): } """ playqueue = self.playqueue.playqueues[data['playlistid']] + # Did PKC cause this add? Then lets not do anything + if playqueue.is_kodi_onremove() is False: + LOG.debug('PKC removed this item already from playqueue - ignoring') + return # Check whether we even need to update our known playqueue kodi_playqueue = js.playlist_get_items(data['playlistid']) if playqueue.old_kodi_pl == kodi_playqueue: @@ -245,7 +249,11 @@ class KodiMonitor(Monitor): u'playlistid': 1, } """ - self.playqueue.playqueues[data['playlistid']].clear() + playqueue = self.playqueue.playqueues[data['playlistid']] + if playqueue.is_kodi_onclear() is False: + LOG.debug('PKC already cleared the playqueue - ignoring') + return + playqueue.clear() @LOCKER.lockthis def PlayBackStart(self, data): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index e5bab020..919a3f5e 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -45,6 +45,8 @@ class PlaylistObjectBaseclase(object): # Needed to not add an item twice (first through PKC, then the kodi # monitor) self._onadd_queue = [] + self._onremove_queue = [] + self._onclear_queue = [] def __repr__(self): """ @@ -83,11 +85,53 @@ class PlaylistObjectBaseclase(object): return True return False + def kodi_onremove(self): + """ + Call this before removing an item from the Kodi playqueue + """ + self._onremove_queue.append(None) + + def is_kodi_onremove(self): + """ + Returns False if the last kodimonitor on_remove was caused by PKC - so + that we are not adding a playlist item twice. + + Calling this function will remove the item from our "checklist" + """ + try: + self._onremove_queue.pop() + except IndexError: + return True + return False + + def kodi_onclear(self): + """ + Call this before clearing the Kodi playqueue IF it was not empty + """ + self._onclear_queue.append(None) + + def is_kodi_onclear(self): + """ + Returns False if the last kodimonitor on_remove was caused by PKC - so + that we are not clearing the playlist twice. + + Calling this function will remove the item from our "checklist" + """ + try: + self._onclear_queue.pop() + except IndexError: + return True + return False + def clear(self): """ Resets the playlist object to an empty playlist """ - self.kodi_pl.clear() # Clear Kodi playlist object + # kodi monitor's on_clear method will only be called if there were some + # items to begin with + if self.kodi_pl.size() != 0: + self.kodi_onclear() + self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] self.old_kodi_pl = [] self.id = None From 5337ae571563fa6a30a5b2bd450281ba9a03565c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 13:28:39 +0100 Subject: [PATCH 163/509] Major Plex Companion overhaul, part 6 --- resources/lib/PlexCompanion.py | 39 +-- resources/lib/companion.py | 6 + resources/lib/kodimonitor.py | 13 + resources/lib/playqueue.py | 8 +- resources/lib/plexbmchelper/listener.py | 166 +++++------ resources/lib/plexbmchelper/subscribers.py | 322 +++++++++++---------- resources/lib/state.py | 5 + resources/lib/variables.py | 14 + 8 files changed, 302 insertions(+), 271 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 15a33f77..08ec3d7b 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -34,13 +34,11 @@ class PlexCompanion(Thread): """ def __init__(self, callback=None): LOG.info("----===## Starting PlexCompanion ##===----") - if callback is not None: - self.mgr = callback + self.mgr = callback # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails() - LOG.debug("Registration string is:\n%s", - self.client.getClientDetails()) + LOG.debug("Registration string is:\n%s", self.client.getClientDetails()) # kodi player instance self.player = player.PKC_Player() self.httpd = False @@ -54,14 +52,13 @@ class PlexCompanion(Thread): try: xml[0].attrib except (AttributeError, IndexError, TypeError): - LOG.error('Could not download Plex metadata') + LOG.error('Could not download Plex metadata for: %s', data) return api = API(xml[0]) if api.getType() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') - queue = self.mgr.playqueue.init_playqueue_from_plex_children( - api.getRatingKey()) - queue.plex_transient_token = data.get('token') + self.mgr.playqueue.init_playqueue_from_plex_children( + api.getRatingKey(), transient_token=data.get('token')) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') params = { @@ -92,13 +89,7 @@ class PlexCompanion(Thread): @LOCKER.lockthis def _process_playlist(self, data): # Get the playqueue ID - try: - _, container_key, query = ParseContainerKey(data['containerKey']) - except: - LOG.error('Exception while processing') - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - return + _, container_key, query = ParseContainerKey(data['containerKey']) try: playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) @@ -114,16 +105,12 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - if playqueue.id == container_key: - # OK, really weird, this happens at least with Plex for Android - LOG.debug('Already know this Plex playQueue, ignoring this command') - else: - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - playqueue_id=container_key, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('token')) + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('token')) @LOCKER.lockthis def _process_streams(self, data): @@ -309,5 +296,5 @@ class PlexCompanion(Thread): # Don't sleep continue sleep(50) - self.subscription_manager.signal_stop() + subscription_manager.signal_stop() client.stop_all() diff --git a/resources/lib/companion.py b/resources/lib/companion.py index eccc60c2..7004ff06 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -9,6 +9,7 @@ from variables import ALEXA_TO_COMPANION from playqueue import Playqueue from PlexFunctions import GetPlexKeyNumber import json_rpc as js +import state ############################################################################### @@ -64,6 +65,7 @@ def process_command(request_path, params, queue=None): convert_alexa_to_companion(params) LOG.debug('Received request_path: %s, params: %s', request_path, params) if request_path == 'player/playback/playMedia': + state.PLAYBACK_INIT_DONE = False # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' queue.put({ @@ -71,6 +73,7 @@ def process_command(request_path, params, queue=None): 'data': params }) elif request_path == 'player/playback/refreshPlayQueue': + state.PLAYBACK_INIT_DONE = False queue.put({ 'action': 'refreshPlayQueue', 'data': params @@ -93,10 +96,13 @@ def process_command(request_path, params, queue=None): elif request_path == "player/playback/stepBack": js.smallbackward() elif request_path == "player/playback/skipNext": + state.PLAYBACK_INIT_DONE = False js.skipnext() elif request_path == "player/playback/skipPrevious": + state.PLAYBACK_INIT_DONE = False js.skipprevious() elif request_path == "player/playback/skipTo": + state.PLAYBACK_INIT_DONE = False skip_to(params) elif request_path == "player/navigation/moveUp": js.input_up() diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c796f74a..51d18680 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -125,7 +125,9 @@ class KodiMonitor(Monitor): LOG.debug("Method: %s Data: %s", method, data) if method == "Player.OnPlay": + state.PLAYBACK_INIT_DONE = False self.PlayBackStart(data) + state.PLAYBACK_INIT_DONE = True elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') @@ -336,6 +338,17 @@ class KodiMonitor(Monitor): kodi_item={'id': kodi_id, 'type': kodi_type, 'file': path}) + # Set the Plex container key (e.g. using the Plex playqueue) + container_key = None + if info['playlistid'] != -1: + # -1 is Kodi's answer if there is no playlist + container_key = self.playqueue.playqueues[playerid].id + if container_key is not None: + container_key = '/playQueues/%s' % container_key + elif plex_id is not None: + container_key = '/library/metadata/%s' % plex_id + state.PLAYER_STATES[playerid]['container_key'] = container_key + LOG.debug('Set the Plex container_key to: %s', container_key) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index cdb23cf6..416078ca 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -77,7 +77,7 @@ class Playqueue(Thread): raise ValueError('Wrong playlist type passed in: %s' % typus) return playqueue - def init_playqueue_from_plex_children(self, plex_id): + def init_playqueue_from_plex_children(self, plex_id, transient_token=None): """ Init a new playqueue e.g. from an album. Alexa does this @@ -95,6 +95,7 @@ class Playqueue(Thread): for i, child in enumerate(xml): api = API(child) PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) + playqueue.plex_transient_token = transient_token LOG.debug('Firing up Kodi player') Player().play(playqueue.kodi_pl, None, False, 0) return playqueue @@ -114,6 +115,9 @@ class Playqueue(Thread): """ LOG.info('New playqueue %s received from Plex companion with offset ' '%s, repeat %s', playqueue_id, offset, repeat) + # Safe transient token from being deleted + if transient_token is None: + transient_token = playqueue.plex_transient_token with LOCK: xml = PL.get_PMS_playlist(playqueue, playqueue_id) playqueue.clear() @@ -123,7 +127,7 @@ class Playqueue(Thread): LOG.error('Could not get playqueue ID %s', playqueue_id) return playqueue.repeat = 0 if not repeat else int(repeat) - playqueue.token = transient_token + playqueue.plex_transient_token = transient_token PlaybackUtils(xml, playqueue).play_all() window('plex_customplaylist', value="true") if offset not in (None, "0"): diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index b3735a39..a39c9531 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -9,6 +9,7 @@ from urlparse import urlparse, parse_qs from xbmc import sleep from companion import process_command +from utils import window import json_rpc as js from clientinfo import getXArgsDeviceInfo import variables as v @@ -19,6 +20,21 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### +RESOURCES_XML = ('%s\n' + ' \n' + '\n') % (v.XML_HEADER, + v.ADDON_NAME, + v.PLATFORM, + v.ADDON_VERSION) class MyHandler(BaseHTTPRequestHandler): """ @@ -78,94 +94,68 @@ class MyHandler(BaseHTTPRequestHandler): self.serverlist = self.server.client.getServerList() sub_mgr = self.server.subscription_manager - try: - request_path = self.path[1:] - request_path = sub(r"\?.*", "", request_path) - url = urlparse(self.path) - paramarrays = parse_qs(url.query) - params = {} - for key in paramarrays: - params[key] = paramarrays[key][0] - LOG.debug("remote request_path: %s", request_path) - LOG.debug("params received from remote: %s", params) - sub_mgr.update_command_id(self.headers.get( - 'X-Plex-Client-Identifier', - self.client_address[0]), - params.get('commandID', False)) - if request_path == "version": - self.response( - "PlexKodiConnect Plex Companion: Running\nVersion: %s" - % v.ADDON_VERSION) - elif request_path == "verify": - self.response("XBMC JSON connection test:\n" + - js.ping()) - elif request_path == 'resources': - resp = ('%s' - '' - '' - '' - % (v.XML_HEADER, - v.DEVICENAME, - v.PKC_MACHINE_IDENTIFIER, - v.PLATFORM, - v.ADDON_VERSION)) - LOG.debug("crafted resources response: %s", resp) - self.response(resp, getXArgsDeviceInfo(include_token=False)) - elif "/poll" in request_path: - if params.get('wait', False) == '1': - sleep(950) - command_id = params.get('commandID', 0) - self.response( - sub(r"INSERTCOMMANDID", - str(command_id), - sub_mgr.msg(js.get_players())), - { - 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'X-Plex-Protocol': '1.0', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Max-Age': '1209600', - 'Access-Control-Expose-Headers': - 'X-Plex-Client-Identifier', - 'Content-Type': 'text/xml;charset=utf-8' - }) - elif "/subscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) - protocol = params.get('protocol', False) - host = self.client_address[0] - port = params.get('port', False) - uuid = self.headers.get('X-Plex-Client-Identifier', "") - command_id = params.get('commandID', 0) - sub_mgr.add_subscriber(protocol, - host, - port, - uuid, - command_id) - elif "/unsubscribe" in request_path: - self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) - uuid = self.headers.get('X-Plex-Client-Identifier', False) \ - or self.client_address[0] - sub_mgr.remove_subscriber(uuid) - else: - # Throw it to companion.py - process_command(request_path, params, self.server.queue) - self.response('', getXArgsDeviceInfo(include_token=False)) - sub_mgr.notify() - except: - LOG.error('Error encountered. Traceback:') - import traceback - LOG.error(traceback.print_exc()) + request_path = self.path[1:] + request_path = sub(r"\?.*", "", request_path) + url = urlparse(self.path) + paramarrays = parse_qs(url.query) + params = {} + for key in paramarrays: + params[key] = paramarrays[key][0] + LOG.debug("remote request_path: %s", request_path) + LOG.debug("params received from remote: %s", params) + sub_mgr.update_command_id(self.headers.get( + 'X-Plex-Client-Identifier', self.client_address[0]), + params.get('commandID')) + if request_path == "version": + self.response( + "PlexKodiConnect Plex Companion: Running\nVersion: %s" + % v.ADDON_VERSION) + elif request_path == "verify": + self.response("XBMC JSON connection test:\n" + js.ping()) + elif request_path == 'resources': + self.response( + RESOURCES_XML.format( + title=v.DEVICENAME, + machineIdentifier=window('plex_machineIdentifier')), + getXArgsDeviceInfo(include_token=False)) + elif "/poll" in request_path: + if params.get('wait') == '1': + sleep(950) + self.response( + sub_mgr.msg(js.get_players()).format( + command_id=params.get('commandID', 0)), + { + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', + 'Access-Control-Expose-Headers': + 'X-Plex-Client-Identifier', + 'Content-Type': 'text/xml;charset=utf-8' + }) + elif "/subscribe" in request_path: + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) + protocol = params.get('protocol') + host = self.client_address[0] + port = params.get('port') + uuid = self.headers.get('X-Plex-Client-Identifier') + command_id = params.get('commandID', 0) + sub_mgr.add_subscriber(protocol, + host, + port, + uuid, + command_id) + elif "/unsubscribe" in request_path: + self.response(v.COMPANION_OK_MESSAGE, + getXArgsDeviceInfo(include_token=False)) + uuid = self.headers.get('X-Plex-Client-Identifier') \ + or self.client_address[0] + sub_mgr.remove_subscriber(uuid) + else: + # Throw it to companion.py + process_command(request_path, params, self.server.queue) + self.response('', getXArgsDeviceInfo(include_token=False)) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 3876d73d..afab85c3 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -3,12 +3,10 @@ Manages getting playstate from Kodi and sending it to the PMS as well as subscribed Plex Companion clients. """ from logging import getLogger -from re import sub -from threading import Thread, Lock +from threading import Thread, RLock from downloadutils import DownloadUtils as DU from utils import window, kodi_time_to_millis, Lock_Function -from playlist_func import init_Plex_playlist import state import variables as v import json_rpc as js @@ -17,19 +15,19 @@ import json_rpc as js LOG = getLogger("PLEX." + __name__) # Need to lock all methods and functions messing with subscribers or state -LOCK = Lock() +LOCK = RLock() LOCKER = Lock_Function(LOCK) ############################################################################### # What is Companion controllable? CONTROLLABLE = { - v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', - v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' - 'skipPrevious,skipNext,stepBack,stepForward', - v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' - 'subtitleStream,seekTo,skipPrevious,skipNext,' - 'stepBack,stepForward' + v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' + 'subtitleStream,seekTo,skipPrevious,skipNext,' + 'stepBack,stepForward', + v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' + 'skipPrevious,skipNext,stepBack,stepForward', + v.PLEX_PLAYLIST_TYPE_PHOTO: 'skipPrevious,skipNext,stop' } STREAM_DETAILS = { @@ -38,6 +36,24 @@ STREAM_DETAILS = { 'subtitle': 'currentsubtitle' } +XML = ('%s\n' + ' \n' + ' \n' + ' \n' + '\n') % (v.XML_HEADER, + v.PLEX_PLAYLIST_TYPE_VIDEO, + v.PLEX_PLAYLIST_TYPE_AUDIO, + v.PLEX_PLAYLIST_TYPE_PHOTO) + + +def update_player_info(playerid): + """ + Updates all player info for playerid [int] in state.py. + """ + state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) + state.PLAYER_STATES[playerid]['volume'] = js.get_volume() + state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + class SubscriptionMgr(object): """ @@ -47,8 +63,6 @@ class SubscriptionMgr(object): self.serverlist = [] self.subscribers = {} self.info = {} - self.container_key = None - self.ratingkey = None self.server = "" self.protocol = "http" self.port = "" @@ -90,18 +104,126 @@ class SubscriptionMgr(object): Returns a timeline xml as str (xml containing video, audio, photo player state) """ - msg = v.XML_HEADER - msg += '\n' % (CONTROLLABLE[ptype], ptype, ptype) - playerid = player['playerid'] - info = self._player_info(playerid) - playqueue = self.playqueue.playqueues[playerid] - pos = info['position'] - try: - playqueue.items[pos] - except IndexError: - # E.g. for direct path playback for single item - return ' \n' % (CONTROLLABLE[ptype], ptype, ptype) - LOG.debug('INFO: %s', info) - LOG.debug('playqueue: %s', playqueue) - status = 'paused' if info['speed'] == '0' else 'playing' - ret = ' \n' - @LOCKER.lockthis def update_command_id(self, uuid, command_id): """ @@ -241,28 +256,27 @@ class SubscriptionMgr(object): if command_id and self.subscribers.get(uuid): self.subscribers[uuid].command_id = int(command_id) + @LOCKER.lockthis def notify(self): """ Causes PKC to tell the PMS and Plex Companion players to receive a notification what's being played. """ - with LOCK: - self._cleanup() - # Do we need a check to NOT tell about e.g. PVR/TV and Addon playback? + self._cleanup() + # Get all the active/playing Kodi players (video, audio, pictures) players = js.get_players() - # fetch the message, subscribers or not, since the server will need the - # info anyway - self.isplaying = False - msg = self.msg(players) - with LOCK: + # Update the PKC info with what's playing on the Kodi side + for player in players.values(): + update_player_info(player['playerid']) + if self.subscribers and state.PLAYBACK_INIT_DONE is True: + msg = self.msg(players) if self.isplaying is True: # If we don't check here, Plex Companion devices will simply # drop out of the Plex Companion playback screen for subscriber in self.subscribers.values(): subscriber.send_update(msg, not players) - self._notify_server(players) - self.lastplayers = players - return True + self._notify_server(players) + self.lastplayers = players def _notify_server(self, players): for typus, player in players.iteritems(): @@ -273,7 +287,7 @@ class SubscriptionMgr(object): except KeyError: pass # Process the players we have left (to signal a stop) - for _, player in self.lastplayers.iteritems(): + for player in self.lastplayers.values(): self.last_params['state'] = 'stopped' self._send_pms_notification(player['playerid'], self.last_params) @@ -282,18 +296,17 @@ class SubscriptionMgr(object): status = 'paused' if info['speed'] == '0' else 'playing' params = { 'state': status, - 'ratingKey': self.ratingkey, - 'key': '/library/metadata/%s' % self.ratingkey, + 'ratingKey': info['plex_id'], + 'key': '/library/metadata/%s' % info['plex_id'], 'time': kodi_time_to_millis(info['time']), 'duration': kodi_time_to_millis(info['totaltime']) } - if self.container_key: - params['containerKey'] = self.container_key - if self.container_key is not None and \ - self.container_key.startswith('/playQueues/'): - playqueue = self.playqueue.playqueues[playerid] - params['playQueueVersion'] = playqueue.version - params['playQueueItemID'] = playqueue.id + if info['container_key'] is not None: + params['containerKey'] = info['container_key'] + if info['container_key'].startswith('/playQueues/'): + playqueue = self.playqueue.playqueues[playerid] + params['playQueueVersion'] = playqueue.version + params['playQueueItemID'] = playqueue.id self.last_params = params return params @@ -384,11 +397,10 @@ class Subscriber(object): return True else: self.navlocationsent = True - msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg) + msg = msg.format(command_id=self.command_id) LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", self.uuid, self.command_id, msg) - url = self.protocol + '://' + self.host + ':' + self.port \ - + "/:/timeline" + url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port) thread = Thread(target=self._threaded_send, args=(url, msg)) thread.start() diff --git a/resources/lib/state.py b/resources/lib/state.py index a115f5bc..4c675e8d 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -106,6 +106,7 @@ PLAYER_STATES = { 'kodi_type': None, 'plex_id': None, 'plex_type': None, + 'container_key': None, 'volume': 100, 'muted': False }, @@ -116,6 +117,10 @@ PLAYER_STATES = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Set to False after having received a Companion command to play something +# Set to True after Kodi monitor PlayBackStart is done +# This will prohibit "old" Plex Companion messages being sent +PLAYBACK_INIT_DONE = True # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 2284f9c0..086a3ec0 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -129,6 +129,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo' PLEX_TYPE_PHOTO = 'photo' +# Used for /:/timeline XML messages +PLEX_PLAYLIST_TYPE_VIDEO = 'video' +PLEX_PLAYLIST_TYPE_AUDIO = 'music' +PLEX_PLAYLIST_TYPE_PHOTO = 'photo' + +KODI_PLAYLIST_TYPE_VIDEO = 'video' +KODI_PLAYLIST_TYPE_AUDIO = 'audio' +KODI_PLAYLIST_TYPE_PHOTO = 'picture' + +KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = { + PLEX_PLAYLIST_TYPE_VIDEO: KODI_PLAYLIST_TYPE_VIDEO, + PLEX_PLAYLIST_TYPE_AUDIO: KODI_PLAYLIST_TYPE_AUDIO, + PLEX_PLAYLIST_TYPE_PHOTO: KODI_PLAYLIST_TYPE_PHOTO +} # All the Kodi types as e.g. used in the JSON API KODI_TYPE_VIDEO = 'video' From 6c0ab381933205e494c3c3484220bc74ad2e8089 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 13:40:45 +0100 Subject: [PATCH 164/509] Fix wrong Plex machineIdentifier --- resources/lib/plexbmchelper/listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index a39c9531..2a03a432 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -116,7 +116,7 @@ class MyHandler(BaseHTTPRequestHandler): self.response( RESOURCES_XML.format( title=v.DEVICENAME, - machineIdentifier=window('plex_machineIdentifier')), + machineIdentifier=v.PKC_MACHINE_IDENTIFIER), getXArgsDeviceInfo(include_token=False)) elif "/poll" in request_path: if params.get('wait') == '1': From d8e40936965e29c06cbd5bd956e695c6f04cbbe3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 13:46:21 +0100 Subject: [PATCH 165/509] Use variable.py's machineIdentifier --- resources/lib/player.py | 2 +- resources/lib/websocket_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index e1d8e2ac..63071e91 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -343,6 +343,6 @@ class PKC_Player(Player): LOG.info("Transcoding for %s terminating" % itemid) self.doUtils().downloadUrl( "{server}/video/:/transcode/universal/stop", - parameters={'session': window('plex_client_Id')}) + parameters={'session': v.PKC_MACHINE_IDENTIFIER}) self.played_info.clear() diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index aeb3385c..748edfc0 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -14,6 +14,7 @@ from xbmc import sleep from utils import window, settings, thread_methods from companion import process_command import state +import variables as v ############################################################################### @@ -238,10 +239,9 @@ class Alexa_Websocket(WebSocket): __thread_suspended = False def getUri(self): - self.plex_client_Id = window('plex_client_Id') uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' % (state.PLEX_USER_ID, - self.plex_client_Id, state.PLEX_TOKEN)) + v.PKC_MACHINE_IDENTIFIER, state.PLEX_TOKEN)) sslopt = {} log.debug("%s: Uri: %s, sslopt: %s" % (self.__class__.__name__, uri, sslopt)) From 6caa759ce1d4a708992a18d730dcb2f5063fa699 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 14:23:08 +0100 Subject: [PATCH 166/509] Fix wrong partIndex --- resources/lib/plexbmchelper/subscribers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index afab85c3..c59c55ff 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -176,7 +176,7 @@ class SubscriptionMgr(object): 'volume': info['volume'], 'mute': mute, 'mediaIndex': pos, # Still to implement from here - 'partIndex':pos, + 'partIndex':0, 'partCount': len(playqueue.items), 'providerIdentifier': 'com.plexapp.plugins.library', } From 2e5249ca4fbf22a831c9b3a84d1a969d6c5751db Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 17:15:01 +0100 Subject: [PATCH 167/509] Don't allow spaces in devicename --- resources/lib/variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 086a3ec0..96996c67 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -61,7 +61,7 @@ DEVICENAME = DEVICENAME.replace("?", "") DEVICENAME = DEVICENAME.replace('|', "") DEVICENAME = DEVICENAME.replace('(', "") DEVICENAME = DEVICENAME.replace(')', "") -DEVICENAME = DEVICENAME.strip() +DEVICENAME = DEVICENAME.replace(' ', "") COMPANION_PORT = int(_ADDON.getSetting('companionPort')) From 18a9e77b33f41d52ff860a09e7b162ad0a47d85c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 1 Jan 2018 18:36:28 +0100 Subject: [PATCH 168/509] Plex Companion optimizations --- resources/lib/PlexCompanion.py | 16 +-- resources/lib/plexbmchelper/subscribers.py | 109 ++++++++++++++------- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 08ec3d7b..7e424166 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -105,12 +105,16 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = self.mgr.playqueue.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - playqueue_id=container_key, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('token')) + if container_key == playqueue.id: + LOG.info('Already know this playqueue - ignoring') + playqueue.transient_token = data.get('token') + else: + self.mgr.playqueue.update_playqueue_from_PMS( + playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('token')) @LOCKER.lockthis def _process_streams(self, data): diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index c59c55ff..c042eb9a 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -46,6 +46,34 @@ XML = ('%s\n' v.PLEX_PLAYLIST_TYPE_PHOTO) +def headers_pms(): + """ + Headers are different for Plex Companion - use these for PMS notifications + """ + return { + 'Content-type': 'text/plain', + 'Connection': 'Keep-Alive', + 'Keep-Alive': 'timeout=20', + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', + 'X-Plex-Protocol': "1.0" + } + + +def headers_companion_client(): + """ + Headers are different for Plex Companion - use these for a Plex Companion + client + """ + return { + 'Content-type': 'application/xml', + 'Connection': 'Keep-Alive', + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en,*' + } + + def update_player_info(playerid): """ Updates all player info for playerid [int] in state.py. @@ -67,6 +95,7 @@ class SubscriptionMgr(object): self.protocol = "http" self.port = "" self.isplaying = False + self.last_timelines = {} # In order to be able to signal a stop at the end self.last_params = {} self.lastplayers = {} @@ -75,19 +104,7 @@ class SubscriptionMgr(object): self.playqueue = mgr.playqueue self.request_mgr = request_mgr - @staticmethod - def _headers(): - """ - Headers are different for Plex Companion! - """ - return { - 'Content-type': 'text/plain', - 'Connection': 'Keep-Alive', - 'Keep-Alive': 'timeout=20', - 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', - 'X-Plex-Protocol': "1.0" - } + def _server_by_host(self, host): if len(self.serverlist) == 1: @@ -113,16 +130,29 @@ class SubscriptionMgr(object): } for typus in timelines: if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None: - timeline = { + timeline = self._dict_to_xml({ 'controllable': CONTROLLABLE[typus], 'type': typus, 'state': 'stopped' - } + }) else: - timeline = self._timeline_dict(players[ - v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]], typus) - timelines[typus] = self._dict_to_xml(timeline) + try: + timeline = self._dict_to_xml(self._timeline_dict(players[ + v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]], + typus)) + except RuntimeError: + try: + timeline = self.last_timelines[typus] + except KeyError: + # On startup + timeline = self._dict_to_xml({ + 'controllable': CONTROLLABLE[typus], + 'type': typus, + 'state': 'stopped' + }) + timelines[typus] = timeline location = 'fullScreenVideo' if self.isplaying else 'navigation' + self.last_timelines = dict(timelines) timelines.update({'command_id': '{command_id}', 'location': location}) return answ.format(**timelines) @@ -142,7 +172,7 @@ class SubscriptionMgr(object): playqueue = self.playqueue.playqueues[playerid] pos = info['position'] try: - playqueue.items[pos] + item = playqueue.items[pos] except IndexError: # E.g. for direct path playback for single item return { @@ -150,6 +180,11 @@ class SubscriptionMgr(object): 'type': ptype, 'state': 'stopped' } + self.isplaying = True + if item.plex_id != info['plex_id']: + # Kodi playqueue already progressed; need to wait until everything + # is loaded + raise RuntimeError pbmc_server = window('pms_server') if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') @@ -180,10 +215,11 @@ class SubscriptionMgr(object): 'partCount': len(playqueue.items), 'providerIdentifier': 'com.plexapp.plugins.library', } - - if info['plex_id']: - answ['key'] = '/library/metadata/%s' % info['plex_id'] - answ['ratingKey'] = info['plex_id'] + # Get the plex id from the PKC playqueue not info, as Kodi jumps to next + # playqueue element way BEFORE kodi monitor onplayback is called + if item.plex_id: + answ['key'] = '/library/metadata/%s' % item.plex_id + answ['ratingKey'] = item.plex_id # PlayQueue stuff if info['container_key']: answ['containerKey'] = info['container_key'] @@ -191,9 +227,9 @@ class SubscriptionMgr(object): info['container_key'].startswith('/playQueues')): answ['playQueueID'] = playqueue.id answ['playQueueVersion'] = playqueue.version - answ['playQueueItemID'] = playqueue.items[pos].id + answ['playQueueItemID'] = item.id if playqueue.items[pos].guid: - answ['guid'] = playqueue.items[pos].guid + answ['guid'] = item.guid # Temp. token set? if state.PLEX_TRANSIENT_TOKEN: answ['token'] = state.PLEX_TRANSIENT_TOKEN @@ -222,7 +258,6 @@ class SubscriptionMgr(object): if strm_id is not None: # If None, then the subtitle is only present on Kodi side answ['subtitleStreamID'] = strm_id - self.isplaying = True return answ def signal_stop(self): @@ -268,6 +303,7 @@ class SubscriptionMgr(object): # Update the PKC info with what's playing on the Kodi side for player in players.values(): update_player_info(player['playerid']) + self._notify_server(players) if self.subscribers and state.PLAYBACK_INIT_DONE is True: msg = self.msg(players) if self.isplaying is True: @@ -275,7 +311,6 @@ class SubscriptionMgr(object): # drop out of the Plex Companion playback screen for subscriber in self.subscribers.values(): subscriber.send_update(msg, not players) - self._notify_server(players) self.lastplayers = players def _notify_server(self, players): @@ -293,26 +328,31 @@ class SubscriptionMgr(object): def _get_pms_params(self, playerid): info = state.PLAYER_STATES[playerid] + playqueue = self.playqueue.playqueues[playerid] + try: + item = playqueue.items[info['position']] + except IndexError: + return self.last_params status = 'paused' if info['speed'] == '0' else 'playing' params = { 'state': status, - 'ratingKey': info['plex_id'], - 'key': '/library/metadata/%s' % info['plex_id'], + 'ratingKey': item.plex_id, + 'key': '/library/metadata/%s' % item.plex_id, 'time': kodi_time_to_millis(info['time']), 'duration': kodi_time_to_millis(info['totaltime']) } if info['container_key'] is not None: params['containerKey'] = info['container_key'] if info['container_key'].startswith('/playQueues/'): - playqueue = self.playqueue.playqueues[playerid] params['playQueueVersion'] = playqueue.version - params['playQueueItemID'] = playqueue.id + params['playQueueID'] = playqueue.id + params['playQueueItemID'] = item.id self.last_params = params return params def _send_pms_notification(self, playerid, params): serv = self._server_by_host(self.server) - xargs = self._headers() + xargs = headers_pms() playqueue = self.playqueue.playqueues[playerid] if state.PLEX_TRANSIENT_TOKEN: xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN @@ -409,6 +449,9 @@ class Subscriber(object): Threaded POST request, because they stall due to PMS response missing the Content-Length header :-( """ - response = DU().downloadUrl(url, postBody=msg, action_type="POST") + response = DU().downloadUrl(url, + postBody=msg, + action_type="POST", + headerOptions=headers_companion_client()) if response in (False, None, 401): self.sub_mgr.remove_subscriber(self.uuid) From 3a9f65d9088cf3ea89fd337f69a4c501fece9312 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 10:58:28 +0100 Subject: [PATCH 169/509] Remove obsolete code --- resources/lib/plexbmchelper/subscribers.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index c042eb9a..6b6b0d30 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -310,7 +310,7 @@ class SubscriptionMgr(object): # If we don't check here, Plex Companion devices will simply # drop out of the Plex Companion playback screen for subscriber in self.subscribers.values(): - subscriber.send_update(msg, not players) + subscriber.send_update(msg) self.lastplayers = players def _notify_server(self, players): @@ -412,7 +412,6 @@ class Subscriber(object): self.port = port or 32400 self.uuid = uuid or host self.command_id = int(command_id) or 0 - self.navlocationsent = False self.age = 0 self.sub_mgr = sub_mgr self.request_mgr = request_mgr @@ -426,17 +425,11 @@ class Subscriber(object): """ self.request_mgr.closeConnection(self.protocol, self.host, self.port) - def send_update(self, msg, is_nav): + def send_update(self, msg): """ Sends msg to the Plex Companion client (via .../:/timeline) """ self.age += 1 - if not is_nav: - self.navlocationsent = False - elif self.navlocationsent: - return True - else: - self.navlocationsent = True msg = msg.format(command_id=self.command_id) LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", self.uuid, self.command_id, msg) From 14183cccca09161227c51b87c3b53defd0de33a5 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 11:48:44 +0100 Subject: [PATCH 170/509] Fix Plex Companion headers & URL arguments --- resources/lib/downloadutils.py | 8 +++- resources/lib/plexbmchelper/subscribers.py | 56 +++++++++++++++------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index d7078fe0..d54722a6 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -141,7 +141,8 @@ class DownloadUtils(): def downloadUrl(self, url, action_type="GET", postBody=None, parameters=None, authenticate=True, headerOptions=None, - verifySSL=True, timeout=None, return_response=False): + verifySSL=True, timeout=None, return_response=False, + headerOverride=None): """ Override SSL check with verifySSL=False @@ -172,7 +173,10 @@ class DownloadUtils(): # User is not (yet) authenticated. Used to communicate with # plex.tv and to check for PMS servers s = requests - headerOptions = self.getHeader(options=headerOptions) + if not headerOverride: + headerOptions = self.getHeader(options=headerOptions) + else: + headerOptions = headerOverride if settings('sslcert') != 'None': kwargs['cert'] = settings('sslcert') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 6b6b0d30..3e913187 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -45,18 +45,31 @@ XML = ('%s\n' v.PLEX_PLAYLIST_TYPE_AUDIO, v.PLEX_PLAYLIST_TYPE_PHOTO) +# Headers are different for Plex Companion - use these for PMS notifications +HEADERS_PMS = { + 'Connection': 'Keep-Alive', + 'Accept': 'text/plain, */*; q=0.01', + 'Accept-Language': 'en', + 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM), + 'Accept-Encoding': 'gzip, deflate' +} -def headers_pms(): + +def params_pms(): """ - Headers are different for Plex Companion - use these for PMS notifications + Returns the url parameters for communicating with the PMS """ return { - 'Content-type': 'text/plain', - 'Connection': 'Keep-Alive', - 'Keep-Alive': 'timeout=20', + # 'X-Plex-Client-Capabilities': ['protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:2160&level:52};audioDecoders=mp3,aac,dts{bitrate:800000&channels:2},ac3{bitrate:800000&channels:2}'], 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier', - 'X-Plex-Protocol': "1.0" + 'X-Plex-Device': v.PLATFORM, + 'X-Plex-Device-Name': v.DEVICENAME, + # 'X-Plex-Device-Screen-Resolution': ['1916x1018,1920x1080'], + 'X-Plex-Platform': v.PLATFORM, + 'X-Plex-Product': v.ADDON_NAME, + 'X-Plex-Version': v.ADDON_VERSION, + 'hasMDE': '1', + # 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'], } @@ -69,6 +82,12 @@ def headers_companion_client(): 'Content-type': 'application/xml', 'Connection': 'Keep-Alive', 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Device': v.PLATFORM, + 'X-Plex-Device-Name': v.DEVICENAME, + 'X-Plex-Platform': v.PLATFORM, + 'X-Plex-Product': v.ADDON_NAME, + 'X-Plex-Version': v.ADDON_VERSION, + 'X-Plex-Provides': 'client,controller,player,pubsub-player', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en,*' } @@ -342,18 +361,19 @@ class SubscriptionMgr(object): 'duration': kodi_time_to_millis(info['totaltime']) } if info['container_key'] is not None: - params['containerKey'] = info['container_key'] + # params['containerKey'] = info['container_key'] if info['container_key'].startswith('/playQueues/'): - params['playQueueVersion'] = playqueue.version - params['playQueueID'] = playqueue.id + # params['playQueueVersion'] = playqueue.version + # params['playQueueID'] = playqueue.id params['playQueueItemID'] = item.id self.last_params = params return params def _send_pms_notification(self, playerid, params): serv = self._server_by_host(self.server) - xargs = headers_pms() playqueue = self.playqueue.playqueues[playerid] + xargs = params_pms() + xargs.update(params) if state.PLEX_TRANSIENT_TOKEN: xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN elif playqueue.plex_transient_token: @@ -363,9 +383,12 @@ class SubscriptionMgr(object): url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) - DU().downloadUrl(url, parameters=params, headerOptions=xargs) + DU().downloadUrl(url, + authenticate=False, + parameters=xargs, + headerOverride=HEADERS_PMS) LOG.debug("Sent server notification with parameters: %s to %s", - params, url) + xargs, url) @LOCKER.lockthis def add_subscriber(self, protocol, host, port, uuid, command_id): @@ -439,12 +462,13 @@ class Subscriber(object): def _threaded_send(self, url, msg): """ - Threaded POST request, because they stall due to PMS response missing + Threaded POST request, because they stall due to response missing the Content-Length header :-( """ response = DU().downloadUrl(url, - postBody=msg, action_type="POST", - headerOptions=headers_companion_client()) + postBody=msg, + authenticate=False, + headerOverride=headers_companion_client()) if response in (False, None, 401): self.sub_mgr.remove_subscriber(self.uuid) From 95356d94837f4c5bca5284d4774d36e2991689de Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 12:13:41 +0100 Subject: [PATCH 171/509] Fix headers --- resources/lib/plexbmchelper/subscribers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 3e913187..193042cf 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -50,7 +50,6 @@ HEADERS_PMS = { 'Connection': 'Keep-Alive', 'Accept': 'text/plain, */*; q=0.01', 'Accept-Language': 'en', - 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM), 'Accept-Encoding': 'gzip, deflate' } @@ -79,15 +78,14 @@ def headers_companion_client(): client """ return { - 'Content-type': 'application/xml', + 'Content-Type': 'application/xml', 'Connection': 'Keep-Alive', 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'X-Plex-Device': v.PLATFORM, 'X-Plex-Device-Name': v.DEVICENAME, 'X-Plex-Platform': v.PLATFORM, + 'X-Plex-Platform-Version': 'unknown', 'X-Plex-Product': v.ADDON_NAME, 'X-Plex-Version': v.ADDON_VERSION, - 'X-Plex-Provides': 'client,controller,player,pubsub-player', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en,*' } From ac3be938948c94c61cdaec11b233605ce69fdf19 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 13:28:25 +0100 Subject: [PATCH 172/509] More Plex Companion fixes --- resources/lib/plexbmchelper/subscribers.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 193042cf..85d1b0e3 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -27,7 +27,7 @@ CONTROLLABLE = { 'stepBack,stepForward', v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' 'skipPrevious,skipNext,stepBack,stepForward', - v.PLEX_PLAYLIST_TYPE_PHOTO: 'skipPrevious,skipNext,stop' + v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext' } STREAM_DETAILS = { @@ -47,10 +47,11 @@ XML = ('%s\n' # Headers are different for Plex Companion - use these for PMS notifications HEADERS_PMS = { - 'Connection': 'Keep-Alive', + 'Connection': 'keep-alive', 'Accept': 'text/plain, */*; q=0.01', 'Accept-Language': 'en', - 'Accept-Encoding': 'gzip, deflate' + 'Accept-Encoding': 'gzip, deflate', + 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM) } @@ -59,13 +60,19 @@ def params_pms(): Returns the url parameters for communicating with the PMS """ return { - # 'X-Plex-Client-Capabilities': ['protocols=shoutcast,http-video;videoDecoders=h264{profile:high&resolution:2160&level:52};audioDecoders=mp3,aac,dts{bitrate:800000&channels:2},ac3{bitrate:800000&channels:2}'], + # 'X-Plex-Client-Capabilities': 'protocols=shoutcast,http-video;' + # 'videoDecoders=h264{profile:high&resolution:2160&level:52};' + # 'audioDecoders=mp3,aac,dts{bitrate:800000&channels:2},' + # 'ac3{bitrate:800000&channels:2}', 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'X-Plex-Device': v.PLATFORM, 'X-Plex-Device-Name': v.DEVICENAME, - # 'X-Plex-Device-Screen-Resolution': ['1916x1018,1920x1080'], + # 'X-Plex-Device-Screen-Resolution': '1916x1018,1920x1080', + 'X-Plex-Model': 'unknown', 'X-Plex-Platform': v.PLATFORM, + 'X-Plex-Platform-Version': 'unknown', 'X-Plex-Product': v.ADDON_NAME, + 'X-Plex-Provider-Version': v.ADDON_VERSION, 'X-Plex-Version': v.ADDON_VERSION, 'hasMDE': '1', # 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'], @@ -227,9 +234,9 @@ class SubscriptionMgr(object): 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']], 'volume': info['volume'], 'mute': mute, - 'mediaIndex': pos, # Still to implement from here + 'mediaIndex': 0, # Still to implement from here 'partIndex':0, - 'partCount': len(playqueue.items), + 'partCount': 1, 'providerIdentifier': 'com.plexapp.plugins.library', } # Get the plex id from the PKC playqueue not info, as Kodi jumps to next From b84a833e0de823639799d143c8ee67ed95b8edc6 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 14:30:54 +0100 Subject: [PATCH 173/509] Remove unreliable check for playback init --- resources/lib/companion.py | 5 ----- resources/lib/kodimonitor.py | 2 -- resources/lib/plexbmchelper/subscribers.py | 2 +- resources/lib/state.py | 4 ---- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 7004ff06..0b24df4f 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -65,7 +65,6 @@ def process_command(request_path, params, queue=None): convert_alexa_to_companion(params) LOG.debug('Received request_path: %s, params: %s', request_path, params) if request_path == 'player/playback/playMedia': - state.PLAYBACK_INIT_DONE = False # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' queue.put({ @@ -73,7 +72,6 @@ def process_command(request_path, params, queue=None): 'data': params }) elif request_path == 'player/playback/refreshPlayQueue': - state.PLAYBACK_INIT_DONE = False queue.put({ 'action': 'refreshPlayQueue', 'data': params @@ -96,13 +94,10 @@ def process_command(request_path, params, queue=None): elif request_path == "player/playback/stepBack": js.smallbackward() elif request_path == "player/playback/skipNext": - state.PLAYBACK_INIT_DONE = False js.skipnext() elif request_path == "player/playback/skipPrevious": - state.PLAYBACK_INIT_DONE = False js.skipprevious() elif request_path == "player/playback/skipTo": - state.PLAYBACK_INIT_DONE = False skip_to(params) elif request_path == "player/navigation/moveUp": js.input_up() diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 51d18680..d250841b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -125,9 +125,7 @@ class KodiMonitor(Monitor): LOG.debug("Method: %s Data: %s", method, data) if method == "Player.OnPlay": - state.PLAYBACK_INIT_DONE = False self.PlayBackStart(data) - state.PLAYBACK_INIT_DONE = True elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 85d1b0e3..17b8f1a4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -328,7 +328,7 @@ class SubscriptionMgr(object): for player in players.values(): update_player_info(player['playerid']) self._notify_server(players) - if self.subscribers and state.PLAYBACK_INIT_DONE is True: + if self.subscribers: msg = self.msg(players) if self.isplaying is True: # If we don't check here, Plex Companion devices will simply diff --git a/resources/lib/state.py b/resources/lib/state.py index 4c675e8d..931a5227 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -117,10 +117,6 @@ PLAYER_STATES = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} -# Set to False after having received a Companion command to play something -# Set to True after Kodi monitor PlayBackStart is done -# This will prohibit "old" Plex Companion messages being sent -PLAYBACK_INIT_DONE = True # Kodi webserver details WEBSERVER_PORT = 8080 From 6bfd67a41da1107b6de37954c0fd5b69fa36bed3 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 14:31:57 +0100 Subject: [PATCH 174/509] Fix Plex ratingKey being stored as int, not str --- resources/lib/kodimonitor.py | 2 +- resources/lib/playlist_func.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d250841b..f7cc5cce 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -302,7 +302,7 @@ class KodiMonitor(Monitor): with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) try: - plex_id = plex_dbitem[0] + plex_id = str(plex_dbitem[0]) plex_type = plex_dbitem[2] except TypeError: # No plex id, hence item not in the library. E.g. clips diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 919a3f5e..422c9b90 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -183,7 +183,7 @@ class Playlist_Item(object): plex_id = None [str] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' plex_uuid = None [str] Plex librarySectionUUID - kodi_id = None Kodi unique kodi id (unique only within type!) + kodi_id = None [int] Kodi unique kodi id (unique only within type!) kodi_type = None [str] Kodi type: 'movie' file = None [str] Path to the item's file. STRING!! uri = None [str] Weird Plex uri path involving plex_uuid. STRING! @@ -276,9 +276,9 @@ def playlist_item_from_kodi(kodi_item): plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'], kodi_item['type']) try: - item.plex_id = plex_dbitem[0] + item.plex_id = str(plex_dbitem[0]) item.plex_type = plex_dbitem[2] - item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-) + item.plex_uuid = str(plex_dbitem[0]) # we dont need the uuid yet :-) except TypeError: pass item.file = kodi_item.get('file') From 93b878ad786535e7b46fa1de5a91b4b481208285 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 15:10:23 +0100 Subject: [PATCH 175/509] Fix playlist item representation (str, not int) --- resources/lib/playlist_func.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 422c9b90..f8cf80fd 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -209,8 +209,8 @@ class Playlist_Item(object): Print the playlist item, e.g. to log """ answ = '{\'%s\': {' % (self.__class__.__name__) - answ += '\'id\': %s, ' % self.id - answ += '\'plex_id\': %s, ' % self.plex_id + answ += '\'id\': \'%s\', ' % self.id + answ += '\'plex_id\': \'%s\', ' % self.plex_id for key in self.__dict__: if key in ('id', 'plex_id', 'xml'): continue From f4e83f6be592e276070cc5c3cadfb5ef93376987 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 15:11:49 +0100 Subject: [PATCH 176/509] Better detect if PKC playback init is still ongoing --- resources/lib/plexbmchelper/subscribers.py | 54 +++++++++++++--------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 17b8f1a4..aea7e0cd 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -119,7 +119,6 @@ class SubscriptionMgr(object): self.protocol = "http" self.port = "" self.isplaying = False - self.last_timelines = {} # In order to be able to signal a stop at the end self.last_params = {} self.lastplayers = {} @@ -154,29 +153,17 @@ class SubscriptionMgr(object): } for typus in timelines: if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None: - timeline = self._dict_to_xml({ + timeline = { 'controllable': CONTROLLABLE[typus], 'type': typus, 'state': 'stopped' - }) + } else: - try: - timeline = self._dict_to_xml(self._timeline_dict(players[ + timeline = self._timeline_dict(players[ v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]], - typus)) - except RuntimeError: - try: - timeline = self.last_timelines[typus] - except KeyError: - # On startup - timeline = self._dict_to_xml({ - 'controllable': CONTROLLABLE[typus], - 'type': typus, - 'state': 'stopped' - }) - timelines[typus] = timeline + typus) + timelines[typus] = self._dict_to_xml(timeline) location = 'fullScreenVideo' if self.isplaying else 'navigation' - self.last_timelines = dict(timelines) timelines.update({'command_id': '{command_id}', 'location': location}) return answ.format(**timelines) @@ -205,10 +192,6 @@ class SubscriptionMgr(object): 'state': 'stopped' } self.isplaying = True - if item.plex_id != info['plex_id']: - # Kodi playqueue already progressed; need to wait until everything - # is loaded - raise RuntimeError pbmc_server = window('pms_server') if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') @@ -315,6 +298,28 @@ class SubscriptionMgr(object): if command_id and self.subscribers.get(uuid): self.subscribers[uuid].command_id = int(command_id) + def _playqueue_init_done(self, players): + """ + update_player_info() can result in values BEFORE kodi monitor is called. + Hence we'd have a missmatch between the state.PLAYER_STATES and our + playqueues. + """ + for player in players.values(): + info = state.PLAYER_STATES[player['playerid']] + playqueue = self.playqueue.playqueues[player['playerid']] + try: + item = playqueue.items[info['position']] + except IndexError: + # E.g. for direct path playback for single item + return False + LOG.debug('item: %s', item) + LOG.debug('playstate: %s', info) + if item.plex_id != info['plex_id']: + # Kodi playqueue already progressed; need to wait until + # everything is loaded + return False + return True + @LOCKER.lockthis def notify(self): """ @@ -327,6 +332,11 @@ class SubscriptionMgr(object): # Update the PKC info with what's playing on the Kodi side for player in players.values(): update_player_info(player['playerid']) + # Check whether we can use the CURRENT info or whether PKC is still + # initializing + if self._playqueue_init_done(players) is False: + LOG.debug('PKC playqueue is still initializing - skipping update') + return self._notify_server(players) if self.subscribers: msg = self.msg(players) From 359a8d0221d8d0208594f5f9a76c6044fdff84ae Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 15:12:01 +0100 Subject: [PATCH 177/509] Revert "Fix Plex ratingKey being stored as int, not str" This reverts commit 6bfd67a41da1107b6de37954c0fd5b69fa36bed3. --- resources/lib/kodimonitor.py | 2 +- resources/lib/playlist_func.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index f7cc5cce..d250841b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -302,7 +302,7 @@ class KodiMonitor(Monitor): with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) try: - plex_id = str(plex_dbitem[0]) + plex_id = plex_dbitem[0] plex_type = plex_dbitem[2] except TypeError: # No plex id, hence item not in the library. E.g. clips diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index f8cf80fd..310a1f64 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -183,7 +183,7 @@ class Playlist_Item(object): plex_id = None [str] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' plex_uuid = None [str] Plex librarySectionUUID - kodi_id = None [int] Kodi unique kodi id (unique only within type!) + kodi_id = None Kodi unique kodi id (unique only within type!) kodi_type = None [str] Kodi type: 'movie' file = None [str] Path to the item's file. STRING!! uri = None [str] Weird Plex uri path involving plex_uuid. STRING! @@ -276,9 +276,9 @@ def playlist_item_from_kodi(kodi_item): plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'], kodi_item['type']) try: - item.plex_id = str(plex_dbitem[0]) + item.plex_id = plex_dbitem[0] item.plex_type = plex_dbitem[2] - item.plex_uuid = str(plex_dbitem[0]) # we dont need the uuid yet :-) + item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-) except TypeError: pass item.file = kodi_item.get('file') From ec4a5d2b7c22ca586165d90cd87c51dafbb7e0f6 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Tue, 2 Jan 2018 15:39:48 +0100 Subject: [PATCH 178/509] Prettify --- resources/lib/playbackutils.py | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 0fc5f618..cf5dee55 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -25,7 +25,7 @@ import state ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -41,7 +41,7 @@ class PlaybackUtils(): plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting to the PMS """ - log.info("Playbackutils called") + LOG.info("Playbackutils called") item = self.xml[0] api = API(item) playqueue = self.playqueue @@ -51,7 +51,7 @@ class PlaybackUtils(): playutils = putils.PlayUtils(item) playurl = playutils.getPlayUrl() if not playurl: - log.error('No playurl found, aborting') + LOG.error('No playurl found, aborting') return if kodi_id in (None, 'plextrailer', 'plexnode'): @@ -67,8 +67,8 @@ class PlaybackUtils(): try: xml[0].attrib except (TypeError, AttributeError): - log.error('Could not download %s' - % item[0][0].attrib.get('key')) + LOG.error('Could not download %s', + item[0][0].attrib.get('key')) return playurl = tryEncode(xml[0].attrib.get('key')) window('plex_%s.playmethod' % playurl, value='DirectStream') @@ -101,10 +101,10 @@ class PlaybackUtils(): introsPlaylist = False dummyPlaylist = False - log.info("Playing from contextmenu: %s" % contextmenu_play) - log.info("Playlist start position: %s" % startPos) - log.info("Playlist plugin position: %s" % self.currentPosition) - log.info("Playlist size: %s" % sizePlaylist) + LOG.info("Playing from contextmenu: %s", contextmenu_play) + LOG.info("Playlist start position: %s", startPos) + LOG.info("Playlist plugin position: %s", self.currentPosition) + LOG.info("Playlist size: %s", sizePlaylist) # RESUME POINT ################ seektime, runtime = api.getRuntime() @@ -116,7 +116,7 @@ class PlaybackUtils(): # Otherwise we get a loop. if not propertiesPlayback: window('plex_playbackProps', value="true") - log.info("Setting up properties in playlist.") + LOG.info("Setting up properties in playlist.") # Where will the player need to start? # Do we need to get trailers? trailers = False @@ -146,7 +146,7 @@ class PlaybackUtils(): 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.") + LOG.debug("Adding dummy file to playlist.") dummyPlaylist = True add_listitem_to_Kodi_playlist( playqueue, @@ -181,7 +181,7 @@ class PlaybackUtils(): 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.") + LOG.info("Adding main item to playlist.") add_item_to_kodi_playlist( playqueue, self.currentPosition, @@ -192,7 +192,7 @@ class PlaybackUtils(): if state.DIRECT_PATHS: # Cannot add via JSON with full metadata because then we # Would be using the direct path - log.debug("Adding contextmenu item for direct paths") + LOG.debug("Adding contextmenu item for direct paths") if window('plex_%s.playmethod' % playurl) == "Transcode": playutils.audioSubsPref(listitem, tryDecode(playurl)) api.CreateListItemFromPlexItem(listitem) @@ -225,7 +225,7 @@ class PlaybackUtils(): 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.") + LOG.info("Processed as a playlist. First item is skipped.") # Delete the item that's gonna fail! del playqueue.items[startPos] # Don't attach listitem @@ -233,7 +233,7 @@ class PlaybackUtils(): # We just skipped adding properties. Reset flag for next time. elif propertiesPlayback: - log.debug("Resetting properties playback flag.") + LOG.debug("Resetting properties playback flag.") window('plex_playbackProps', clear=True) # SETUP MAIN ITEM ########## @@ -249,7 +249,7 @@ class PlaybackUtils(): # PLAYBACK ################ if (homeScreen and seektime and window('plex_customplaylist') != "true" and not contextmenu_play): - log.info("Play as a widget item") + LOG.info("Play as a widget item") api.CreateListItemFromPlexItem(listitem) result.listitem = listitem return result @@ -259,7 +259,7 @@ class PlaybackUtils(): contextmenu_play): # Playlist was created just now, play it. # Contextmenu plays always need this - log.info("Play playlist from starting position %s" % startPos) + LOG.info("Play playlist from starting position %s", startPos) # Need a separate thread because Player won't return in time thread = Thread(target=Player().play, args=(playqueue.kodi_pl, None, False, startPos)) @@ -268,7 +268,7 @@ class PlaybackUtils(): # Don't attach listitem return result else: - log.info("Play as a regular item") + LOG.info("Play as a regular item") result.listitem = listitem return result @@ -276,7 +276,7 @@ class PlaybackUtils(): """ Play all items contained in the xml passed in. Called by Plex Companion """ - log.info("Playbackutils play_all called") + LOG.info("Playbackutils play_all called") window('plex_playbackProps', value="true") self.currentPosition = 0 for item in self.xml: @@ -322,7 +322,7 @@ class PlaybackUtils(): introAPI.set_listitem_artwork(listitem) # Overwrite the Plex url listitem.setPath(introPlayurl) - log.info("Adding Plex trailer: %s" % introPlayurl) + LOG.info("Adding Plex trailer: %s", introPlayurl) add_listitem_to_Kodi_playlist( self.playqueue, self.currentPosition, @@ -346,8 +346,8 @@ class PlaybackUtils(): playutils = putils.PlayUtils(item) additionalPlayurl = playutils.getPlayUrl( partNumber=counter) - log.debug("Adding additional part: %s, url: %s" - % (counter, additionalPlayurl)) + LOG.debug("Adding additional part: %s, url: %s", + counter, additionalPlayurl) api.CreateListItemFromPlexItem(additionalListItem) api.set_playback_win_props(additionalPlayurl, additionalListItem) From 546e79d9254be5be3c82929f0bcda66271286dea Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 3 Jan 2018 17:50:01 +0100 Subject: [PATCH 179/509] Move propertiesPlayback from window to state.py --- resources/lib/playbackutils.py | 11 +++++------ resources/lib/player.py | 2 +- resources/lib/state.py | 4 ++++ service.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index cf5dee55..d140feef 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -97,7 +97,6 @@ class PlaybackUtils(): startPos = max(playqueue.kodi_pl.getposition(), 0) self.currentPosition = startPos - propertiesPlayback = window('plex_playbackProps') == "true" introsPlaylist = False dummyPlaylist = False @@ -114,8 +113,8 @@ 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") + if not v.PLAYBACK_SETUP_DONE: + v.PLAYBACK_SETUP_DONE = True LOG.info("Setting up properties in playlist.") # Where will the player need to start? # Do we need to get trailers? @@ -232,9 +231,9 @@ class PlaybackUtils(): return result # We just skipped adding properties. Reset flag for next time. - elif propertiesPlayback: + elif v.PLAYBACK_SETUP_DONE: LOG.debug("Resetting properties playback flag.") - window('plex_playbackProps', clear=True) + v.PLAYBACK_SETUP_DONE = False # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref @@ -277,7 +276,7 @@ class PlaybackUtils(): Play all items contained in the xml passed in. Called by Plex Companion """ LOG.info("Playbackutils play_all called") - window('plex_playbackProps', value="true") + v.PLAYBACK_SETUP_DONE = True self.currentPosition = 0 for item in self.xml: api = API(item) diff --git a/resources/lib/player.py b/resources/lib/player.py index 63071e91..1df9bf24 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -263,12 +263,12 @@ class PKC_Player(Player): for item in ('plex_currently_playing_itemid', 'plex_customplaylist', 'plex_customplaylist.seektime', - 'plex_playbackProps', 'plex_forcetranscode'): window(item, clear=True) # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) state.PLEX_TRANSIENT_TOKEN = None + v.PLAYBACK_SETUP_DONE = False LOG.debug("Cleared playlist properties.") def onPlayBackEnded(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index 931a5227..a9dc091c 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -117,6 +117,10 @@ PLAYER_STATES = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Former playbackProps; used by playbackutils.py and set to True if initial +# playback setup has been done (and playbackutils will be called again +# subsequently) +PLAYBACK_SETUP_DONE = False # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/service.py b/service.py index 06dd1ab9..8f7e6a38 100644 --- a/service.py +++ b/service.py @@ -108,7 +108,7 @@ class Service(): "plex_online", "plex_serverStatus", "plex_onWake", "plex_kodiScan", "plex_shouldStop", "plex_dbScan", - "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", + "plex_initialScan", "plex_customplayqueue", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", From e0f1225c21513658aaa0087399d21c6e258d9666 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 6 Jan 2018 12:55:24 +0100 Subject: [PATCH 180/509] Move plex_playbackProbs to state.py --- resources/lib/playbackutils.py | 15 +++++++-------- resources/lib/player.py | 2 +- service.py | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index d140feef..03afcb8c 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -113,8 +113,8 @@ class PlaybackUtils(): # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. - if not v.PLAYBACK_SETUP_DONE: - v.PLAYBACK_SETUP_DONE = True + if not state.PLAYBACK_SETUP_DONE: + state.PLAYBACK_SETUP_DONE = True LOG.info("Setting up properties in playlist.") # Where will the player need to start? # Do we need to get trailers? @@ -139,7 +139,6 @@ class PlaybackUtils(): get_playlist_details_from_xml(playqueue, xml=xml) except KeyError: return - playqueue.items.append(playlist_item_from_xml(playqueue, xml[0])) if (not homeScreen and not seektime and sizePlaylist < 2 and window('plex_customplaylist') != "true" and @@ -199,7 +198,7 @@ class PlaybackUtils(): api.set_listitem_artwork(listitem) add_listitem_to_Kodi_playlist( playqueue, - self.currentPosition+1, + self.currentPosition, convert_PKC_to_listitem(listitem), file=playurl, kodi_item={'id': kodi_id, 'type': kodi_type}) @@ -207,7 +206,7 @@ class PlaybackUtils(): # Full metadata$ add_item_to_kodi_playlist( playqueue, - self.currentPosition+1, + self.currentPosition, kodi_id, kodi_type) self.currentPosition += 1 @@ -231,9 +230,9 @@ class PlaybackUtils(): return result # We just skipped adding properties. Reset flag for next time. - elif v.PLAYBACK_SETUP_DONE: + elif state.PLAYBACK_SETUP_DONE: LOG.debug("Resetting properties playback flag.") - v.PLAYBACK_SETUP_DONE = False + state.PLAYBACK_SETUP_DONE = False # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref @@ -276,7 +275,7 @@ class PlaybackUtils(): Play all items contained in the xml passed in. Called by Plex Companion """ LOG.info("Playbackutils play_all called") - v.PLAYBACK_SETUP_DONE = True + state.PLAYBACK_SETUP_DONE = True self.currentPosition = 0 for item in self.xml: api = API(item) diff --git a/resources/lib/player.py b/resources/lib/player.py index 1df9bf24..1e13d431 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -268,7 +268,7 @@ class PKC_Player(Player): # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) state.PLEX_TRANSIENT_TOKEN = None - v.PLAYBACK_SETUP_DONE = False + state.PLAYBACK_SETUP_DONE = False LOG.debug("Cleared playlist properties.") def onPlayBackEnded(self): diff --git a/service.py b/service.py index 8f7e6a38..06dd1ab9 100644 --- a/service.py +++ b/service.py @@ -108,7 +108,7 @@ class Service(): "plex_online", "plex_serverStatus", "plex_onWake", "plex_kodiScan", "plex_shouldStop", "plex_dbScan", - "plex_initialScan", "plex_customplayqueue", + "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", From e17824609abca8a2e3c199a18a6c6a635af9847c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 6 Jan 2018 15:19:12 +0100 Subject: [PATCH 181/509] Greatly simplify handling of PKC playqueues --- resources/lib/PlexCompanion.py | 40 ++- resources/lib/command_pipeline.py | 16 +- resources/lib/companion.py | 13 +- resources/lib/initialsetup.py | 106 ++++---- resources/lib/kodimonitor.py | 15 +- resources/lib/librarysync.py | 6 +- resources/lib/playback_starter.py | 55 ++-- resources/lib/playbackutils.py | 16 +- resources/lib/player.py | 1 - resources/lib/playqueue.py | 296 +++++++-------------- resources/lib/plexbmchelper/listener.py | 6 +- resources/lib/plexbmchelper/subscribers.py | 16 +- resources/lib/state.py | 11 +- resources/lib/userclient.py | 4 +- resources/lib/websocket_client.py | 9 +- service.py | 30 +-- 16 files changed, 259 insertions(+), 381 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 7e424166..653c9ca3 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -3,7 +3,7 @@ The Plex Companion master python file """ from logging import getLogger from threading import Thread -from Queue import Queue, Empty +from Queue import Empty from socket import SHUT_RDWR from urllib import urlencode @@ -19,6 +19,7 @@ import json_rpc as js import player import variables as v import state +import playqueue as PQ ############################################################################### @@ -32,9 +33,9 @@ class PlexCompanion(Thread): """ Plex Companion monitoring class. Invoke only once """ - def __init__(self, callback=None): + def __init__(self): LOG.info("----===## Starting PlexCompanion ##===----") - self.mgr = callback + # Init Plex Companion queue # Start GDM for server/client discovery self.client = plexgdm.plexgdm() self.client.clientDetails() @@ -42,7 +43,6 @@ class PlexCompanion(Thread): # kodi player instance self.player = player.PKC_Player() self.httpd = False - self.queue = None self.subscription_manager = None Thread.__init__(self) @@ -57,8 +57,9 @@ class PlexCompanion(Thread): api = API(xml[0]) if api.getType() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') - self.mgr.playqueue.init_playqueue_from_plex_children( - api.getRatingKey(), transient_token=data.get('token')) + PQ.init_playqueue_from_plex_children( + api.getRatingKey(), + transient_token=data.get('token')) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') params = { @@ -91,7 +92,7 @@ class PlexCompanion(Thread): # Get the playqueue ID _, container_key, query = ParseContainerKey(data['containerKey']) try: - playqueue = self.mgr.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) except KeyError: # E.g. Plex web does not supply the media type @@ -103,13 +104,13 @@ class PlexCompanion(Thread): LOG.error('Could not download Plex metadata') return api = API(xml[0]) - playqueue = self.mgr.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) if container_key == playqueue.id: LOG.info('Already know this playqueue - ignoring') playqueue.transient_token = data.get('token') else: - self.mgr.playqueue.update_playqueue_from_PMS( + PQ.update_playqueue_from_PMS( playqueue, playqueue_id=container_key, repeat=query.get('repeat'), @@ -121,7 +122,7 @@ class PlexCompanion(Thread): """ Plex Companion client adjusted audio or subtitle stream """ - playqueue = self.mgr.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) pos = js.get_position(playqueue.playlistid) if 'audioStreamID' in data: @@ -151,15 +152,13 @@ class PlexCompanion(Thread): plex_type = get_plextype_from_xml(xml) if plex_type is None: return - playqueue = self.mgr.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) playqueue.clear() return - playqueue = self.mgr.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - self.mgr.playqueue.update_playqueue_from_PMS( - playqueue, - data['playQueueID']) + PQ.update_playqueue_from_PMS(playqueue, data['playQueueID']) def _process_tasks(self, task): """ @@ -217,11 +216,9 @@ class PlexCompanion(Thread): # Start up instances request_mgr = httppersist.RequestMgr() - subscription_manager = subscribers.SubscriptionMgr( - request_mgr, self.player, self.mgr) + subscription_manager = subscribers.SubscriptionMgr(request_mgr, + self.player) self.subscription_manager = subscription_manager - queue = Queue(maxsize=100) - self.queue = queue if settings('plexCompanion') == 'true': # Start up httpd @@ -231,7 +228,6 @@ class PlexCompanion(Thread): httpd = listener.ThreadedHTTPServer( client, subscription_manager, - queue, ('', v.COMPANION_PORT), listener.MyHandler) httpd.timeout = 0.95 @@ -290,13 +286,13 @@ class PlexCompanion(Thread): LOG.warn(traceback.format_exc()) # See if there's anything we need to process try: - task = queue.get(block=False) + task = state.COMPANION_QUEUE.get(block=False) except Empty: pass else: # Got instructions, process them self._process_tasks(task) - queue.task_done() + state.COMPANION_QUEUE.task_done() # Don't sleep continue sleep(50) diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 92c6cc57..20d37d4a 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -2,7 +2,6 @@ ############################################################################### import logging from threading import Thread -from Queue import Queue from xbmc import sleep @@ -10,8 +9,7 @@ from utils import window, thread_methods import state ############################################################################### -log = logging.getLogger("PLEX."+__name__) - +LOG = logging.getLogger("PLEX." + __name__) ############################################################################### @@ -23,16 +21,10 @@ class Monitor_Window(Thread): Adjusts state.py accordingly """ - # Borg - multiple instances, shared state - def __init__(self, callback=None): - self.mgr = callback - self.playback_queue = Queue() - Thread.__init__(self) - def run(self): thread_stopped = self.thread_stopped - queue = self.playback_queue - log.info("----===## Starting Kodi_Play_Client ##===----") + queue = state.COMMAND_PIPELINE_QUEUE + LOG.info("----===## Starting Kodi_Play_Client ##===----") while not thread_stopped(): if window('plex_command'): value = window('plex_command') @@ -70,4 +62,4 @@ class Monitor_Window(Thread): sleep(50) # Put one last item into the queue to let playback_starter end queue.put(None) - log.info("----===## Kodi_Play_Client stopped ##===----") + LOG.info("----===## Kodi_Play_Client stopped ##===----") diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 0b24df4f..60aa29ce 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -6,7 +6,7 @@ from logging import getLogger from xbmc import Player from variables import ALEXA_TO_COMPANION -from playqueue import Playqueue +import playqueue as PQ from PlexFunctions import GetPlexKeyNumber import json_rpc as js import state @@ -29,9 +29,8 @@ def skip_to(params): LOG.debug('Skipping to playQueueItemID %s, plex_id %s', playqueue_item_id, plex_id) found = True - playqueues = Playqueue() for player in js.get_players().values(): - playqueue = playqueues.playqueues[player['playerid']] + playqueue = PQ.PLAYQUEUES[player['playerid']] for i, item in enumerate(playqueue.items): if item.id == playqueue_item_id: found = True @@ -57,7 +56,7 @@ def convert_alexa_to_companion(dictionary): del dictionary[key] -def process_command(request_path, params, queue=None): +def process_command(request_path, params): """ queue: Queue() of PlexCompanion.py """ @@ -67,12 +66,12 @@ def process_command(request_path, params, queue=None): if request_path == 'player/playback/playMedia': # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' - queue.put({ + state.COMPANION_QUEUE.put({ 'action': action, 'data': params }) elif request_path == 'player/playback/refreshPlayQueue': - queue.put({ + state.COMPANION_QUEUE.put({ 'action': 'refreshPlayQueue', 'data': params }) @@ -114,7 +113,7 @@ def process_command(request_path, params, queue=None): elif request_path == "player/navigation/back": js.input_back() elif request_path == "player/playback/setStreams": - queue.put({ + state.COMPANION_QUEUE.put({ 'action': 'setStreams', 'data': params }) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 13641603..92b1eafc 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- - ############################################################################### from logging import getLogger +from Queue import Queue import xbmc import xbmcgui @@ -15,10 +15,11 @@ from PlexAPI import PlexAPI from PlexFunctions import GetMachineIdentifier, get_PMS_settings import state from migration import check_migration +import playqueue as PQ ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -26,7 +27,7 @@ log = getLogger("PLEX."+__name__) class InitialSetup(): def __init__(self): - log.debug('Entering initialsetup class') + LOG.debug('Entering initialsetup class') self.doUtils = downloadutils.DownloadUtils().downloadUrl self.plx = PlexAPI() self.dialog = xbmcgui.Dialog() @@ -42,7 +43,7 @@ class InitialSetup(): # Token for the PMS, not plex.tv self.pms_token = settings('accessToken') if self.plexToken: - log.debug('Found a plex.tv token in the settings') + LOG.debug('Found a plex.tv token in the settings') def PlexTVSignIn(self): """ @@ -68,7 +69,7 @@ class InitialSetup(): chk = self.plx.CheckConnection('plex.tv', token=self.plexToken) if chk in (401, 403): # HTTP Error: unauthorized. Token is no longer valid - log.info('plex.tv connection returned HTTP %s' % str(chk)) + LOG.info('plex.tv connection returned HTTP %s', str(chk)) # Delete token in the settings settings('plexToken', value='') settings('plexLogin', value='') @@ -77,12 +78,12 @@ class InitialSetup(): answer = self.PlexTVSignIn() elif chk is False or chk >= 400: # Problems connecting to plex.tv. Network or internet issue? - log.info('Problems connecting to plex.tv; connection returned ' - 'HTTP %s' % str(chk)) + LOG.info('Problems connecting to plex.tv; connection returned ' + 'HTTP %s', str(chk)) self.dialog.ok(lang(29999), lang(39010)) answer = False else: - log.info('plex.tv connection with token successful') + LOG.info('plex.tv connection with token successful') settings('plex_status', value=lang(39227)) # Refresh the info from Plex.tv xml = self.doUtils('https://plex.tv/users/account', @@ -91,14 +92,14 @@ class InitialSetup(): try: self.plexLogin = xml.attrib['title'] except (AttributeError, KeyError): - log.error('Failed to update Plex info from plex.tv') + LOG.error('Failed to update Plex info from plex.tv') else: settings('plexLogin', value=self.plexLogin) home = 'true' if xml.attrib.get('home') == '1' else 'false' settings('plexhome', value=home) settings('plexAvatar', value=xml.attrib.get('thumb')) settings('plexHomeSize', value=xml.attrib.get('homeSize', '1')) - log.info('Updated Plex info from plex.tv') + LOG.info('Updated Plex info from plex.tv') return answer def CheckPMS(self): @@ -114,24 +115,24 @@ class InitialSetup(): answer = True chk = self.plx.CheckConnection(self.server, verifySSL=False) if chk is False: - log.warn('Could not reach PMS %s' % self.server) + LOG.warn('Could not reach PMS %s', self.server) answer = False if answer is True and not self.serverid: - log.info('No PMS machineIdentifier found for %s. Trying to ' - 'get the PMS unique ID' % self.server) + LOG.info('No PMS machineIdentifier found for %s. Trying to ' + 'get the PMS unique ID', self.server) self.serverid = GetMachineIdentifier(self.server) if self.serverid is None: - log.warn('Could not retrieve machineIdentifier') + LOG.warn('Could not retrieve machineIdentifier') answer = False else: settings('plex_machineIdentifier', value=self.serverid) elif answer is True: tempServerid = GetMachineIdentifier(self.server) if tempServerid != self.serverid: - log.warn('The current PMS %s was expected to have a ' + LOG.warn('The current PMS %s was expected to have a ' 'unique machineIdentifier of %s. But we got ' - '%s. Pick a new server to be sure' - % (self.server, self.serverid, tempServerid)) + '%s. Pick a new server to be sure', + self.server, self.serverid, tempServerid) answer = False return answer @@ -142,7 +143,7 @@ class InitialSetup(): self.plx.discoverPMS(xbmc.getIPAddress(), plexToken=self.plexToken) serverlist = self.plx.returnServerList(self.plx.g_PMS) - log.debug('PMS serverlist: %s' % serverlist) + LOG.debug('PMS serverlist: %s', serverlist) return serverlist def _checkServerCon(self, server): @@ -209,7 +210,7 @@ class InitialSetup(): try: xml.attrib except AttributeError: - log.error('Could not get PMS settings for %s' % url) + LOG.error('Could not get PMS settings for %s', url) return for entry in xml: if entry.attrib.get('id', '') == 'allowMediaDeletion': @@ -236,9 +237,9 @@ class InitialSetup(): server = item if server is None: name = settings('plex_servername') - log.warn('The PMS you have used before with a unique ' + LOG.warn('The PMS you have used before with a unique ' 'machineIdentifier of %s and name %s is ' - 'offline' % (self.serverid, name)) + 'offline', self.serverid, name) return chk = self._checkServerCon(server) if chk == 504 and httpsUpdated is False: @@ -247,8 +248,8 @@ class InitialSetup(): httpsUpdated = True continue if chk == 401: - log.warn('Not yet authorized for Plex server %s' - % server['name']) + LOG.warn('Not yet authorized for Plex server %s', + server['name']) if self.CheckPlexTVSignIn() is True: if checkedPlexTV is False: # Try again @@ -256,7 +257,7 @@ class InitialSetup(): httpsUpdated = False continue else: - log.warn('Not authorized even though we are signed ' + LOG.warn('Not authorized even though we are signed ' ' in to plex.tv correctly') self.dialog.ok(lang(29999), '%s %s' % (lang(39214), @@ -266,11 +267,11 @@ class InitialSetup(): return # Problems connecting elif chk >= 400 or chk is False: - log.warn('Problems connecting to server %s. chk is %s' - % (server['name'], chk)) + LOG.warn('Problems connecting to server %s. chk is %s', + server['name'], chk) return - log.info('We found a server to automatically connect to: %s' - % server['name']) + LOG.info('We found a server to automatically connect to: %s', + server['name']) return server def _UserPickPMS(self): @@ -285,7 +286,7 @@ class InitialSetup(): serverlist = self._getServerList() # Exit if no servers found if len(serverlist) == 0: - log.warn('No plex media servers found!') + LOG.warn('No plex media servers found!') self.dialog.ok(lang(29999), lang(39011)) return # Get a nicer list @@ -322,8 +323,8 @@ class InitialSetup(): continue httpsUpdated = False if chk == 401: - log.warn('Not yet authorized for Plex server %s' - % server['name']) + LOG.warn('Not yet authorized for Plex server %s', + server['name']) # Please sign in to plex.tv self.dialog.ok(lang(29999), lang(39013) + server['name'], @@ -370,7 +371,7 @@ class InitialSetup(): scheme = server['scheme'] settings('ipaddress', server['ip']) settings('port', server['port']) - log.debug("Setting SSL verify to false, because server is " + LOG.debug("Setting SSL verify to false, because server is " "local") settings('sslverify', 'false') else: @@ -378,7 +379,7 @@ class InitialSetup(): scheme = baseURL[0] settings('ipaddress', baseURL[1].replace('//', '')) settings('port', baseURL[2]) - log.debug("Setting SSL verify to true, because server is not " + LOG.debug("Setting SSL verify to true, because server is not " "local") settings('sslverify', 'true') @@ -387,10 +388,10 @@ class InitialSetup(): else: settings('https', 'false') # And finally do some logging - log.debug("Writing to Kodi user settings file") - log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s " - % (server['machineIdentifier'], server['ip'], - server['port'], server['scheme'])) + LOG.debug("Writing to Kodi user settings file") + LOG.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s ", + server['machineIdentifier'], server['ip'], server['port'], + server['scheme']) def setup(self): """ @@ -399,14 +400,14 @@ class InitialSetup(): Check server, user, direct paths, music, direct stream if not direct path. """ - log.info("Initial setup called.") + LOG.info("Initial setup called.") dialog = self.dialog # Get current Kodi video cache setting cache, _ = advancedsettings_xml(['cache', 'memorysize']) # Kodi default cache if no setting is set cache = str(cache.text) if cache is not None else '20971520' - log.info('Current Kodi video memory cache in bytes: %s', cache) + LOG.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) # Disable foreground "Loading media information from files" # (still used by Kodi, even though the Wiki says otherwise) @@ -420,13 +421,20 @@ class InitialSetup(): if self.plexToken and self.myplexlogin: self.CheckPlexTVSignIn() + # Initialize the PKC playqueues + PQ.init_playqueues() + # Init some Queues() + state.COMMAND_PIPELINE_QUEUE = Queue() + state.COMPANION_QUEUE = Queue(maxsize=100) + state.WEBSOCKET_QUEUE = Queue() + # If a Plex server IP has already been set # return only if the right machine identifier is found if self.server: - log.info("PMS is already set: %s. Checking now..." % self.server) + LOG.info("PMS is already set: %s. Checking now...", self.server) if self.CheckPMS(): - log.info("Using PMS %s with machineIdentifier %s" - % (self.server, self.serverid)) + LOG.info("Using PMS %s with machineIdentifier %s", + self.server, self.serverid) self._write_PMS_settings(self.server, self.pms_token) return @@ -452,19 +460,19 @@ class InitialSetup(): lang(39028), nolabel="Addon (Default)", yeslabel="Native (Direct Paths)"): - log.debug("User opted to use direct paths.") + LOG.debug("User opted to use direct paths.") settings('useDirectPaths', value="1") state.DIRECT_PATHS = True # Are you on a system where you would like to replace paths # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) if dialog.yesno(heading=lang(29999), line1=lang(39033)): - log.debug("User chose to replace paths with smb") + LOG.debug("User chose to replace paths with smb") else: settings('replaceSMB', value="false") # complete replace all original Plex library paths with custom SMB if dialog.yesno(heading=lang(29999), line1=lang(39043)): - log.debug("User chose custom smb paths") + LOG.debug("User chose custom smb paths") settings('remapSMB', value="true") # Please enter your custom smb paths in the settings under # "Sync Options" and then restart Kodi @@ -475,22 +483,22 @@ class InitialSetup(): if dialog.yesno(heading=lang(29999), line1=lang(39029), line2=lang(39030)): - log.debug("Presenting network credentials dialog.") + LOG.debug("Presenting network credentials dialog.") from utils import passwordsXML passwordsXML() # Disable Plex music? if dialog.yesno(heading=lang(29999), line1=lang(39016)): - log.debug("User opted to disable Plex music library.") + LOG.debug("User opted to disable Plex music library.") settings('enableMusic', value="false") # Download additional art from FanArtTV if dialog.yesno(heading=lang(29999), line1=lang(39061)): - log.debug("User opted to use FanArtTV") + LOG.debug("User opted to use FanArtTV") settings('FanartTV', value="true") # Do you want to replace your custom user ratings with an indicator of # how many versions of a media item you posses? if dialog.yesno(heading=lang(29999), line1=lang(39718)): - log.debug("User opted to replace user ratings with version number") + LOG.debug("User opted to replace user ratings with version number") settings('indicate_media_versions', value="true") # If you use several Plex libraries of one kind, e.g. "Kids Movies" and diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index d250841b..17bf28eb 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -16,6 +16,7 @@ import json_rpc as js import playlist_func as PL import state import variables as v +import playqueue as PQ ############################################################################### @@ -51,10 +52,8 @@ class KodiMonitor(Monitor): """ PKC implementation of the Kodi Monitor class. Invoke only once. """ - def __init__(self, callback): - self.mgr = callback + def __init__(self): self.xbmcplayer = Player() - self.playqueue = self.mgr.playqueue Monitor.__init__(self) LOG.info("Kodi monitor started.") @@ -198,7 +197,7 @@ class KodiMonitor(Monitor): } Will NOT be called if playback initiated by Kodi widgets """ - playqueue = self.playqueue.playqueues[data['playlistid']] + playqueue = PQ.PLAYQUEUES[data['playlistid']] # Did PKC cause this add? Then lets not do anything if playqueue.is_kodi_onadd() is False: LOG.debug('PKC added this item to the playqueue - ignoring') @@ -228,7 +227,7 @@ class KodiMonitor(Monitor): u'position': 0 } """ - playqueue = self.playqueue.playqueues[data['playlistid']] + playqueue = PQ.PLAYQUEUES[data['playlistid']] # Did PKC cause this add? Then lets not do anything if playqueue.is_kodi_onremove() is False: LOG.debug('PKC removed this item already from playqueue - ignoring') @@ -249,7 +248,7 @@ class KodiMonitor(Monitor): u'playlistid': 1, } """ - playqueue = self.playqueue.playqueues[data['playlistid']] + playqueue = PQ.PLAYQUEUES[data['playlistid']] if playqueue.is_kodi_onclear() is False: LOG.debug('PKC already cleared the playqueue - ignoring') return @@ -317,7 +316,7 @@ class KodiMonitor(Monitor): LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) # Check whether we need to init our playqueues (e.g. direct play) init = False - playqueue = self.playqueue.playqueues[playerid] + playqueue = PQ.PLAYQUEUES[playerid] try: playqueue.items[info['position']] except IndexError: @@ -340,7 +339,7 @@ class KodiMonitor(Monitor): container_key = None if info['playlistid'] != -1: # -1 is Kodi's answer if there is no playlist - container_key = self.playqueue.playqueues[playerid].id + container_key = PQ.PLAYQUEUES[playerid].id if container_key is not None: container_key = '/playQueues/%s' % container_key elif plex_id is not None: diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 6943ec65..28d173b0 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -43,9 +43,7 @@ log = getLogger("PLEX."+__name__) class LibrarySync(Thread): """ """ - def __init__(self, callback=None): - self.mgr = callback - + def __init__(self): self.itemsToProcess = [] self.sessionKeys = [] self.fanartqueue = Queue.Queue() @@ -1527,7 +1525,7 @@ class LibrarySync(Thread): oneDay = 60*60*24 # Link to Websocket queue - queue = self.mgr.ws.queue + queue = state.WEBSOCKET_QUEUE startupComplete = False self.views = [] diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index c097c4ca..901e75e6 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -12,7 +12,7 @@ from playbackutils import PlaybackUtils from utils import window from PlexFunctions import GetPlexMetadata from PlexAPI import API -from playqueue import LOCK +import playqueue as PQ import variables as v from downloadutils import DownloadUtils from PKC_listitem import convert_PKC_to_listitem @@ -21,7 +21,8 @@ from context_entry import ContextMenu import state ############################################################################### -log = getLogger("PLEX."+__name__) + +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -30,26 +31,21 @@ 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)) + LOG.info("Process_play called with plex_id %s, kodi_id %s", + plex_id, kodi_id) if not state.AUTHENTICATED: - log.error('Not yet authenticated for PMS, abort starting playback') + LOG.error('Not yet authenticated for PMS, abort starting playback') # Todo: Warn user with dialog return xml = GetPlexMetadata(plex_id) try: xml[0].attrib except (IndexError, TypeError, AttributeError): - log.error('Could not get a PMS xml for plex id %s' % plex_id) + LOG.error('Could not get a PMS xml for plex id %s', plex_id) return api = API(xml[0]) if api.getType() == v.PLEX_TYPE_PHOTO: @@ -60,15 +56,14 @@ class Playback_Starter(Thread): result.listitem = listitem else: # Video and Music - playqueue = self.playqueue.get_playqueue_from_type( + playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - with LOCK: + with PQ.LOCK: result = PlaybackUtils(xml, playqueue).play( plex_id, kodi_id, xml.attrib.get('librarySectionUUID')) - log.info('Done process_play, playqueues: %s' - % self.playqueue.playqueues) + LOG.info('Done process_play, playqueues: %s', PQ.PLAYQUEUES) return result def process_plex_node(self, url, viewOffset, directplay=False, @@ -77,8 +72,8 @@ class Playback_Starter(Thread): Called for Plex directories or redirect for playback (e.g. trailers, clips, watchlater) """ - log.info('process_plex_node called with url: %s, viewOffset: %s' - % (url, viewOffset)) + LOG.info('process_plex_node called with url: %s, viewOffset: %s', + url, viewOffset) # Plex redirect, e.g. watch later. Need to get actual URLs if url.startswith('http') or url.startswith('{server}'): xml = DownloadUtils().downloadUrl(url) @@ -87,7 +82,7 @@ class Playback_Starter(Thread): try: xml[0].attrib except: - log.error('Could not download PMS metadata') + LOG.error('Could not download PMS metadata') return if viewOffset != '0': try: @@ -96,7 +91,7 @@ class Playback_Starter(Thread): pass else: window('plex_customplaylist.seektime', value=str(viewOffset)) - log.info('Set resume point to %s' % str(viewOffset)) + LOG.info('Set resume point to %s', viewOffset) api = API(xml[0]) typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()] if node is True: @@ -110,10 +105,10 @@ class Playback_Starter(Thread): try: kodi_id = plexdb_item[0] except TypeError: - log.info('Couldnt find item %s in Kodi db' - % api.getRatingKey()) - playqueue = self.playqueue.get_playqueue_from_type(typus) - with LOCK: + LOG.info('Couldnt find item %s in Kodi db', + api.getRatingKey()) + playqueue = PQ.get_playqueue_from_type(typus) + with PQ.LOCK: result = PlaybackUtils(xml, playqueue).play( plex_id, kodi_id=kodi_id, @@ -130,7 +125,7 @@ class Playback_Starter(Thread): _, params = item.split('?', 1) params = dict(parse_qsl(params)) mode = params.get('mode') - log.debug('Received mode: %s, params: %s' % (mode, params)) + LOG.debug('Received mode: %s, params: %s', mode, params) try: if mode == 'play': result = self.process_play(params.get('id'), @@ -147,18 +142,18 @@ class Playback_Starter(Thread): ContextMenu() result = Playback_Successful() except: - log.error('Error encountered for mode %s, params %s' - % (mode, params)) + LOG.error('Error encountered for mode %s, params %s', + mode, params) import traceback - log.error(traceback.format_exc()) + LOG.error(traceback.format_exc()) # Let default.py know! pickle_me(None) else: pickle_me(result) def run(self): - queue = self.mgr.command_pipeline.playback_queue - log.info("----===## Starting Playback_Starter ##===----") + queue = state.COMMAND_PIPELINE_QUEUE + LOG.info("----===## Starting Playback_Starter ##===----") while True: item = queue.get() if item is None: @@ -167,4 +162,4 @@ class Playback_Starter(Thread): else: self.triage(item) queue.task_done() - log.info("----===## Playback_Starter stopped ##===----") + LOG.info("----===## Playback_Starter stopped ##===----") diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 03afcb8c..aedb0e6f 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -97,6 +97,7 @@ class PlaybackUtils(): startPos = max(playqueue.kodi_pl.getposition(), 0) self.currentPosition = startPos + propertiesPlayback = window('plex_playbackProps') == "true" introsPlaylist = False dummyPlaylist = False @@ -113,8 +114,8 @@ class PlaybackUtils(): # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. - if not state.PLAYBACK_SETUP_DONE: - state.PLAYBACK_SETUP_DONE = True + if not propertiesPlayback: + window('plex_playbackProps', value="true") LOG.info("Setting up properties in playlist.") # Where will the player need to start? # Do we need to get trailers? @@ -139,7 +140,6 @@ class PlaybackUtils(): get_playlist_details_from_xml(playqueue, xml=xml) except KeyError: return - if (not homeScreen and not seektime and sizePlaylist < 2 and window('plex_customplaylist') != "true" and not contextmenu_play): @@ -198,7 +198,7 @@ class PlaybackUtils(): api.set_listitem_artwork(listitem) add_listitem_to_Kodi_playlist( playqueue, - self.currentPosition, + self.currentPosition+1, convert_PKC_to_listitem(listitem), file=playurl, kodi_item={'id': kodi_id, 'type': kodi_type}) @@ -206,7 +206,7 @@ class PlaybackUtils(): # Full metadata$ add_item_to_kodi_playlist( playqueue, - self.currentPosition, + self.currentPosition+1, kodi_id, kodi_type) self.currentPosition += 1 @@ -230,9 +230,9 @@ class PlaybackUtils(): return result # We just skipped adding properties. Reset flag for next time. - elif state.PLAYBACK_SETUP_DONE: + elif propertiesPlayback: LOG.debug("Resetting properties playback flag.") - state.PLAYBACK_SETUP_DONE = False + window('plex_playbackProps', clear=True) # SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref @@ -275,7 +275,7 @@ class PlaybackUtils(): Play all items contained in the xml passed in. Called by Plex Companion """ LOG.info("Playbackutils play_all called") - state.PLAYBACK_SETUP_DONE = True + window('plex_playbackProps', value="true") self.currentPosition = 0 for item in self.xml: api = API(item) diff --git a/resources/lib/player.py b/resources/lib/player.py index 1e13d431..be006ff2 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -268,7 +268,6 @@ class PKC_Player(Player): # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) state.PLEX_TRANSIENT_TOKEN = None - state.PLAYBACK_SETUP_DONE = False LOG.debug("Cleared playlist properties.") def onPlayBackEnded(self): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 416078ca..40804803 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -4,9 +4,9 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly from logging import getLogger from threading import RLock, Thread -from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO +from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, thread_methods +from utils import window import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API @@ -20,218 +20,126 @@ LOG = getLogger("PLEX." + __name__) # lock used for playqueue manipulations LOCK = RLock() PLUGIN = 'plugin://%s' % v.ADDON_ID + +# Our PKC playqueues (3 instances of Playqueue_Object()) +PLAYQUEUES = [] ############################################################################### -@thread_methods(add_suspends=['PMS_STATUS']) -class Playqueue(Thread): +def init_playqueues(): """ - Monitors Kodi's playqueues for changes on the Kodi side + Call this once on startup to initialize the PKC playqueue objects in + the list PLAYQUEUES """ - # Borg - multiple instances, shared state - __shared_state = {} - playqueues = None - - def __init__(self, callback=None): - self.__dict__ = self.__shared_state - if self.playqueues is not None: - LOG.debug('Playqueue thread has already been initialized') - Thread.__init__(self) - return - self.mgr = callback - - # Initialize Kodi playqueues - with LOCK: - self.playqueues = [] + if PLAYQUEUES: + LOG.debug('Playqueues have already been initialized') + return + # Initialize Kodi playqueues + with LOCK: + for i in (0, 1, 2): + # Just in case the Kodi response is not sorted correctly for queue in js.get_playlists(): + if queue['playlistid'] != i: + continue playqueue = PL.Playqueue_Object() - playqueue.playlistid = queue['playlistid'] + playqueue.playlistid = i playqueue.type = queue['type'] # Initialize each Kodi playlist - if playqueue.type == 'audio': + if playqueue.type == v.KODI_TYPE_AUDIO: playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC) - elif playqueue.type == 'video': + elif playqueue.type == v.KODI_TYPE_VIDEO: playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) else: # Currently, only video or audio playqueues available playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) # Overwrite 'picture' with 'photo' playqueue.type = v.KODI_TYPE_PHOTO - self.playqueues.append(playqueue) - # sort the list by their playlistid, just in case - self.playqueues = sorted( - self.playqueues, key=lambda i: i.playlistid) - LOG.debug('Initialized the Kodi play queues: %s', self.playqueues) - Thread.__init__(self) + PLAYQUEUES.append(playqueue) + LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES) - def get_playqueue_from_type(self, typus): - """ - Returns the playqueue according to the typus ('video', 'audio', - 'picture') passed in - """ - with LOCK: - for playqueue in self.playqueues: - if playqueue.type == typus: - break - else: - raise ValueError('Wrong playlist type passed in: %s' % typus) - return playqueue - def init_playqueue_from_plex_children(self, plex_id, transient_token=None): - """ - Init a new playqueue e.g. from an album. Alexa does this - - Returns the Playlist_Object - """ - xml = GetAllPlexChildren(plex_id) - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not download the PMS xml for %s', plex_id) - return - playqueue = self.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - playqueue.clear() - for i, child in enumerate(xml): - api = API(child) - PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) - playqueue.plex_transient_token = transient_token - LOG.debug('Firing up Kodi player') - Player().play(playqueue.kodi_pl, None, False, 0) +def get_playqueue_from_type(typus): + """ + Returns the playqueue according to the typus ('video', 'audio', + 'picture') passed in + """ + with LOCK: + for playqueue in PLAYQUEUES: + if playqueue.type == typus: + break + else: + raise ValueError('Wrong playlist type passed in: %s' % typus) return playqueue - def update_playqueue_from_PMS(self, - playqueue, - playqueue_id=None, - repeat=None, - offset=None, - transient_token=None): - """ - Completely updates the Kodi playqueue with the new Plex playqueue. Pass - in playqueue_id if we need to fetch a new playqueue - repeat = 0, 1, 2 - offset = time offset in Plextime (milliseconds) - """ - LOG.info('New playqueue %s received from Plex companion with offset ' - '%s, repeat %s', playqueue_id, offset, repeat) - # Safe transient token from being deleted - if transient_token is None: - transient_token = playqueue.plex_transient_token - with LOCK: - xml = PL.get_PMS_playlist(playqueue, playqueue_id) - playqueue.clear() - try: - PL.get_playlist_details_from_xml(playqueue, xml) - except KeyError: - LOG.error('Could not get playqueue ID %s', playqueue_id) - return - playqueue.repeat = 0 if not repeat else int(repeat) - playqueue.plex_transient_token = transient_token - PlaybackUtils(xml, playqueue).play_all() - window('plex_customplaylist', value="true") - if offset not in (None, "0"): - window('plex_customplaylist.seektime', - str(ConvertPlexToKodiTime(offset))) - for startpos, item in enumerate(playqueue.items): - if item.id == playqueue.selectedItemID: - break - else: - startpos = 0 - # Start playback. Player does not return in time - LOG.debug('Playqueues after Plex Companion update are now: %s', - self.playqueues) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, - None, - False, - startpos)) - thread.setDaemon(True) - thread.start() +def init_playqueue_from_plex_children(plex_id, transient_token=None): + """ + Init a new playqueue e.g. from an album. Alexa does this - def _compare_playqueues(self, playqueue, new): - """ - Used to poll the Kodi playqueue and update the Plex playqueue if needed - """ - old = list(playqueue.items) - index = list(range(0, len(old))) - LOG.debug('Comparing new Kodi playqueue %s with our play queue %s', - new, old) - if self.thread_stopped(): - # Chances are that we got an empty Kodi playlist due to - # Kodi exit + Returns the Playlist_Object + """ + xml = GetAllPlexChildren(plex_id) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download the PMS xml for %s', plex_id) + return + playqueue = get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) + playqueue.clear() + for i, child in enumerate(xml): + api = API(child) + PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) + playqueue.plex_transient_token = transient_token + LOG.debug('Firing up Kodi player') + Player().play(playqueue.kodi_pl, None, False, 0) + return playqueue + + +def update_playqueue_from_PMS(playqueue, + playqueue_id=None, + repeat=None, + offset=None, + transient_token=None): + """ + Completely updates the Kodi playqueue with the new Plex playqueue. Pass + in playqueue_id if we need to fetch a new playqueue + + repeat = 0, 1, 2 + offset = time offset in Plextime (milliseconds) + """ + LOG.info('New playqueue %s received from Plex companion with offset ' + '%s, repeat %s', playqueue_id, offset, repeat) + # Safe transient token from being deleted + if transient_token is None: + transient_token = playqueue.plex_transient_token + with LOCK: + xml = PL.get_PMS_playlist(playqueue, playqueue_id) + playqueue.clear() + try: + PL.get_playlist_details_from_xml(playqueue, xml) + except KeyError: + LOG.error('Could not get playqueue ID %s', playqueue_id) return - for i, new_item in enumerate(new): - if (new_item['file'].startswith('plugin://') and - not new_item['file'].startswith(PLUGIN)): - # Ignore new media added by other addons - continue - for j, old_item in enumerate(old): - try: - if (old_item.file.startswith('plugin://') and - not old_item['file'].startswith(PLUGIN)): - # Ignore media by other addons - continue - except (TypeError, AttributeError): - # were not passed a filename; ignore - pass - if new_item.get('id') is None: - identical = old_item.file == new_item['file'] - else: - identical = (old_item.kodi_id == new_item['id'] and - old_item.kodi_type == new_item['type']) - if j == 0 and identical: - del old[j], index[j] - break - elif identical: - LOG.debug('Detected playqueue item %s moved to position %s', - i+j, i) - PL.move_playlist_item(playqueue, i + j, i) - del old[j], index[j] - break - else: - LOG.debug('Detected new Kodi element at position %s: %s ', - i, new_item) - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, - kodi_item=new_item) - else: - PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=new_item) - for j in range(i, len(index)): - index[j] += 1 - for i in reversed(index): - LOG.debug('Detected deletion of playqueue element at pos %s', i) - PL.delete_playlist_item_from_PMS(playqueue, i) - LOG.debug('Done comparing playqueues') - - def run(self): - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended - 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(js.playlist_get_items(playqueue.id)): - 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 thread_stopped(): - while thread_suspended(): - if thread_stopped(): - break - sleep(1000) - # with LOCK: - # for playqueue in self.playqueues: - # kodi_playqueue = js.playlist_get_items(playqueue.id) - # 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) - # # Still sleep a bit so Kodi does not become - # # unresponsive - # sleep(10) - # continue - sleep(200) - LOG.info("----===## PlayQueue client stopped ##===----") + playqueue.repeat = 0 if not repeat else int(repeat) + playqueue.plex_transient_token = transient_token + PlaybackUtils(xml, playqueue).play_all() + window('plex_customplaylist', value="true") + if offset not in (None, "0"): + window('plex_customplaylist.seektime', + str(ConvertPlexToKodiTime(offset))) + for startpos, item in enumerate(playqueue.items): + if item.id == playqueue.selectedItemID: + break + else: + startpos = 0 + # Start playback. Player does not return in time + LOG.debug('Playqueues after Plex Companion update are now: %s', + PLAYQUEUES) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, + None, + False, + startpos)) + thread.setDaemon(True) + thread.start() diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 2a03a432..d4aa46bc 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -9,7 +9,6 @@ from urlparse import urlparse, parse_qs from xbmc import sleep from companion import process_command -from utils import window import json_rpc as js from clientinfo import getXArgsDeviceInfo import variables as v @@ -154,7 +153,7 @@ class MyHandler(BaseHTTPRequestHandler): sub_mgr.remove_subscriber(uuid) else: # Throw it to companion.py - process_command(request_path, params, self.server.queue) + process_command(request_path, params) self.response('', getXArgsDeviceInfo(include_token=False)) @@ -164,7 +163,7 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ daemon_threads = True - def __init__(self, client, subscription_manager, queue, *args, **kwargs): + def __init__(self, client, subscription_manager, *args, **kwargs): """ client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to- date serverlist without instantiating anything @@ -173,5 +172,4 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ self.client = client self.subscription_manager = subscription_manager - self.queue = queue HTTPServer.__init__(self, *args, **kwargs) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index aea7e0cd..3ba4ebed 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -10,6 +10,7 @@ from utils import window, kodi_time_to_millis, Lock_Function import state import variables as v import json_rpc as js +import playqueue as PQ ############################################################################### @@ -111,7 +112,7 @@ class SubscriptionMgr(object): """ Manages Plex companion subscriptions """ - def __init__(self, request_mgr, player, mgr): + def __init__(self, request_mgr, player): self.serverlist = [] self.subscribers = {} self.info = {} @@ -124,11 +125,8 @@ class SubscriptionMgr(object): self.lastplayers = {} self.xbmcplayer = player - self.playqueue = mgr.playqueue self.request_mgr = request_mgr - - def _server_by_host(self, host): if len(self.serverlist) == 1: return self.serverlist[0] @@ -180,7 +178,7 @@ class SubscriptionMgr(object): def _timeline_dict(self, player, ptype): playerid = player['playerid'] info = state.PLAYER_STATES[playerid] - playqueue = self.playqueue.playqueues[playerid] + playqueue = PQ.PLAYQUEUES[playerid] pos = info['position'] try: item = playqueue.items[pos] @@ -284,7 +282,7 @@ class SubscriptionMgr(object): stream_type: 'video', 'audio', 'subtitle' """ - playqueue = self.playqueue.playqueues[playerid] + playqueue = PQ.PLAYQUEUES[playerid] info = state.PLAYER_STATES[playerid] return playqueue.items[info['position']].plex_stream_index( info[STREAM_DETAILS[stream_type]]['index'], stream_type) @@ -306,7 +304,7 @@ class SubscriptionMgr(object): """ for player in players.values(): info = state.PLAYER_STATES[player['playerid']] - playqueue = self.playqueue.playqueues[player['playerid']] + playqueue = PQ.PLAYQUEUES[player['playerid']] try: item = playqueue.items[info['position']] except IndexError: @@ -362,7 +360,7 @@ class SubscriptionMgr(object): def _get_pms_params(self, playerid): info = state.PLAYER_STATES[playerid] - playqueue = self.playqueue.playqueues[playerid] + playqueue = PQ.PLAYQUEUES[playerid] try: item = playqueue.items[info['position']] except IndexError: @@ -386,7 +384,7 @@ class SubscriptionMgr(object): def _send_pms_notification(self, playerid, params): serv = self._server_by_host(self.server) - playqueue = self.playqueue.playqueues[playerid] + playqueue = PQ.PLAYQUEUES[playerid] xargs = params_pms() xargs.update(params) if state.PLEX_TRANSIENT_TOKEN: diff --git a/resources/lib/state.py b/resources/lib/state.py index a9dc091c..a964ee51 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -75,6 +75,13 @@ PLEX_USER_ID = None # another user playing something! Token identifies user PLEX_TRANSIENT_TOKEN = None +# Plex Companion Queue() +COMPANION_QUEUE = None +# Command Pipeline Queue() +COMMAND_PIPELINE_QUEUE = None +# Websocket_client queue to communicate with librarysync +WEBSOCKET_QUEUE = None + # Kodi player states - here, initial values are set PLAYER_STATES = { 1: { @@ -117,10 +124,6 @@ PLAYER_STATES = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} -# Former playbackProps; used by playbackutils.py and set to True if initial -# playback setup has been done (and playbackutils will be called again -# subsequently) -PLAYBACK_SETUP_DONE = False # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 17104d63..9e94598a 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -30,10 +30,8 @@ class UserClient(Thread): # Borg - multiple instances, shared state __shared_state = {} - def __init__(self, callback=None): + def __init__(self): self.__dict__ = self.__shared_state - if callback is not None: - self.mgr = callback self.auth = True self.retry = 0 diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 748edfc0..dc42cb21 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -6,7 +6,6 @@ import websocket from json import loads import xml.etree.ElementTree as etree from threading import Thread -from Queue import Queue from ssl import CERT_NONE from xbmc import sleep @@ -165,9 +164,6 @@ class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion """ - # Communication with librarysync - queue = Queue() - def getUri(self): server = window('pms_server') # Get the appropriate prefix for the websocket @@ -221,7 +217,7 @@ class PMS_Websocket(WebSocket): % self.__class__.__name__) else: # Put PMS message on queue and let libsync take care of it - self.queue.put(message) + state.WEBSOCKET_QUEUE.put(message) def IOError_response(self): log.warn("Repeatedly could not connect to PMS, " @@ -271,8 +267,7 @@ class Alexa_Websocket(WebSocket): % self.__class__.__name__) return process_command(message.attrib['path'][1:], - message.attrib, - queue=self.mgr.plexCompanion.queue) + message.attrib) def IOError_response(self): pass diff --git a/service.py b/service.py index 06dd1ab9..246c2daa 100644 --- a/service.py +++ b/service.py @@ -36,7 +36,6 @@ from librarysync import LibrarySync import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket import downloadutils -from playqueue import Playqueue import clientinfo import PlexAPI @@ -80,14 +79,12 @@ class Service(): ws = None library = None plexCompanion = None - playqueue = None user_running = False ws_running = False alexa_running = False library_running = False plexCompanion_running = False - playqueue_running = False kodimonitor_running = False playback_starter_running = False image_cache_thread_running = False @@ -145,21 +142,20 @@ class Service(): monitor = self.monitor kodiProfile = v.KODI_PROFILE - # Detect playback start early on - self.command_pipeline = Monitor_Window(self) - self.command_pipeline.start() - # Server auto-detect initialsetup.InitialSetup().setup() + # Detect playback start early on + self.command_pipeline = Monitor_Window() + self.command_pipeline.start() + # Initialize important threads, handing over self for callback purposes - self.user = UserClient(self) - self.ws = PMS_Websocket(self) - self.alexa = Alexa_Websocket(self) - self.library = LibrarySync(self) - self.plexCompanion = PlexCompanion(self) - self.playqueue = Playqueue(self) - self.playback_starter = Playback_Starter(self) + self.user = UserClient() + self.ws = PMS_Websocket() + self.alexa = Alexa_Websocket() + self.library = LibrarySync() + self.plexCompanion = PlexCompanion() + self.playback_starter = Playback_Starter() if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() @@ -200,11 +196,7 @@ class Service(): time=2000, sound=False) # Start monitoring kodi events - self.kodimonitor_running = KodiMonitor(self) - # Start playqueue client - if not self.playqueue_running: - self.playqueue_running = True - self.playqueue.start() + self.kodimonitor_running = KodiMonitor() # Start the Websocket Client if not self.ws_running: self.ws_running = True From eb6b1fbe488853f1cb1feaafb7dc96a7aa2cb020 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 7 Jan 2018 10:56:24 +0100 Subject: [PATCH 182/509] Remove obsolete code --- resources/lib/websocket_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index dc42cb21..575db958 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -24,12 +24,7 @@ log = getLogger("PLEX."+__name__) class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) - - def __init__(self, callback=None): - if callback is not None: - self.mgr = callback - self.ws = None - Thread.__init__(self) + ws = None def process(self, opcode, message): raise NotImplementedError From 607fdab326d30f8e03abecc3d2b45aa79aabada7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 7 Jan 2018 15:16:53 +0100 Subject: [PATCH 183/509] Force-set some important Kodi settings - Fixes #389 --- resources/lib/initialsetup.py | 34 +++-- resources/lib/librarysync.py | 10 +- resources/lib/music.py | 101 ++++++-------- resources/lib/utils.py | 248 +++++++++++++++++++++------------- 4 files changed, 219 insertions(+), 174 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 92b1eafc..767f1abc 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -2,12 +2,13 @@ ############################################################################### from logging import getLogger from Queue import Queue +from xml.etree.ElementTree import ParseError import xbmc import xbmcgui from utils import settings, window, language as lang, tryEncode, \ - advancedsettings_xml + XmlKodiSetting, reboot_kodi import downloadutils from userclient import UserClient @@ -402,17 +403,28 @@ class InitialSetup(): """ LOG.info("Initial setup called.") dialog = self.dialog - - # Get current Kodi video cache setting - cache, _ = advancedsettings_xml(['cache', 'memorysize']) + try: + with XmlKodiSetting('advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml: + # Get current Kodi video cache setting + cache = xml.get_setting(['cache', 'memorysize']) + # Disable foreground "Loading media information from files" + # (still used by Kodi, even though the Wiki says otherwise) + xml.set_setting(['musiclibrary', 'backgroundupdate'], + value='true') + # Disable cleaning of library - not compatible with PKC + xml.set_setting(['videolibrary', 'cleanonupdate'], + value='false') + reboot = xml.write_xml + except ParseError: + cache = None + reboot = False # Kodi default cache if no setting is set cache = str(cache.text) if cache is not None else '20971520' LOG.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) - # Disable foreground "Loading media information from files" - # (still used by Kodi, even though the Wiki says otherwise) - advancedsettings_xml(['musiclibrary', 'backgroundupdate'], - new_value='true') + # Do we need to migrate stuff? check_migration() # Optionally sign into plex.tv. Will not be called on very first run @@ -436,6 +448,8 @@ class InitialSetup(): LOG.info("Using PMS %s with machineIdentifier %s", self.server, self.serverid) self._write_PMS_settings(self.server, self.pms_token) + if reboot is True: + reboot_kodi() return # If not already retrieved myplex info, optionally let user sign in @@ -450,6 +464,8 @@ class InitialSetup(): # User already answered the installation questions if settings('InstallQuestionsAnswered') == 'true': + if reboot is True: + reboot_kodi() return # Additional settings where the user needs to choose @@ -517,3 +533,5 @@ class InitialSetup(): state.PMS_STATUS = 'Stop' xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') + elif reboot is True: + reboot_kodi() diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 28d173b0..82f21f4b 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -458,14 +458,8 @@ class LibrarySync(Thread): Compare the views to Plex """ if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True: - if music.set_excludefromscan_music_folders() is True: - log.info('Detected new Music library - restarting now') - # 'New Plex music library detected. Sorry, but we need to - # restart Kodi now due to the changes made.' - dialog('ok', heading='{plex}', line1=lang(39711)) - from xbmc import executebuiltin - executebuiltin('RestartApp') - return False + # Will reboot Kodi is new library detected + music.excludefromscan_music_folders() self.views = [] vnodes = self.vnodes diff --git a/resources/lib/music.py b/resources/lib/music.py index ab229f0e..136c0a7a 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -1,57 +1,33 @@ # -*- coding: utf-8 -*- from logging import getLogger from re import compile as re_compile -import xml.etree.ElementTree as etree +from xml.etree.ElementTree import ParseError -from utils import advancedsettings_xml, indent, tryEncode +from utils import XmlKodiSetting, reboot_kodi, language as lang from PlexFunctions import get_plex_sections from PlexAPI import API import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''') ############################################################################### -def get_current_music_folders(): - """ - Returns a list of encoded strings as paths to the currently "blacklisted" - excludefromscan music folders in the advancedsettings.xml - """ - paths = [] - root, _ = advancedsettings_xml(['audio', 'excludefromscan']) - if root is None: - return paths - - for element in root: - try: - path = REGEX_MUSICPATH.findall(element.text)[0] - except IndexError: - log.error('Could not parse %s of xml element %s' - % (element.text, element.tag)) - continue - else: - paths.append(path) - return paths - - -def set_excludefromscan_music_folders(): +def excludefromscan_music_folders(): """ Gets a complete list of paths for music libraries from the PMS. Sets them to be excluded in the advancedsettings.xml from being scanned by Kodi. Existing keys will be replaced - Returns False if no new Plex libraries needed to be exluded, True otherwise + Reboots Kodi if new library detected """ - changed = False - write_xml = False xml = get_plex_sections() try: xml[0].attrib except (TypeError, IndexError, AttributeError): - log.error('Could not get Plex sections') + LOG.error('Could not get Plex sections') return # Build paths paths = [] @@ -66,39 +42,38 @@ def set_excludefromscan_music_folders(): typus=v.PLEX_TYPE_ARTIST, omitCheck=True) paths.append(__turn_to_regex(path)) - # Get existing advancedsettings - root, tree = advancedsettings_xml(['audio', 'excludefromscan'], - force_create=True) - - for path in paths: - for element in root: - if element.text == path: - # Path already excluded - break - else: - changed = True - write_xml = True - log.info('New Plex music library detected: %s' % path) - element = etree.Element(tag='regexp') - element.text = path - root.append(element) - - # Delete obsolete entries (unlike above, we don't change 'changed' to not - # enforce a restart) - for element in root: - for path in paths: - if element.text == path: - break - else: - log.info('Deleting Plex music library from advancedsettings: %s' - % element.text) - root.remove(element) - write_xml = True - - if write_xml is True: - indent(tree.getroot()) - tree.write('%sadvancedsettings.xml' % v.KODI_PROFILE, encoding="UTF-8") - return changed + try: + with XmlKodiSetting('advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml: + parent = xml.set_setting(['audio', 'excludefromscan']) + for path in paths: + for element in parent: + if element.text == path: + # Path already excluded + break + else: + LOG.info('New Plex music library detected: %s', path) + xml.set_setting(['audio', 'excludefromscan', 'regexp'], + value=path, check_existing=False) + # We only need to reboot if we ADD new paths! + reboot = xml.write_xml + # Delete obsolete entries + for element in parent: + for path in paths: + if element.text == path: + break + else: + LOG.info('Deleting music library from advancedsettings: %s', + element.text) + parent.remove(element) + except (ParseError, IOError): + LOG.error('Could not adjust advancedsettings.xml') + reboot = False + if reboot is True: + # 'New Plex music library detected. Sorry, but we need to + # restart Kodi now due to the changes made.' + reboot_kodi(lang(39711)) def __turn_to_regex(path): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 6dfdd941..68ab881b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -37,6 +37,17 @@ ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') # Main methods +def reboot_kodi(message=None): + """ + Displays an OK prompt with 'Kodi will now restart to apply the changes' + Kodi will then reboot. + + Set optional custom message + """ + message = message or language(33033) + dialog('ok', heading='{plex}', line1=message) + xbmc.executebuiltin('RestartApp') + def window(property, value=None, clear=False, windowid=10000): """ Get or set window property - thread safe! @@ -457,12 +468,7 @@ def reset(): dataPath = "%ssettings.xml" % addondir log.info("Deleting: settings.xml") remove(dataPath) - - # Kodi will now restart to apply the changes. - dialog('ok', - heading='{plex} %s ' % language(30132), - line1=language(33033)) - xbmc.executebuiltin('RestartApp') + reboot_kodi() def profiling(sortby="cumulative"): @@ -617,114 +623,166 @@ def guisettingsXML(): return root -def __setXMLTag(element, tag, value, attrib=None): +class XmlKodiSetting(object): """ - Looks for an element's subelement and sets its value. - If "subelement" does not exist, create it using attrib and value. + Used to load a Kodi XML settings file from special://profile as an etree + object to read settings or set them. Usage: + with XmlKodiSetting(filename, + path=None, + force_create=False, + top_element=None) as xml: + xml.get_setting('test') - element : etree element - tag : unicode for subelement - value : unicode - attrib : dict; will use etree attrib method + filename [str]: filename of the Kodi settings file under + path [str]: if set, replace special://profile path with custom + path + force_create: will create the XML file if it does not exist + top_element [str]: Name of the top xml element; used if xml does not + yet exist - Returns the subelement + Raises IOError if the file does not exist or is empty and force_create + has been set to False. + Raises etree.ParseError if the file could not be parsed by etree + + xml.write_xml Set to True if we need to write the XML to disk """ - subelement = element.find(tag) - if subelement is None: - # Setting does not exist yet; create it - if attrib is None: - etree.SubElement(element, tag).text = value + def __init__(self, filename, path=None, force_create=False, + top_element=None): + self.filename = filename + if path is None: + self.path = join(KODI_PROFILE, filename) else: - etree.SubElement(element, tag, attrib=attrib).text = value - else: - subelement.text = value - return subelement + self.path = join(path, filename) + self.force_create = force_create + self.top_element = top_element + self.tree = None + self.root = None + self.write_xml = False + def __enter__(self): + try: + self.tree = etree.parse(self.path) + except IOError: + # Document is blank or missing + if self.force_create is False: + log.debug('%s does not seem to exist; not creating', self.path) + # This will abort __enter__ + self.__exit__(IOError, None, None) + # Create topmost xml entry + self.tree = etree.ElementTree( + element=etree.Element(self.top_element)) + self.write_xml = True + except etree.ParseError: + log.error('Error parsing %s', self.path) + # "Kodi cannot parse {0}. PKC will not function correctly. Please + # visit {1} and correct your file!" + dialog('ok', language(29999), language(39716).format( + self.filename, + 'http://kodi.wiki')) + self.__exit__(etree.ParseError, None, None) + self.root = self.tree.getroot() + return self -def __setSubElement(element, subelement): - """ - Returns an etree element's subelement. Creates one if not exist - """ - answ = element.find(subelement) - if answ is None: - answ = etree.SubElement(element, subelement) - return answ + def __exit__(self, e_typ, e_val, trcbak): + if e_typ: + raise + # Only safe to file if we did not botch anything + if self.write_xml is True: + # Indent and make readable + indent(self.root) + # Safe the changed xml + self.tree.write(self.path, encoding="UTF-8") + @staticmethod + def _set_sub_element(element, subelement): + """ + Returns an etree element's subelement. Creates one if not exist + """ + answ = element.find(subelement) + if answ is None: + answ = etree.SubElement(element, subelement) + return answ -def advancedsettings_xml(node_list, new_value=None, attrib=None, - force_create=False): - """ - Returns - etree element, tree - or - None, None + def get_setting(self, node_list): + """ + node_list is a list of node names starting from the outside, ignoring + the outter advancedsettings. + Example nodelist=['video', 'busydialogdelayms'] for the following xml + would return the etree Element: - node_list is a list of node names starting from the outside, ignoring the - outter advancedsettings. Example nodelist=['video', 'busydialogdelayms'] - for the following xml would return the etree Element: - - 750 - - for the following example xml: - - - - - - If new_value is set, '750' will be replaced accordingly, returning the new - etree Element. Advancedsettings might be generated if it did not exist - already + for the following example xml: - If the dict attrib is set, the Element's attributs will be appended - accordingly + + + - force_create=True will forcibly create the key even if no value is provided - """ - path = '%sadvancedsettings.xml' % KODI_PROFILE - try: - tree = etree.parse(path) - except IOError: - # Document is blank or missing - if new_value is None and attrib is None and force_create is False: - log.debug('Could not parse advancedsettings.xml, returning None') - return None, None - # Create topmost xml entry - tree = etree.ElementTree(element=etree.Element('advancedsettings')) - except etree.ParseError: - log.error('Error parsing %s' % path) - # "Kodi cannot parse {0}. PKC will not function correctly. Please visit - # {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( - 'advancedsettings.xml', - 'http://kodi.wiki/view/Advancedsettings.xml')) - return None, None - root = tree.getroot() - element = root - - # Reading values - if new_value is None and attrib is None and force_create is False: + Returns the etree element or None if not found + """ + element = self.root for node in node_list: element = element.find(node) if element is None: break - return element, tree + return element - # Setting new values. Get correct element first - for node in node_list: - element = __setSubElement(element, node) - # Write new values - element.text = new_value or '' - if attrib is not None: - for key, attribute in attrib.iteritems(): - element.set(key, attribute) - # Indent and make readable - indent(root) - # Safe the changed xml - tree.write(path, encoding="UTF-8") - return element, tree + def set_setting(self, node_list, value=None, attrib=None, + check_existing=True): + """ + node_list is a list of node names starting from the outside, ignoring + the outter advancedsettings. + Example nodelist=['video', 'busydialogdelayms'] for the following xml + would return the etree Element: + + 750 + + for the following example xml: + + + + + + value, e.g. '750' will be set accordingly, returning the new + etree Element. Advancedsettings might be generated if it did not exist + already + + If the dict attrib is set, the Element's attributs will be appended + accordingly + + If check_existing is True, it will return the FIRST matching element of + node_list. Set to False if there are several elements of the same tag! + + Returns the (last) etree element + """ + attrib = attrib or {} + value = value or '' + if check_existing is True: + old = self.get_setting(node_list) + if old is not None: + already_set = True + if old.text.strip() != value: + already_set = False + elif old.attrib != attrib: + already_set = False + if already_set is True: + log.debug('Element has already been found') + return old + # Need to set new setting, indeed + self.write_xml = True + element = self.root + for node in node_list: + element = self._set_sub_element(element, node) + # Write new values + element.text = value + if attrib: + for key, attribute in attrib.iteritems(): + element.set(key, attribute) + return element def sourcesXML(): From 36bcd70c9d79df84a27388c88f34fe74f539f7ec Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 7 Jan 2018 15:20:25 +0100 Subject: [PATCH 184/509] Do not check plex.tv connection on startup --- resources/lib/initialsetup.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 767f1abc..f0cf9067 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, tryEncode, \ XmlKodiSetting, reboot_kodi -import downloadutils +from downloadutils import DownloadUtils as DU from userclient import UserClient from PlexAPI import PlexAPI @@ -29,7 +29,6 @@ class InitialSetup(): def __init__(self): LOG.debug('Entering initialsetup class') - self.doUtils = downloadutils.DownloadUtils().downloadUrl self.plx = PlexAPI() self.dialog = xbmcgui.Dialog() @@ -87,9 +86,9 @@ class InitialSetup(): LOG.info('plex.tv connection with token successful') settings('plex_status', value=lang(39227)) # Refresh the info from Plex.tv - xml = self.doUtils('https://plex.tv/users/account', - authenticate=False, - headerOptions={'X-Plex-Token': self.plexToken}) + xml = DU().downloadUrl('https://plex.tv/users/account', + authenticate=False, + headerOptions={'X-Plex-Token': self.plexToken}) try: self.plexLogin = xml.attrib['title'] except (AttributeError, KeyError): @@ -427,11 +426,6 @@ class InitialSetup(): # Do we need to migrate stuff? check_migration() - # Optionally sign into plex.tv. Will not be called on very first run - # as plexToken will be '' - settings('plex_status', value=lang(39226)) - if self.plexToken and self.myplexlogin: - self.CheckPlexTVSignIn() # Initialize the PKC playqueues PQ.init_playqueues() From 671424ecbe571f881406549391f59d2f4daffac0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 7 Jan 2018 15:44:20 +0100 Subject: [PATCH 185/509] Move PKC Kodi master lock hack to PKC startup --- resources/lib/initialsetup.py | 32 ++++++++++++++++++++++-- resources/lib/librarysync.py | 6 +---- resources/lib/utils.py | 47 ----------------------------------- 3 files changed, 31 insertions(+), 54 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index f0cf9067..4a8f9e83 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -2,7 +2,7 @@ ############################################################################### from logging import getLogger from Queue import Queue -from xml.etree.ElementTree import ParseError +import xml.etree.ElementTree as etree import xbmc import xbmcgui @@ -416,7 +416,7 @@ class InitialSetup(): xml.set_setting(['videolibrary', 'cleanonupdate'], value='false') reboot = xml.write_xml - except ParseError: + except etree.ParseError: cache = None reboot = False # Kodi default cache if no setting is set @@ -424,6 +424,34 @@ class InitialSetup(): LOG.info('Current Kodi video memory cache in bytes: %s', cache) settings('kodi_video_cache', value=cache) + # Hack to make PKC Kodi master lock compatible + try: + with XmlKodiSetting('sources.xml', + force_create=True, + top_element='sources') as xml: + root = xml.set_setting(['video']) + count = 2 + for source in root.findall('.//path'): + if source.text == "smb://": + count -= 1 + if count == 0: + # sources already set + break + else: + # Missing smb:// occurences, re-add. + for _ in range(0, count): + source = etree.SubElement(root, 'source') + etree.SubElement(source, + 'name').text = "PlexKodiConnect Masterlock Hack" + etree.SubElement(source, + 'path', + attrib={'pathversion': "1"}).text = "smb://" + etree.SubElement(source, 'allowsharing').text = "true" + if reboot is False: + reboot = xml.write_xml + except etree.ParseError: + pass + # Do we need to migrate stuff? check_migration() diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 82f21f4b..f7df635d 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -8,7 +8,7 @@ from random import shuffle import xbmc from xbmcvfs import exists -from utils import window, settings, getUnixTimestamp, sourcesXML,\ +from utils import window, settings, getUnixTimestamp, \ thread_methods, create_actor_db_index, dialog, LogTime, playlistXSP,\ language as lang, DateToKodi, reset, tryDecode, deletePlaylists, \ deleteNodes, tryEncode, compare_version @@ -266,10 +266,6 @@ class LibrarySync(Thread): screensaver = js.get_setting('screensaver.mode') js.set_setting('screensaver.mode', '') 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)') diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 68ab881b..990f3f5c 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -785,53 +785,6 @@ class XmlKodiSetting(object): return element -def sourcesXML(): - # To make Master lock compatible - path = tryDecode(xbmc.translatePath("special://profile/")) - xmlpath = "%ssources.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except IOError: # Document is blank or missing - root = etree.Element('sources') - except etree.ParseError: - log.error('Error parsing %s' % xmlpath) - # "Kodi cannot parse {0}. PKC will not function correctly. Please visit - # {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( - 'sources.xml', 'http://kodi.wiki/view/sources.xml')) - return - else: - root = xmlparse.getroot() - - video = root.find('video') - if video is None: - video = etree.SubElement(root, 'video') - etree.SubElement(video, 'default', attrib={'pathversion': "1"}) - - # Add elements - count = 2 - for source in root.findall('.//path'): - if source.text == "smb://": - count -= 1 - - if count == 0: - # sources already set - break - else: - # Missing smb:// occurences, re-add. - for i in range(0, count): - source = etree.SubElement(video, 'source') - etree.SubElement(source, 'name').text = "Plex" - etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://" - etree.SubElement(source, 'allowsharing').text = "true" - # Prettify and write to file - try: - indent(root) - except: pass - etree.ElementTree(root).write(xmlpath, encoding="UTF-8") - - def passwordsXML(): # To add network credentials path = tryDecode(xbmc.translatePath("special://userdata/")) From f0a2955b83a65f448fb3cb73edffe1443fbff5d0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 7 Jan 2018 17:50:30 +0100 Subject: [PATCH 186/509] Revamp playback start, part 1 --- resources/lib/PlexAPI.py | 23 +++-- resources/lib/playback.py | 84 +++++++++++++++++ resources/lib/playlist_func.py | 2 + resources/lib/playutils.py | 163 ++++++++++++--------------------- 4 files changed, 157 insertions(+), 115 deletions(-) create mode 100644 resources/lib/playback.py diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index e0c36a16..ae03b4d1 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1181,6 +1181,13 @@ class API(): """ return self.item.attrib.get('key', '') + def plex_media_streams(self): + """ + Returns the media streams directly from the PMS xml. + Mind self.mediastream to be set before and self.part! + """ + return self.item[self.mediastream][self.part] + def getFilePath(self, forceFirstMediaStream=False): """ Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv' @@ -2343,9 +2350,8 @@ class API(): url = transcodePath + urlencode(xargs) + '&' + urlencode(args) return url - def externalSubs(self, playurl): + def externalSubs(self): externalsubs = [] - mapping = {} try: mediastreams = self.item[0][self.part] except (TypeError, KeyError, IndexError): @@ -2375,13 +2381,9 @@ class API(): else: path = self.addPlexCredentialsToUrl( "%s%s" % (self.server, key)) - # map external subtitles for mapping - mapping[kodiindex] = stream.attrib['id'] externalsubs.append(path) kodiindex += 1 - mapping = dumps(mapping) - window('plex_%s.indexMapping' % playurl, value=mapping) - log.info('Found external subs: %s' % externalsubs) + log.info('Found external subs: %s', externalsubs) return externalsubs @staticmethod @@ -2402,13 +2404,14 @@ class API(): log.error('Could not temporarily download subtitle %s' % url) return else: - log.debug('Writing temp subtitle to %s' % path) + log.debug('Writing temp subtitle to %s', path) try: with open(path, 'wb') as f: f.write(r.content) except UnicodeEncodeError: - log.debug('Need to slugify the filename %s' % path) - with open(slugify(path), 'wb') as f: + log.debug('Need to slugify the filename %s', path) + path = slugify(path) + with open(path, 'wb') as f: f.write(r.content) return path diff --git a/resources/lib/playback.py b/resources/lib/playback.py new file mode 100644 index 00000000..42808be9 --- /dev/null +++ b/resources/lib/playback.py @@ -0,0 +1,84 @@ +""" +Used to kick off Kodi playback +""" +from PlexAPI import API +import playqueue as PQ +from playutils import PlayUtils +from PKC_listitem import PKC_ListItem, convert_PKC_to_listitem +from pickler import Playback_Successful +from utils import settings, dialog, language as lang + + +def playback_setup(plex_id, kodi_id, kodi_type, path): + """ + Get XML + For the single element, e.g. including trailers and parts + For playQueue (init by Companion or Alexa) + Set up + PKC/Kodi/Plex Playqueue + Trailers + Clips + Several parts + companion playqueue + Alexa music album + + """ + trailers = False + if (api.getType() == v.PLEX_TYPE_MOVIE and + not seektime and + sizePlaylist < 2 and + settings('enableCinema') == "true"): + if settings('askCinema') == "true": + trailers = dialog('yesno', lang(29999), "Play trailers?") + trailers = True if trailers else False + else: + trailers = True + # Post to the PMS. REUSE THE PLAYQUEUE! + xml = init_plex_playqueue(plex_id, + plex_lib_UUID, + mediatype=api.getType(), + trailers=trailers) + pass + + +def conclude_playback_startup(playqueue_no, + pos, + plex_id=None, + kodi_id=None, + kodi_type=None, + path=None): + """ + ONLY if actually being played (e.g. at 5th position of a playqueue). + + Decide on direct play, direct stream, transcoding + path to + direct paths: file itself + PMS URL + Web URL + audiostream (e.g. let user choose) + subtitle stream (e.g. let user choose) + Init Kodi Playback (depending on situation): + start playback + return PKC listitem attached to result + """ + result = Playback_Successful() + listitem = PKC_ListItem() + playqueue = PQ.PLAYQUEUES[playqueue_no] + item = playqueue.items[pos] + api = API(item.xml) + api.setPartNumber(item.part) + api.CreateListItemFromPlexItem(listitem) + if plex_id is not None: + playutils = PlayUtils(api, item) + playurl = playutils.getPlayUrl() + elif path is not None: + playurl = path + item.playmethod = 'DirectStream' + listitem.setPath(playurl) + if item.playmethod in ("DirectStream", "DirectPlay"): + listitem.setSubtitles(api.externalSubs()) + else: + playutils.audio_subtitle_prefs(listitem) + listitem.setPath(playurl) + result.listitem = listitem + return result diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 310a1f64..3d4e8dc5 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -189,6 +189,7 @@ class Playlist_Item(object): uri = None [str] Weird Plex uri path involving plex_uuid. STRING! guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below + playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' """ def __init__(self): self.id = None @@ -201,6 +202,7 @@ class Playlist_Item(object): self.uri = None self.guid = None self.xml = None + self.playmethod = None # Yet to be implemented: handling of a movie with several parts self.part = 0 diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 26fe8163..af7e267d 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -4,63 +4,49 @@ from logging import getLogger from downloadutils import DownloadUtils -from utils import window, settings, tryEncode, language as lang, dialog +from utils import window, settings, language as lang, dialog, tryEncode import variables as v -import PlexAPI ############################################################################### - -log = getLogger("PLEX."+__name__) - +LOG = getLogger("PLEX." + __name__) ############################################################################### class PlayUtils(): - def __init__(self, item): - self.item = item - self.API = PlexAPI.API(item) + def __init__(self, api, playqueue_item): + self.api = api + self.item = playqueue_item self.doUtils = DownloadUtils().downloadUrl - self.machineIdentifier = window('plex_machineIdentifier') - def getPlayUrl(self, partNumber=None): + def getPlayUrl(self): """ - Returns the playurl for the part with number partNumber + Returns the playurl for the part (movie might consist of several files) - playurl is utf-8 encoded! + playurl is in unicode! """ - self.API.setPartNumber(partNumber) - self.API.getMediastreamNumber() + self.api.getMediastreamNumber() playurl = self.isDirectPlay() - if playurl is not None: - log.info("File is direct playing.") - playurl = tryEncode(playurl) - # Set playmethod property - window('plex_%s.playmethod' % playurl, "DirectPlay") - + LOG.info("File is direct playing.") + self.item.playmethod = 'DirectPlay' elif self.isDirectStream(): - log.info("File is direct streaming.") - playurl = tryEncode( - self.API.getTranscodeVideoPath('DirectStream')) - # Set playmethod property - window('plex_%s.playmethod' % playurl, "DirectStream") - + LOG.info("File is direct streaming.") + playurl = self.api.getTranscodeVideoPath('DirectStream') + self.item.playmethod = 'DirectStream' else: - log.info("File is transcoding.") - playurl = tryEncode(self.API.getTranscodeVideoPath( + LOG.info("File is transcoding.") + playurl = self.api.getTranscodeVideoPath( 'Transcode', quality={ 'maxVideoBitrate': self.get_bitrate(), 'videoResolution': self.get_resolution(), 'videoQuality': '100', 'mediaBufferSize': int(settings('kodi_video_cache'))/1024, - })) - # Set playmethod property - window('plex_%s.playmethod' % playurl, value="Transcode") - - log.info("The playurl is: %s" % playurl) + }) + self.item.playmethod = 'Transcode' + LOG.info("The playurl is: %s", playurl) return playurl def isDirectPlay(self): @@ -68,52 +54,29 @@ class PlayUtils(): Returns the path/playurl if we can direct play, None otherwise """ # True for e.g. plex.tv watch later - if self.API.shouldStream() is True: - log.info("Plex item optimized for direct streaming") + if self.api.shouldStream() is True: + LOG.info("Plex item optimized for direct streaming") return # Check whether we have a strm file that we need to throw at Kodi 1:1 - path = self.API.getFilePath() + path = self.api.getFilePath() if path is not None and path.endswith('.strm'): - log.info('.strm file detected') - playurl = self.API.validatePlayurl(path, - self.API.getType(), + LOG.info('.strm file detected') + playurl = self.api.validatePlayurl(path, + self.api.getType(), forceCheck=True) - if playurl is None: - return - else: - return tryEncode(playurl) + return playurl # set to either 'Direct Stream=1' or 'Transcode=2' # and NOT to 'Direct Play=0' if settings('playType') != "0": # User forcing to play via HTTP - log.info("User chose to not direct play") + LOG.info("User chose to not direct play") return if self.mustTranscode(): return - return self.API.validatePlayurl(path, - self.API.getType(), + return self.api.validatePlayurl(path, + self.api.getType(), forceCheck=True) - def directPlay(self): - try: - playurl = self.item['MediaSources'][0]['Path'] - except (IndexError, KeyError): - playurl = self.item['Path'] - if self.item.get('VideoType'): - # Specific format modification - if self.item['VideoType'] == "Dvd": - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif self.item['VideoType'] == "BluRay": - playurl = "%s/BDMV/index.bdmv" % playurl - # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") - if "apple.com" in playurl: - USER_AGENT = "QuickTime/7.7.4" - playurl += "?|User-Agent=%s" % USER_AGENT - return playurl - def mustTranscode(self): """ Returns True if we need to transcode because @@ -125,42 +88,42 @@ class PlayUtils(): - video bitrate above specified settings bitrate if the corresponding file settings are set to 'true' """ - if self.API.getType() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG): - log.info('Plex clip or music track, not transcoding') + if self.api.getType() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG): + LOG.info('Plex clip or music track, not transcoding') return False - videoCodec = self.API.getVideoCodec() - log.info("videoCodec: %s" % videoCodec) + videoCodec = self.api.getVideoCodec() + LOG.info("videoCodec: %s" % videoCodec) if window('plex_forcetranscode') == 'true': - log.info('User chose to force-transcode') + LOG.info('User chose to force-transcode') return True codec = videoCodec['videocodec'] if codec is None: # e.g. trailers. Avoids TypeError with "'h265' in codec" - log.info('No codec from PMS, not transcoding.') + LOG.info('No codec from PMS, not transcoding.') return False if ((settings('transcodeHi10P') == 'true' and videoCodec['bitDepth'] == '10') and ('h264' in codec)): - log.info('Option to transcode 10bit h264 video content enabled.') + LOG.info('Option to transcode 10bit h264 video content enabled.') return True try: bitrate = int(videoCodec['bitrate']) except (TypeError, ValueError): - log.info('No video bitrate from PMS, not transcoding.') + 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' + 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.') + LOG.info('No video resolution from PMS, not transcoding.') return False if 'h265' in codec or 'hevc' in codec: if resolution >= self.getH265(): - log.info("Option to transcode h265/HEVC enabled. Resolution " + LOG.info("Option to transcode h265/HEVC enabled. Resolution " "of the media: %s, transcoding limit resolution: %s" % (str(resolution), str(self.getH265()))) return True @@ -168,12 +131,12 @@ class PlayUtils(): def isDirectStream(self): # Never transcode Music - if self.API.getType() == 'track': + if self.api.getType() == 'track': return True # set to 'Transcode=2' if settings('playType') == "2": # User forcing to play via HTTP - log.info("User chose to transcode") + LOG.info("User chose to transcode") return False if self.mustTranscode(): return False @@ -255,7 +218,7 @@ class PlayUtils(): } return res[chosen] - def audioSubsPref(self, listitem, url, part=None): + def audio_subtitle_prefs(self, listitem): """ For transcoding only @@ -263,15 +226,13 @@ class PlayUtils(): stream by a PUT request to the PMS """ # Set media and part where we're at - if self.API.mediastream is None: - self.API.getMediastreamNumber() - if part is None: - part = 0 + if self.api.mediastream is None: + self.api.getMediastreamNumber() try: - mediastreams = self.item[self.API.mediastream][part] + mediastreams = self.api.plex_media_streams() except (TypeError, IndexError): - log.error('Could not get media %s, part %s' - % (self.API.mediastream, part)) + LOG.error('Could not get media %s, part %s', + self.api.mediastream, self.api.part) return part_id = mediastreams.attrib['id'] audio_streams_list = [] @@ -296,17 +257,17 @@ class PlayUtils(): # Audio if typus == "2": codec = stream.attrib.get('codec') - channelLayout = stream.attrib.get('audioChannelLayout', "") + channellayout = stream.attrib.get('audioChannelLayout', "") try: track = "%s %s - %s %s" % (audio_numb+1, stream.attrib['language'], codec, - channelLayout) - except: + channellayout) + except KeyError: track = "%s %s - %s %s" % (audio_numb+1, lang(39707), # unknown codec, - channelLayout) + channellayout) audio_streams_list.append(index) audio_streams.append(tryEncode(track)) audio_numb += 1 @@ -330,13 +291,13 @@ class PlayUtils(): if downloadable: # We do know the language - temporarily download if 'language' in stream.attrib: - path = self.API.download_external_subtitles( + path = self.api.download_external_subtitles( '{server}%s' % stream.attrib['key'], "subtitle.%s.%s" % (stream.attrib['language'], stream.attrib['codec'])) # We don't know the language - no need to download else: - path = self.API.addPlexCredentialsToUrl( + path = self.api.addPlexCredentialsToUrl( "%s%s" % (window('pms_server'), stream.attrib['key'])) downloadable_streams.append(index) @@ -371,7 +332,7 @@ class PlayUtils(): select_subs_index = None if (settings('pickPlexSubtitles') == 'true' and default_sub is not None): - log.info('Using default Plex subtitle: %s' % default_sub) + LOG.info('Using default Plex subtitle: %s', default_sub) select_subs_index = default_sub else: resp = dialog('select', lang(33014), subtitle_streams) @@ -381,22 +342,14 @@ class PlayUtils(): # User selected no subtitles or backed out of dialog select_subs_index = '' - log.debug('Adding external subtitles: %s' % download_subs) + LOG.debug('Adding external subtitles: %s', download_subs) # Enable Kodi to switch autonomously to downloadable subtitles if download_subs: listitem.setSubtitles(download_subs) - + # Don't additionally burn in subtitles if select_subs_index in downloadable_streams: - for i, stream in enumerate(downloadable_streams): - if stream == select_subs_index: - # Set the correct subtitle - window('plex_%s.subtitle' % tryEncode(url), value=str(i)) - break - # Don't additionally burn in subtitles select_subs_index = '' - else: - window('plex_%s.subtitle' % tryEncode(url), value='None') - + # Now prep the PMS for our choice args = { 'subtitleStreamID': select_subs_index, 'allParts': 1 From 24f2f60209364a11a560533d7cf573ed70f09e3f Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 9 Jan 2018 19:54:54 +0100 Subject: [PATCH 187/509] Fix TypeError when PMS answer empty --- resources/lib/downloadutils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index d54722a6..9c93a3f3 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -306,18 +306,16 @@ class DownloadUtils(): log.warn("Unable to convert the response for: " "%s" % url) log.warn("Received headers were: %s" % r.headers) - log.warn('Received text:') - log.warn(r.text) + log.warn('Received text: %s', r.text) return True elif r.status_code == 403: # E.g. deleting a PMS item log.warn('PMS sent 403: Forbidden error for url %s' % url) return None else: - log.warn('Unknown answer from PMS %s with status code %s. ' - 'Message:' % (url, r.status_code)) r.encoding = 'utf-8' - log.warn(r.text) + log.warn('Unknown answer from PMS %s with status code %s. ' + 'Message: %s', url, r.status_code, r.text) return True # And now deal with the consequences of the exceptions From fb7eafb27aca6caec6a203bf382edc388a5e4bfd Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 10 Jan 2018 20:14:05 +0100 Subject: [PATCH 188/509] Revamp playback start, part 2 --- resources/lib/itemtypes.py | 9 +- resources/lib/kodimonitor.py | 120 +++++++++------- resources/lib/playback.py | 232 +++++++++++++++++++++++++----- resources/lib/playback_starter.py | 42 +++--- resources/lib/playlist_func.py | 31 ++-- resources/lib/playqueue.py | 11 +- 6 files changed, 310 insertions(+), 135 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index d48612ce..9b0a14e6 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -291,12 +291,11 @@ 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" params = { - 'filename': API.getKey(), - 'id': itemid, - 'dbid': movieid, - 'mode': "play" + 'mode': 'play', + 'plex_id': itemid, + 'plex_type': v.PLEX_TYPE_MOVIE } filename = "%s?%s" % (path, urlencode(params)) playurl = filename diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 17bf28eb..8c18b648 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -12,11 +12,11 @@ from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER from PlexAPI import API +import playqueue as PQ import json_rpc as js import playlist_func as PL import state import variables as v -import playqueue as PQ ############################################################################### @@ -254,31 +254,16 @@ class KodiMonitor(Monitor): return playqueue.clear() - @LOCKER.lockthis - def PlayBackStart(self, data): + def _get_ids(self, json_item): """ - Called whenever playback is started. Example data: - { - u'item': {u'type': u'movie', u'title': u''}, - u'player': {u'playerid': 1, u'speed': 1} - } - Unfortunately when using Widgets, Kodi doesn't tell us shit """ - # Get the type of media we're playing - try: - kodi_type = data['item']['type'] - playerid = data['player']['playerid'] - except (TypeError, KeyError): - LOG.info('Aborting playback report - item invalid for updates %s', - data) - return - json_data = js.get_item(playerid) - path = json_data.get('file') - kodi_id = json_data.get('id') + kodi_id = json_item.get('id') + kodi_type = json_item.get('type') + path = json_item.get('file') if not path and not kodi_id: LOG.info('Aborting playback report - no Kodi id or file for %s', - json_data) - return + json_item) + raise RuntimeError # Plex id will NOT be set with direct paths plex_id = state.PLEX_IDS.get(path) try: @@ -306,28 +291,49 @@ class KodiMonitor(Monitor): except TypeError: # No plex id, hence item not in the library. E.g. clips pass - info = js.get_player_props(playerid) - state.PLAYER_STATES[playerid].update(info) - state.PLAYER_STATES[playerid]['file'] = path - state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id - state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type - state.PLAYER_STATES[playerid]['plex_id'] = plex_id - state.PLAYER_STATES[playerid]['plex_type'] = plex_type - LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) - # Check whether we need to init our playqueues (e.g. direct play) - init = False - playqueue = PQ.PLAYQUEUES[playerid] + return kodi_id, kodi_type, plex_id, plex_type + + @LOCKER.lockthis + def PlayBackStart(self, data): + """ + Called whenever playback is started. Example data: + { + u'item': {u'type': u'movie', u'title': u''}, + u'player': {u'playerid': 1, u'speed': 1} + } + Unfortunately when using Widgets, Kodi doesn't tell us shit + """ + # Get the type of media we're playing try: - playqueue.items[info['position']] + kodi_type = data['item']['type'] + playerid = data['player']['playerid'] + except (TypeError, KeyError): + LOG.info('Aborting playback report - item invalid for updates %s', + data) + return + playqueue = PQ.PLAYQUEUES[playerid] + info = js.get_player_props(playerid) + json_item = js.get_item(playerid) + path = json_item.get('file') + pos = info['position'] if info['position'] != -1 else 0 + LOG.info('Detected position %s for %s', pos, playqueue) + try: + item = playqueue.items[pos] + # See if playback.py already initiated playback + init_done = item.init_done except IndexError: - init = True - if init is False and plex_id is not None: - if plex_id != playqueue.items[info['position']].plex_id: - init = True - elif init is False and path != playqueue.items[info['position']].file: - init = True - if init is True: - LOG.debug('Need to initialize Plex and PKC playqueue') + init_done = False + if init_done is True: + kodi_id = item.kodi_id + kodi_type = item.kodi_type + plex_id = item.plex_id + plex_type = item.plex_type + else: + try: + kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) + except RuntimeError: + return + LOG.info('Need to initialize Plex and PKC playqueue') if plex_id: PL.init_Plex_playlist(playqueue, plex_id=plex_id) else: @@ -335,17 +341,25 @@ class KodiMonitor(Monitor): kodi_item={'id': kodi_id, 'type': kodi_type, 'file': path}) - # Set the Plex container key (e.g. using the Plex playqueue) - container_key = None - if info['playlistid'] != -1: - # -1 is Kodi's answer if there is no playlist - container_key = PQ.PLAYQUEUES[playerid].id - if container_key is not None: - container_key = '/playQueues/%s' % container_key - elif plex_id is not None: - container_key = '/library/metadata/%s' % plex_id - state.PLAYER_STATES[playerid]['container_key'] = container_key - LOG.debug('Set the Plex container_key to: %s', container_key) + # Set the Plex container key (e.g. using the Plex playqueue) + container_key = None + if info['playlistid'] != -1: + # -1 is Kodi's answer if there is no playlist + container_key = PQ.PLAYQUEUES[playerid].id + if container_key is not None: + container_key = '/playQueues/%s' % container_key + elif plex_id is not None: + container_key = '/library/metadata/%s' % plex_id + state.PLAYER_STATES[playerid]['container_key'] = container_key + LOG.debug('Set the Plex container_key to: %s', container_key) + + state.PLAYER_STATES[playerid].update(info) + state.PLAYER_STATES[playerid]['file'] = path + state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id + state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type + state.PLAYER_STATES[playerid]['plex_id'] = plex_id + state.PLAYER_STATES[playerid]['plex_type'] = plex_type + LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): """ diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 42808be9..1a5d7060 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -1,52 +1,213 @@ """ Used to kick off Kodi playback """ +from logging import getLogger +from threading import Thread, Lock +from urllib import urlencode + +from xbmc import Player, getCondVisibility, sleep + from PlexAPI import API +from PlexFunctions import GetPlexMetadata, init_plex_playqueue +import plexdb_functions as plexdb +import playlist_func as PL import playqueue as PQ from playutils import PlayUtils -from PKC_listitem import PKC_ListItem, convert_PKC_to_listitem -from pickler import Playback_Successful -from utils import settings, dialog, language as lang +from PKC_listitem import PKC_ListItem +from pickler import pickle_me, Playback_Successful +import json_rpc as js +from utils import window, settings, dialog, language as lang, Lock_Function +import variables as v +import state + +############################################################################### + +LOG = getLogger("PLEX." + __name__) +LOCKER = Lock_Function(Lock()) + +############################################################################### -def playback_setup(plex_id, kodi_id, kodi_type, path): +@LOCKER.lockthis +def playback_triage(plex_id=None, plex_type=None, path=None): """ - Get XML - For the single element, e.g. including trailers and parts - For playQueue (init by Companion or Alexa) - Set up - PKC/Kodi/Plex Playqueue - Trailers - Clips - Several parts - companion playqueue - Alexa music album + Hit this function for addon path playback, Plex trailers, etc. + Will setup playback first, then on second call complete playback. + Returns Playback_Successful() with potentially a PKC_ListItem() attached + (to be consumed by setResolvedURL) """ + LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', + plex_id, plex_type, path) + if not state.AUTHENTICATED: + LOG.error('Not yet authenticated for PMS, abort starting playback') + # "Unauthorized for PMS" + dialog('notification', lang(29999), lang(30017)) + # Don't cause second notification to appear + return Playback_Successful() + playqueue = PQ.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) + pos = js.get_position(playqueue.playlistid) + pos = pos if pos != -1 else 0 + LOG.info('playQueue position: %s for %s', pos, playqueue) + # Have we already initiated playback? + init_done = True + try: + item = playqueue.items[pos] + except IndexError: + init_done = False + else: + init_done = item.init_done + # Either init the playback now, or - on 2nd pass - kick off playback + if init_done is False: + playback_init(plex_id, path, playqueue) + else: + conclude_playback(playqueue, pos) + + +def playback_init(plex_id, path, playqueue): + """ + Playback setup. Path is the original path PKC default.py has been called + with + """ + contextmenu_play = window('plex_contextplay') == 'true' + window('plex_contextplay', clear=True) + xml = GetPlexMetadata(plex_id) + try: + xml[0].attrib + except (IndexError, TypeError, AttributeError): + LOG.error('Could not get a PMS xml for plex id %s', plex_id) + # "Play error" + dialog('notification', lang(29999), lang(30128)) + return + result = Playback_Successful() + listitem = PKC_ListItem() + # Set the original path again so Kodi will return a 2nd time to PKC + listitem.setPath(path) + api = API(xml[0]) + plex_type = api.getType() + size_playlist = playqueue.kodi_pl.size() + # Can return -1 + start_pos = max(playqueue.kodi_pl.getposition(), 0) + LOG.info("Playlist size %s", size_playlist) + LOG.info("Playlist starting position %s", start_pos) + resume, _ = api.getRuntime() trailers = False - if (api.getType() == v.PLEX_TYPE_MOVIE and - not seektime and - sizePlaylist < 2 and + if (plex_type == v.PLEX_TYPE_MOVIE and + not resume and + size_playlist < 2 and settings('enableCinema') == "true"): if settings('askCinema') == "true": - trailers = dialog('yesno', lang(29999), "Play trailers?") + # "Play trailers?" + trailers = dialog('yesno', lang(29999), lang(33016)) trailers = True if trailers else False else: trailers = True # Post to the PMS. REUSE THE PLAYQUEUE! xml = init_plex_playqueue(plex_id, - plex_lib_UUID, - mediatype=api.getType(), + xml.attrib.get('librarySectionUUID'), + mediatype=plex_type, trailers=trailers) - pass + if xml is None: + LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', + plex_id, xml.attrib.get('librarySectionUUID')) + # "Play error" + dialog('notification', lang(29999), lang(30128)) + return + playqueue.clear() + PL.get_playlist_details_from_xml(playqueue, xml) + stack = _prep_playlist_stack(xml) + force_playback = False + if (not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and + not getCondVisibility('Window.IsVisible(VideoFullScreen.xml)')): + LOG.info("Detected playback from widget") + force_playback = True + if force_playback is False: + # Return the listelement for setResolvedURL + result.listitem = listitem + pickle_me(result) + # Wait for the setResolvedUrl to have taken its course - ugly + sleep(50) + _process_stack(playqueue, stack) + else: + # Need to kickoff playback, not using setResolvedURL + pickle_me(result) + _process_stack(playqueue, stack) + # Need a separate thread because Player won't return in time + listitem.setProperty('StartOffset', str(resume)) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, )) + thread.setDaemon(True) + thread.start() -def conclude_playback_startup(playqueue_no, - pos, - plex_id=None, - kodi_id=None, - kodi_type=None, - path=None): +def _prep_playlist_stack(xml): + stack = [] + for item in xml: + api = API(item) + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) + try: + kodi_id = plex_dbitem[0] + kodi_type = plex_dbitem[4] + except TypeError: + kodi_id = None + kodi_type = None + for part_no, _ in enumerate(item[0]): + api.setPartNumber(part_no) + if kodi_id is not None: + # We don't need the URL, item is in the Kodi library + path = None + listitem = None + else: + # Need to redirect again to PKC to conclude playback + params = { + 'mode': 'play', + 'plex_id': api.getRatingKey(), + 'plex_type': api.getType() + } + path = ('plugin://plugin.video.plexkodiconnect?%s' + % (urlencode(params))) + listitem = api.CreateListItemFromPlexItem() + api.set_listitem_artwork(listitem) + listitem.setPath(path) + stack.append({ + 'kodi_id': kodi_id, + 'kodi_type': kodi_type, + 'file': path, + 'xml_video_element': item, + 'listitem': listitem, + 'part_no': part_no + }) + return stack + + +def _process_stack(playqueue, stack): + """ + Takes our stack and adds the items to the PKC and Kodi playqueues. + This needs to be done AFTER setResolvedURL + """ + for i, item in enumerate(stack): + if item['kodi_id'] is not None: + # Use Kodi id & JSON so we get full artwork + playlist_item = PL.add_item_to_kodi_playlist( + playqueue, + i, + kodi_id=item['kodi_id'], + kodi_type=item['kodi_type'], + xml_video_element=item['xml_video_element']) + else: + playlist_item = PL.add_listitem_to_Kodi_playlist( + playqueue, + i, + item['listitem'], + file=item['file'], + xml_video_element=item['xml_video_element']) + playlist_item.part = item['part_no'] + playlist_item.init_done = True + + +def conclude_playback(playqueue, pos): """ ONLY if actually being played (e.g. at 5th position of a playqueue). @@ -63,17 +224,16 @@ def conclude_playback_startup(playqueue_no, """ result = Playback_Successful() listitem = PKC_ListItem() - playqueue = PQ.PLAYQUEUES[playqueue_no] item = playqueue.items[pos] - api = API(item.xml) - api.setPartNumber(item.part) - api.CreateListItemFromPlexItem(listitem) - if plex_id is not None: + if item.xml is not None: + # Got a Plex element + api = API(item.xml) + api.setPartNumber(item.part) + api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() - elif path is not None: - playurl = path - item.playmethod = 'DirectStream' + else: + playurl = item.file listitem.setPath(playurl) if item.playmethod in ("DirectStream", "DirectPlay"): listitem.setSubtitles(api.externalSubs()) @@ -81,4 +241,4 @@ def conclude_playback_startup(playqueue_no, playutils.audio_subtitle_prefs(listitem) listitem.setPath(playurl) result.listitem = listitem - return result + pickle_me(result) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 901e75e6..b1491ee8 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -9,6 +9,7 @@ from xbmc import Player from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful from playbackutils import PlaybackUtils +import playback from utils import window from PlexFunctions import GetPlexMetadata from PlexAPI import API @@ -126,30 +127,23 @@ class Playback_Starter(Thread): params = dict(parse_qsl(params)) mode = params.get('mode') 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() - elif mode == 'plex_node': - result = self.process_plex_node( - params.get('key'), - params.get('view_offset'), - directplay=True if params.get('play_directly') else False, - node=False if params.get('node') == 'false' else True) - elif mode == 'context_menu': - ContextMenu() - result = Playback_Successful() - 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) + if mode == 'play': + result = playback.playback_triage(plex_id=params.get('plex_id'), + plex_type=params.get('plex_type'), + path=params.get('path')) + elif mode == 'companion': + result = self.process_companion() + elif mode == 'plex_node': + result = self.process_plex_node( + params.get('key'), + params.get('view_offset'), + directplay=True if params.get('play_directly') else False, + node=False if params.get('node') == 'false' else True) + elif mode == 'context_menu': + ContextMenu() + result = Playback_Successful() + # Let default.py know! + # pickle_me(result) def run(self): queue = state.COMMAND_PIPELINE_QUEUE diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 3d4e8dc5..b3007d32 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -190,6 +190,8 @@ class Playlist_Item(object): guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' + part = 0 [int] part number if Plex video consists of mult. parts + init_done = False Set to True only if run through playback init """ def __init__(self): self.id = None @@ -203,8 +205,9 @@ class Playlist_Item(object): self.guid = None self.xml = None self.playmethod = None - # Yet to be implemented: handling of a movie with several parts + # If Plex video consists of several parts; part number self.part = 0 + self.init_done = False def __repr__(self): """ @@ -550,11 +553,11 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, - file=None): + file=None, xml_video_element=None): """ Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS - Returns False if unsuccessful + Returns the playlist item that was just added or None file: str! """ @@ -574,17 +577,20 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, if reply.get('error') is not None: LOG.error('Could not add item to playlist. Kodi reply. %s', reply) playlist.is_kodi_onadd() - return False - item = playlist_item_from_kodi( - {'id': kodi_id, 'type': kodi_type, 'file': file}) - if item.plex_id is not None: - xml = GetPlexMetadata(item.plex_id) - try: + return + if xml_video_element is not None: + item = playlist_item_from_xml(playlist, xml_video_element) + item.kodi_id = kodi_id + item.kodi_type = kodi_type + item.file = file + elif kodi_id is not None: + item = playlist_item_from_kodi( + {'id': kodi_id, 'type': kodi_type, 'file': file}) + if item.plex_id is not None: + xml = GetPlexMetadata(item.plex_id) item.xml = xml[-1] - except (TypeError, IndexError): - LOG.error('Could not get metadata for playlist item %s', item) playlist.items.insert(pos, item) - return True + return item def move_playlist_item(playlist, before_pos, after_pos): @@ -706,6 +712,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, item.file = file playlist.items.insert(pos, item) LOG.debug('Done inserting for %s', playlist) + return item def remove_from_kodi_playlist(playlist, pos): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 40804803..aa9529dc 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -58,17 +58,18 @@ def init_playqueues(): LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES) -def get_playqueue_from_type(typus): +def get_playqueue_from_type(kodi_playlist_type): """ - Returns the playqueue according to the typus ('video', 'audio', - 'picture') passed in + Returns the playqueue according to the kodi_playlist_type ('video', + 'audio', 'picture') passed in """ with LOCK: for playqueue in PLAYQUEUES: - if playqueue.type == typus: + if playqueue.type == kodi_playlist_type: break else: - raise ValueError('Wrong playlist type passed in: %s' % typus) + raise ValueError('Wrong playlist type passed in: %s', + kodi_playlist_type) return playqueue From 7ecaa376a2957023035205247fd9dc7014790a73 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 21 Jan 2018 13:42:22 +0100 Subject: [PATCH 189/509] Revamp playback start, part 3 --- resources/lib/kodimonitor.py | 4 + resources/lib/playback.py | 158 +++++++++++++++------------------ resources/lib/playlist_func.py | 31 +++---- 3 files changed, 90 insertions(+), 103 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 8c18b648..574816ff 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -198,6 +198,10 @@ class KodiMonitor(Monitor): Will NOT be called if playback initiated by Kodi widgets """ playqueue = PQ.PLAYQUEUES[data['playlistid']] + # Have we initiated the playqueue already? If not, ignore this + if not playqueue.items: + LOG.debug('Playqueue not initiated - ignoring') + return # Did PKC cause this add? Then lets not do anything if playqueue.is_kodi_onadd() is False: LOG.debug('PKC added this item to the playqueue - ignoring') diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1a5d7060..99615fb1 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -2,10 +2,10 @@ Used to kick off Kodi playback """ from logging import getLogger -from threading import Thread, Lock +from threading import Thread from urllib import urlencode -from xbmc import Player, getCondVisibility, sleep +from xbmc import Player, sleep from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue @@ -16,14 +16,14 @@ from playutils import PlayUtils from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful import json_rpc as js -from utils import window, settings, dialog, language as lang, Lock_Function +from utils import window, settings, dialog, language as lang +from plexbmchelper.subscribers import LOCKER import variables as v import state ############################################################################### LOG = getLogger("PLEX." + __name__) -LOCKER = Lock_Function(Lock()) ############################################################################### @@ -34,42 +34,43 @@ def playback_triage(plex_id=None, plex_type=None, path=None): Hit this function for addon path playback, Plex trailers, etc. Will setup playback first, then on second call complete playback. - Returns Playback_Successful() with potentially a PKC_ListItem() attached - (to be consumed by setResolvedURL) + Will set Playback_Successful() with potentially a PKC_ListItem() attached + (to be consumed by setResolvedURL in default.py) + + If trailers or additional (movie-)parts are added, default.py is released + and a completely new player instance is called with a new playlist. This + circumvents most issues with Kodi & playqueues """ LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', plex_id, plex_type, path) if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') + # Release default.py + pickle_me(Playback_Successful()) # "Unauthorized for PMS" dialog('notification', lang(29999), lang(30017)) - # Don't cause second notification to appear - return Playback_Successful() + return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) pos = js.get_position(playqueue.playlistid) + # Can return -1 (as in "no playlist") pos = pos if pos != -1 else 0 LOG.info('playQueue position: %s for %s', pos, playqueue) # Have we already initiated playback? - init_done = True try: - item = playqueue.items[pos] + playqueue.items[pos] except IndexError: - init_done = False - else: - init_done = item.init_done - # Either init the playback now, or - on 2nd pass - kick off playback - if init_done is False: - playback_init(plex_id, path, playqueue) + playback_init(plex_id, plex_type, playqueue) else: + # kick off playback on second pass conclude_playback(playqueue, pos) -def playback_init(plex_id, path, playqueue): +def playback_init(plex_id, plex_type, playqueue): """ - Playback setup. Path is the original path PKC default.py has been called - with + Playback setup if Kodi starts playing an item for the first time. """ + LOG.info('Initializing PKC playback') contextmenu_play = window('plex_contextplay') == 'true' window('plex_contextplay', clear=True) xml = GetPlexMetadata(plex_id) @@ -77,25 +78,15 @@ def playback_init(plex_id, path, playqueue): xml[0].attrib except (IndexError, TypeError, AttributeError): LOG.error('Could not get a PMS xml for plex id %s', plex_id) + # Release default.py + pickle_me(Playback_Successful()) # "Play error" - dialog('notification', lang(29999), lang(30128)) + dialog('notification', lang(29999), lang(30128), icon='{error}') return - result = Playback_Successful() - listitem = PKC_ListItem() - # Set the original path again so Kodi will return a 2nd time to PKC - listitem.setPath(path) api = API(xml[0]) - plex_type = api.getType() - size_playlist = playqueue.kodi_pl.size() - # Can return -1 - start_pos = max(playqueue.kodi_pl.getposition(), 0) - LOG.info("Playlist size %s", size_playlist) - LOG.info("Playlist starting position %s", start_pos) resume, _ = api.getRuntime() trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and - not resume and - size_playlist < 2 and + if (plex_type == v.PLEX_TYPE_MOVIE and not resume and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" @@ -103,7 +94,8 @@ def playback_init(plex_id, path, playqueue): trailers = True if trailers else False else: trailers = True - # Post to the PMS. REUSE THE PLAYQUEUE! + LOG.info('Playing trailers: %s', trailers) + # Post to the PMS to create a playqueue - in any case due to Plex Companion xml = init_plex_playqueue(plex_id, xml.attrib.get('librarySectionUUID'), mediatype=plex_type, @@ -111,55 +103,44 @@ def playback_init(plex_id, path, playqueue): if xml is None: LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', plex_id, xml.attrib.get('librarySectionUUID')) + # Release default.py + pickle_me(Playback_Successful()) # "Play error" - dialog('notification', lang(29999), lang(30128)) + dialog('notification', lang(29999), lang(30128), icon='{error}') return + # Should already be empty, but just in case playqueue.clear() PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) - force_playback = False - if (not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and - not getCondVisibility('Window.IsVisible(VideoFullScreen.xml)')): - LOG.info("Detected playback from widget") - force_playback = True - if force_playback is False: - # Return the listelement for setResolvedURL - result.listitem = listitem - pickle_me(result) - # Wait for the setResolvedUrl to have taken its course - ugly - sleep(50) - _process_stack(playqueue, stack) - else: - # Need to kickoff playback, not using setResolvedURL - pickle_me(result) - _process_stack(playqueue, stack) - # Need a separate thread because Player won't return in time - listitem.setProperty('StartOffset', str(resume)) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, )) - thread.setDaemon(True) - thread.start() + # Release our default.py before starting our own Kodi player instance + pickle_me(Playback_Successful()) + # Sleep a bit to let setResolvedUrl do its thing - bit ugly + sleep(200) + _process_stack(playqueue, stack) + # New thread to release this one sooner (e.g. harddisk spinning up) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, )) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + thread.start() def _prep_playlist_stack(xml): stack = [] for item in xml: api = API(item) - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) - try: - kodi_id = plex_dbitem[0] - kodi_type = plex_dbitem[4] - except TypeError: + if api.getType() != v.PLEX_TYPE_CLIP: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) + kodi_id = plex_dbitem[0] if plex_dbitem else None + kodi_type = plex_dbitem[4] if plex_dbitem else None + else: + # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None - for part_no, _ in enumerate(item[0]): - api.setPartNumber(part_no) - if kodi_id is not None: - # We don't need the URL, item is in the Kodi library - path = None - listitem = None - else: + for part, _ in enumerate(item[0]): + api.setPartNumber(part) + if kodi_id is None: # Need to redirect again to PKC to conclude playback params = { 'mode': 'play', @@ -167,17 +148,20 @@ def _prep_playlist_stack(xml): 'plex_type': api.getType() } path = ('plugin://plugin.video.plexkodiconnect?%s' - % (urlencode(params))) + % urlencode(params)) listitem = api.CreateListItemFromPlexItem() - api.set_listitem_artwork(listitem) listitem.setPath(path) + else: + # Will add directly via the Kodi DB + path = None + listitem = None stack.append({ 'kodi_id': kodi_id, 'kodi_type': kodi_type, 'file': path, 'xml_video_element': item, 'listitem': listitem, - 'part_no': part_no + 'part': part }) return stack @@ -185,26 +169,28 @@ def _prep_playlist_stack(xml): def _process_stack(playqueue, stack): """ Takes our stack and adds the items to the PKC and Kodi playqueues. - This needs to be done AFTER setResolvedURL """ - for i, item in enumerate(stack): - if item['kodi_id'] is not None: - # Use Kodi id & JSON so we get full artwork - playlist_item = PL.add_item_to_kodi_playlist( - playqueue, - i, - kodi_id=item['kodi_id'], - kodi_type=item['kodi_type'], - xml_video_element=item['xml_video_element']) - else: + # getposition() can return -1 + pos = max(playqueue.kodi_pl.getposition(), 0) + 1 + for item in stack: + if item['kodi_id'] is None: playlist_item = PL.add_listitem_to_Kodi_playlist( playqueue, - i, + pos, item['listitem'], file=item['file'], xml_video_element=item['xml_video_element']) - playlist_item.part = item['part_no'] + else: + # Directly add element so we have full metadata + playlist_item = PL.add_item_to_kodi_playlist( + playqueue, + pos, + kodi_id=item['kodi_id'], + kodi_type=item['kodi_type'], + xml_video_element=item['xml_video_element']) + playlist_item.part = item['part'] playlist_item.init_done = True + pos += 1 def conclude_playback(playqueue, pos): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index b3007d32..1d03237f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -324,7 +324,8 @@ def playlist_item_from_plex(plex_id): return item -def playlist_item_from_xml(playlist, xml_video_element): +def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, + kodi_type=None): """ Returns a playlist element for the playqueue using the Plex xml @@ -338,7 +339,10 @@ def playlist_item_from_xml(playlist, xml_video_element): item.guid = xml_video_element.attrib.get('guid') if item.guid is not None: item.guid = escape_html(item.guid) - if item.plex_id: + if kodi_id is not None: + item.kodi_id = kodi_id + item.kodi_type = kodi_type + elif item.plex_id is not None: with plexdb.Get_Plex_DB() as plex_db: db_element = plex_db.getItem_byId(item.plex_id) try: @@ -369,20 +373,13 @@ def get_playlist_details_from_xml(playlist, xml): Takes a PMS xml as input and overwrites all the playlist's details, e.g. playlist.id with the XML's playQueueID """ - try: - playlist.id = xml.attrib['%sID' % playlist.kind] - playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] - playlist.selectedItemID = xml.attrib.get( - '%sSelectedItemID' % playlist.kind) - playlist.selectedItemOffset = xml.attrib.get( - '%sSelectedItemOffset' % playlist.kind) - except: - LOG.error('Could not parse xml answer from PMS for playlist %s', - playlist) - import traceback - LOG.error(traceback.format_exc()) - raise KeyError + playlist.id = xml.attrib['%sID' % playlist.kind] + playlist.version = xml.attrib['%sVersion' % playlist.kind] + playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] + playlist.selectedItemID = xml.attrib.get( + '%sSelectedItemID' % playlist.kind) + playlist.selectedItemOffset = xml.attrib.get( + '%sSelectedItemOffset' % playlist.kind) LOG.debug('Updated playlist from xml: %s', playlist) @@ -702,7 +699,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, pos, playlist) # Add the item into Kodi playlist playlist.kodi_onadd() - playlist.kodi_pl.add(file, listitem, index=pos) + playlist.kodi_pl.add(url=file, listitem=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) From 2791da9f651eb138af86f5dfc14ecc145c54b543 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 21 Jan 2018 18:31:49 +0100 Subject: [PATCH 190/509] Revamp playback start, part 4 --- resources/lib/PlexAPI.py | 9 + resources/lib/kodimonitor.py | 21 +- resources/lib/playback.py | 7 +- resources/lib/player.py | 379 +++++++-------------------------- resources/lib/playlist_func.py | 4 + resources/lib/state.py | 70 +++--- 6 files changed, 148 insertions(+), 342 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index ae03b4d1..b6affbf5 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1268,6 +1268,15 @@ class API(): res = '2000-01-01 10:00:00' return res + def getViewCount(self): + """ + Returns the play count for the item as an int or the int 0 if not found + """ + try: + return int(self.item.attrib['viewCount']) + except (KeyError, ValueError): + return 0 + def getUserData(self): """ Returns a dict with None if a value is missing diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 574816ff..38e848f1 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -45,6 +45,7 @@ STATE_SETTINGS = { 'enableMusic': 'ENABLE_MUSIC', 'enableBackgroundSync': 'BACKGROUND_SYNC' } + ############################################################################### @@ -55,6 +56,8 @@ class KodiMonitor(Monitor): def __init__(self): self.xbmcplayer = Player() Monitor.__init__(self) + for playerid in state.PLAYER_STATES: + state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) LOG.info("Kodi monitor started.") def onScanStarted(self, library): @@ -315,6 +318,8 @@ class KodiMonitor(Monitor): LOG.info('Aborting playback report - item invalid for updates %s', data) return + # Remember that this player has been active + state.ACTIVE_PLAYERS.append(playerid) playqueue = PQ.PLAYQUEUES[playerid] info = js.get_player_props(playerid) json_item = js.get_item(playerid) @@ -356,13 +361,15 @@ class KodiMonitor(Monitor): container_key = '/library/metadata/%s' % plex_id state.PLAYER_STATES[playerid]['container_key'] = container_key LOG.debug('Set the Plex container_key to: %s', container_key) - - state.PLAYER_STATES[playerid].update(info) - state.PLAYER_STATES[playerid]['file'] = path - state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id - state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type - state.PLAYER_STATES[playerid]['plex_id'] = plex_id - state.PLAYER_STATES[playerid]['plex_type'] = plex_type + status = state.PLAYER_STATES[playerid] + status.update(info) + status['file'] = path + status['kodi_id'] = kodi_id + status['kodi_type'] = kodi_type + status['plex_id'] = plex_id + status['plex_type'] = plex_type + status['playmethod'] = item.playmethod + status['playcount'] = item.playcount LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) def StartDirectPath(self, plex_id, type, currentFile): diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 99615fb1..ddfb02d3 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -138,6 +138,7 @@ def _prep_playlist_stack(xml): # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None + resume, _ = api.getRuntime() for part, _ in enumerate(item[0]): api.setPartNumber(part) if kodi_id is None: @@ -161,7 +162,9 @@ def _prep_playlist_stack(xml): 'file': path, 'xml_video_element': item, 'listitem': listitem, - 'part': part + 'part': part, + 'playcount': api.getViewCount(), + 'offset': resume }) return stack @@ -188,6 +191,8 @@ def _process_stack(playqueue, stack): kodi_id=item['kodi_id'], kodi_type=item['kodi_type'], xml_video_element=item['xml_video_element']) + playlist_item.playcount = item['playcount'] + playlist_item.offset = item['offset'] playlist_item.part = item['part'] playlist_item.init_done = True pos += 1 diff --git a/resources/lib/player.py b/resources/lib/player.py index be006ff2..73ea0463 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -2,15 +2,14 @@ ############################################################################### from logging import getLogger -from json import loads -from xbmc import Player, sleep +from xbmc import Player -from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode -import downloadutils +from utils import window, DateToKodi, getUnixTimestamp, kodi_time_to_millis +from downloadutils import DownloadUtils as DU import plexdb_functions as plexdb import kodidb_functions as kodidb -import json_rpc as js +from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -22,326 +21,104 @@ LOG = getLogger("PLEX." + __name__) class PKC_Player(Player): - - played_info = state.PLAYED_INFO - playStats = state.PLAYER_STATES - currentFile = None - def __init__(self): - self.doUtils = downloadutils.DownloadUtils Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): """ Will be called when xbmc starts playing a file. - Window values need to have been set in Kodimonitor.py """ - return - self.stopAll() - - # Get current file (in utf-8!) - try: - currentFile = tryDecode(self.getPlayingFile()) - sleep(300) - except: - currentFile = "" - count = 0 - while not currentFile: - sleep(100) - try: - currentFile = tryDecode(self.getPlayingFile()) - except: - pass - if count == 20: - break - else: - count += 1 - if not currentFile: - LOG.warn('Error getting currently playing file; abort reporting') - return - - # Save currentFile for cleanup later and for references - self.currentFile = currentFile - window('plex_lastPlayedFiled', value=currentFile) - # We may need to wait for info to be set in kodi monitor - itemId = window("plex_%s.itemid" % tryEncode(currentFile)) - count = 0 - while not itemId: - sleep(200) - itemId = window("plex_%s.itemid" % tryEncode(currentFile)) - if count == 5: - LOG.warn("Could not find itemId, cancelling playback report!") - return - count += 1 - - LOG.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) - - plexitem = "plex_%s" % tryEncode(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" % plexitem)) - except ValueError: - playcount = 0 - window('plex_skipWatched%s' % itemId, value="true") - - LOG.debug("Playing itemtype is: %s" % itemType) - - customseek = window('plex_customplaylist.seektime') - if customseek: - # Start at, when using custom playlist (play to Kodi from - # webclient) - LOG.info("Seeking to: %s" % customseek) - try: - self.seekTime(int(customseek)) - except: - LOG.error('Could not seek!') - window('plex_customplaylist.seektime', clear=True) - - try: - seekTime = self.getTime() - except RuntimeError: - LOG.error('Could not get current seektime from xbmc player') - seekTime = 0 - volume = js.get_volume() - muted = js.get_muted() - - # Postdata structure to send to plex server - url = "{server}/:/timeline?" - postdata = { - - 'QueueableMediaTypes': "Video", - 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, - 'VolumeLevel': volume, - 'PositionTicks': int(seekTime * 10000000), - 'IsMuted': muted - } - - # Get the current audio track and subtitles - if playMethod == "Transcode": - # property set in PlayUtils.py - postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" - % tryEncode(currentFile)) - postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" - % tryEncode(currentFile)) - else: - # Get the current kodi audio and subtitles and convert to plex equivalent - indexAudio = js.current_audiostream(1).get('index', 0) - subsEnabled = js.subtitle_enabled(1) - if subsEnabled: - indexSubs = js.current_subtitle(1).get('index', 0) - else: - indexSubs = 0 - - # Postdata for the audio - postdata['AudioStreamIndex'] = indexAudio + 1 - - # Postdata for the subtitles - if subsEnabled and len(Player().getAvailableSubtitleStreams()) > 0: - - # Number of audiotracks to help get plex Index - audioTracks = len(Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % plexitem) - - if mapping: # Set in playbackutils.py - - LOG.debug("Mapping for external subtitles index: %s" - % mapping) - externalIndex = loads(mapping) - - if externalIndex.get(str(indexSubs)): - # If the current subtitle is in the mapping - postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] - else: - # Internal subtitle currently selected - subindex = indexSubs - len(externalIndex) + audioTracks + 1 - postdata['SubtitleStreamIndex'] = subindex - - else: # Direct paths enabled scenario or no external subtitles set - postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 - else: - postdata['SubtitleStreamIndex'] = "" - - - # Post playback to server - # log("Sending POST play started: %s." % postdata, 2) - # self.doUtils(url, postBody=postdata, type="POST") - - # Ensure we do have a runtime - try: - runtime = int(runtime) - except ValueError: - try: - runtime = self.getTotalTime() - LOG.error("Runtime is missing, Kodi runtime: %s" % runtime) - except: - LOG.error('Could not get kodi runtime, setting to zero') - runtime = 0 - - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byId(itemId) - try: - fileid = plex_dbitem[1] - except TypeError: - LOG.info("Could not find fileid in plex db.") - fileid = None - # Save data map for updates and position calls - data = { - 'runtime': runtime, - 'item_id': itemId, - 'refresh_id': refresh_id, - 'currentfile': currentFile, - 'AudioStreamIndex': postdata['AudioStreamIndex'], - 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, - 'currentPosition': int(seekTime), - 'fileid': fileid, - 'itemType': itemType, - 'playcount': playcount - } - - self.played_info[currentFile] = data - LOG.info("ADDING_FILE: %s" % data) - - # log some playback stats - '''if(itemType != None): - if(self.playStats.get(itemType) != None): - count = self.playStats.get(itemType) + 1 - self.playStats[itemType] = count - else: - self.playStats[itemType] = 1 - - if(playMethod != None): - if(self.playStats.get(playMethod) != None): - count = self.playStats.get(playMethod) + 1 - self.playStats[playMethod] = count - else: - self.playStats[playMethod] = 1''' + pass def onPlayBackPaused(self): - - currentFile = self.currentFile - LOG.info("PLAYBACK_PAUSED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = True + """ + Will be called when playback is paused + """ + pass def onPlayBackResumed(self): - - currentFile = self.currentFile - LOG.info("PLAYBACK_RESUMED: %s" % currentFile) - - if self.played_info.get(currentFile): - self.played_info[currentFile]['paused'] = False + """ + Will be called when playback is resumed + """ + pass def onPlayBackSeek(self, time, seekOffset): - # Make position when seeking a bit more accurate - currentFile = self.currentFile - LOG.info("PLAYBACK_SEEK: %s" % currentFile) - - if self.played_info.get(currentFile): - try: - position = self.getTime() - except RuntimeError: - # When Kodi is not playing - return - self.played_info[currentFile]['currentPosition'] = position + """ + Will be called when user seeks to a certain time during playback + """ + pass def onPlayBackStopped(self): - # Will be called when user stops xbmc playing a file + """ + Will be called when playback is stopped by the user + """ LOG.info("ONPLAYBACK_STOPPED") + self.cleanup_playback() - self.stopAll() + def onPlayBackEnded(self): + """ + Will be called when playback ends due to the media file being finished + """ + LOG.info("ONPLAYBACK_ENDED") + self.cleanup_playback() + @LOCKER.lockthis + def cleanup_playback(self): + """ + PKC cleanup after playback ends/is stopped + """ + # We might have saved a transient token from a user flinging media via + # Companion (if we could not use the playqueue to store the token) + state.PLEX_TRANSIENT_TOKEN = None for item in ('plex_currently_playing_itemid', 'plex_customplaylist', 'plex_customplaylist.seektime', 'plex_forcetranscode'): window(item, clear=True) - # We might have saved a transient token from a user flinging media via - # Companion (if we could not use the playqueue to store the token) - state.PLEX_TRANSIENT_TOKEN = None - LOG.debug("Cleared playlist properties.") - - def onPlayBackEnded(self): - # Will be called when xbmc stops playing a file, because the file ended - LOG.info("ONPLAYBACK_ENDED") - self.onPlayBackStopped() - - def stopAll(self): - if not self.played_info: - return - LOG.info("Played_information: %s" % self.played_info) - # Process each items - for item in self.played_info: - data = self.played_info.get(item) - if not data: + for playerid in state.ACTIVE_PLAYERS: + status = state.PLAYER_STATES[playerid] + # Check whether we need to mark an item as completely watched + if not status['kodi_id'] or not status['plex_id']: + LOG.info('No PKC info safed for the element just played by Kodi' + ' player %s', playerid) continue - LOG.debug("Item path: %s" % item) - LOG.debug("Item data: %s" % data) - - runtime = data['runtime'] - currentPosition = data['currentPosition'] - itemid = data['item_id'] - refresh_id = data['refresh_id'] - currentFile = data['currentfile'] - media_type = data['Type'] - playMethod = data['playmethod'] - - # Prevent manually mark as watched in Kodi monitor - window('plex_skipWatched%s' % itemid, value="true") - - if not currentPosition or not runtime: + # Stop transcoding + if status['playmethod'] == 'Transcode': + LOG.info('Tell the PMS to stop transcoding') + DU().downloadUrl( + '{server}/video/:/transcode/universal/stop', + parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + if status['plex_type'] == v.PLEX_TYPE_SONG: + LOG.debug('Song has been played, not cleaning up playstate') continue - try: - percentComplete = float(currentPosition) / float(runtime) - except ZeroDivisionError: - # Runtime is 0. - percentComplete = 0 - LOG.info("Percent complete: %s Mark played at: %s" - % (percentComplete, v.MARK_PLAYED_AT)) - if percentComplete >= v.MARK_PLAYED_AT: + resume = kodi_time_to_millis(status['time']) + runtime = kodi_time_to_millis(status['totaltime']) + LOG.info('Item playback progress %s out of %s', resume, runtime) + if not resume or not runtime: + continue + complete = float(resume) / float(runtime) + LOG.info("Percent complete: %s. Mark played at: %s", + complete, v.MARK_PLAYED_AT) + if complete >= v.MARK_PLAYED_AT: # Tell Kodi that we've finished watching (Plex knows) - if (data['fileid'] is not None and - data['itemType'] in (v.KODI_TYPE_MOVIE, - v.KODI_TYPE_EPISODE)): - with kodidb.GetKodiDB('video') as kodi_db: - kodi_db.addPlaystate( - data['fileid'], - None, - None, - data['playcount'] + 1, - DateToKodi(getUnixTimestamp())) - - # Clean the WINDOW properties - for filename in self.played_info: - plex_item = 'plex_%s' % tryEncode(filename) - cleanup = ( - '%s.itemid' % plex_item, - '%s.runtime' % plex_item, - '%s.refreshid' % plex_item, - '%s.playmethod' % plex_item, - '%s.type' % plex_item, - '%s.runtime' % plex_item, - '%s.playcount' % plex_item, - '%s.playlistPosition' % plex_item, - '%s.subtitle' % plex_item, - ) - for item in cleanup: - window(item, clear=True) - - # Stop transcoding - if playMethod == "Transcode": - LOG.info("Transcoding for %s terminating" % itemid) - self.doUtils().downloadUrl( - "{server}/video/:/transcode/universal/stop", - parameters={'session': v.PKC_MACHINE_IDENTIFIER}) - - self.played_info.clear() + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(status['plex_id']) + file_id = plex_dbitem[1] if plex_dbitem else None + if file_id is None: + LOG.error('No file_id found for %s', status) + continue + with kodidb.GetKodiDB('video') as kodi_db: + kodi_db.addPlaystate( + file_id, + None, + None, + status['playcount'] + 1, + DateToKodi(getUnixTimestamp())) + LOG.info('Marked plex element %s as completely watched', + status['plex_id']) + # As all playback has halted, reset the players that have been active + state.ACTIVE_PLAYERS = [] + for playerid in state.PLAYER_STATES: + state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) + LOG.info('Finished PKC playback cleanup') diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 1d03237f..77ec1bf5 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -190,6 +190,8 @@ class Playlist_Item(object): guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' + playcount = None [int] how many times the item has already been played + offset = None [int] the item's view offset UPON START in Plex time part = 0 [int] part number if Plex video consists of mult. parts init_done = False Set to True only if run through playback init """ @@ -205,6 +207,8 @@ class Playlist_Item(object): self.guid = None self.xml = None self.playmethod = None + self.playcount = None + self.offset = None # If Plex video consists of several parts; part number self.part = 0 self.init_done = False diff --git a/resources/lib/state.py b/resources/lib/state.py index a964ee51..db267be5 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -82,44 +82,48 @@ COMMAND_PIPELINE_QUEUE = None # Websocket_client queue to communicate with librarysync WEBSOCKET_QUEUE = None +# Which Kodi player is/has been active? (either int 1, 2 or 3) +ACTIVE_PLAYERS = [] + # Kodi player states - here, initial values are set PLAYER_STATES = { - 1: { - 'type': 'movie', - 'time': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0 - }, - 'totaltime': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0 - }, - 'speed': 0, - 'shuffled': False, - 'repeat': 'off', - 'position': -1, - 'playlistid': -1, - 'currentvideostream': -1, - 'currentaudiostream': -1, - 'subtitleenabled': False, - 'currentsubtitle': -1, - ###### - 'file': '', - 'kodi_id': None, - 'kodi_type': None, - 'plex_id': None, - 'plex_type': None, - 'container_key': None, - 'volume': 100, - 'muted': False - }, + 1: {}, 2: {}, 3: {} } +# "empty" dict for the PLAYER_STATES above +PLAYSTATE = { + 'type': None, + 'time': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'totaltime': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'speed': 0, + 'shuffled': False, + 'repeat': 'off', + 'position': None, + 'playlistid': None, + 'currentvideostream': -1, + 'currentaudiostream': -1, + 'subtitleenabled': False, + 'currentsubtitle': -1, + 'file': None, + 'kodi_id': None, + 'kodi_type': None, + 'plex_id': None, + 'plex_type': None, + 'container_key': None, + 'volume': 100, + 'muted': False, + 'playmethod': None, + 'playcount': None +} # Dict containing all filenames as keys with plex id as values - used for addon # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} From 287b888b6f346de6507f018c41979bd7dfac730f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 22 Jan 2018 11:20:37 +0100 Subject: [PATCH 191/509] Revamp playback start, part 5 --- resources/lib/PlexAPI.py | 14 ++++++++- resources/lib/kodimonitor.py | 36 ++++++++++++++++++++-- resources/lib/pickler.py | 2 +- resources/lib/playback.py | 58 +++++++++++++++++++++++++++++++++--- resources/lib/playutils.py | 1 + resources/lib/state.py | 3 ++ service.py | 4 ++- 7 files changed, 109 insertions(+), 9 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b6affbf5..f5337836 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1505,6 +1505,16 @@ class API(): """ return self.item.attrib.get('year', None) + def getResume(self): + """ + Returns the resume point of time in seconds as int. 0 if not found + """ + try: + resume = float(self.item.attrib['viewOffset']) + except (KeyError, ValueError): + resume = 0.0 + return int(resume * v.PLEX_TO_KODI_TIMEFACTOR) + def getRuntime(self): """ Resume point of time and runtime/totaltime in rounded to seconds. @@ -2521,7 +2531,9 @@ class API(): 'mpaa': self.getMpaa(), 'aired': self.getPremiereDate() } - listItem.setProperty('resumetime', str(userdata['Resume'])) + # Do NOT set resumetime - otherwise Kodi always resumes at that time + # even if the user chose to start element from the beginning + # listItem.setProperty('resumetime', str(userdata['Resume'])) listItem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 38e848f1..4d4410ec 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -3,11 +3,15 @@ PKC Kodi Monitoring implementation """ from logging import getLogger from json import loads +from threading import Thread -from xbmc import Monitor, Player, sleep +from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ + getLocalizedString +from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, CatchExceptions, plex_command +from utils import window, settings, CatchExceptions, plex_command, \ + thread_methods from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER @@ -391,3 +395,31 @@ class KodiMonitor(Monitor): else: window('plex_%s.playmethod' % currentFile, value="DirectPlay") LOG.debug('Window properties set for direct paths!') + + +@thread_methods +class SpecialMonitor(Thread): + """ + Detect the resume dialog for widgets. + Could also be used to detect external players (see Emby implementation) + """ + def run(self): + LOG.info("----====# Starting Special Monitor #====----") + player = Player() + while not self.thread_stopped(): + is_playing = player.isPlaying() + + if (not is_playing and + getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and + not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and + getInfoLabel('Control.GetLabel(1002)') == getLocalizedString(12021)): + control = int(Window(10106).getFocusId()) + if control == 1002: + # Start from beginning + LOG.info("Resume dialog: Start from beginning selected") + state.RESUME_PLAYBACK = False + else: + LOG.info("Resume dialog: resume selected") + state.RESUME_PLAYBACK = True + sleep(200) + LOG.info("#====---- Special Monitor Stopped ----====#") diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index b5579cd4..6e660dd0 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -32,7 +32,7 @@ def pickle_me(obj, window_var='plex_result'): obj can be pretty much any Python object. However, classes and functions won't work. See the Pickle documentation """ - log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG) + log('%sStart pickling' % PREFIX, level=LOGDEBUG) pickl_window(window_var, value=dumps(obj)) log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ddfb02d3..8e0dd586 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -66,6 +66,44 @@ def playback_triage(plex_id=None, plex_type=None, path=None): conclude_playback(playqueue, pos) +def play_resume(playqueue, xml, stack): + """ + If there exists a resume point, Kodi will ask the user whether to continue + playback. We thus need to use setResolvedUrl "correctly". Mind that there + might be several parts! + """ + result = Playback_Successful() + listitem = PKC_ListItem() + # Only get the very first item of our playqueue (i.e. the very first part) + stack_item = stack.pop(0) + api = API(xml[0]) + item = PL.playlist_item_from_xml(playqueue, + xml[0], + kodi_id=stack_item['kodi_id'], + kodi_type=stack_item['kodi_type']) + api.setPartNumber(item.part) + item.playcount = stack_item['playcount'] + item.offset = stack_item['offset'] + item.part = stack_item['part'] + item.init_done = True + api.CreateListItemFromPlexItem(listitem) + playutils = PlayUtils(api, item) + playurl = playutils.getPlayUrl() + listitem.setPath(playurl) + if item.playmethod in ('DirectStream', 'DirectPlay'): + listitem.setSubtitles(api.externalSubs()) + else: + playutils.audio_subtitle_prefs(listitem) + result.listitem = listitem + # Add to our playlist + playqueue.items.append(item) + # This will release default.py with setResolvedUrl + pickle_me(result) + # Add remaining parts to the playlist, if any + if stack: + _process_stack(playqueue, stack) + + def playback_init(plex_id, plex_type, playqueue): """ Playback setup if Kodi starts playing an item for the first time. @@ -112,10 +150,16 @@ def playback_init(plex_id, plex_type, playqueue): playqueue.clear() PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) + # if resume: + # # Need to handle this differently so only 1 dialog is displayed whether + # # user wants to resume to start at the beginning + # LOG.info('Resume detected') + # play_resume(playqueue, xml, stack) + # return # Release our default.py before starting our own Kodi player instance pickle_me(Playback_Successful()) # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(200) + sleep(100) _process_stack(playqueue, stack) # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=Player().play, @@ -138,7 +182,6 @@ def _prep_playlist_stack(xml): # We will never store clips (trailers) in the Kodi DB kodi_id = None kodi_type = None - resume, _ = api.getRuntime() for part, _ in enumerate(item[0]): api.setPartNumber(part) if kodi_id is None: @@ -164,7 +207,7 @@ def _prep_playlist_stack(xml): 'listitem': listitem, 'part': part, 'playcount': api.getViewCount(), - 'offset': resume + 'offset': api.getResume() }) return stack @@ -213,6 +256,7 @@ def conclude_playback(playqueue, pos): start playback return PKC listitem attached to result """ + LOG.info('Concluding playback for playqueue position %s', pos) result = Playback_Successful() listitem = PKC_ListItem() item = playqueue.items[pos] @@ -226,10 +270,16 @@ def conclude_playback(playqueue, pos): else: playurl = item.file listitem.setPath(playurl) - if item.playmethod in ("DirectStream", "DirectPlay"): + if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: playutils.audio_subtitle_prefs(listitem) listitem.setPath(playurl) + if state.RESUME_PLAYBACK is True: + state.RESUME_PLAYBACK = False + LOG.info('Resuming playback at %s', item.offset) + listitem.setProperty('StartOffset', str(item.offset)) + listitem.setProperty('resumetime', str(item.offset)) result.listitem = listitem pickle_me(result) + LOG.info('Done concluding playback') diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index af7e267d..e312232b 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -47,6 +47,7 @@ class PlayUtils(): }) self.item.playmethod = 'Transcode' LOG.info("The playurl is: %s", playurl) + self.item.file = playurl return playurl def isDirectPlay(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index db267be5..54c9f9a3 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -128,6 +128,9 @@ PLAYSTATE = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Set by SpecialMonitor - did user choose to resume playback or start from the +# beginning? +RESUME_PLAYBACK = False # Kodi webserver details WEBSERVER_PORT = 8080 diff --git a/service.py b/service.py index 246c2daa..04525b5b 100644 --- a/service.py +++ b/service.py @@ -31,7 +31,7 @@ sys_path.append(_base_resource) from utils import settings, window, language as lang, dialog, tryDecode from userclient import UserClient import initialsetup -from kodimonitor import KodiMonitor +from kodimonitor import KodiMonitor, SpecialMonitor from librarysync import LibrarySync import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket @@ -155,6 +155,7 @@ class Service(): self.alexa = Alexa_Websocket() self.library = LibrarySync() self.plexCompanion = PlexCompanion() + self.specialMonitor = SpecialMonitor() self.playback_starter = Playback_Starter() if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() @@ -197,6 +198,7 @@ class Service(): sound=False) # Start monitoring kodi events self.kodimonitor_running = KodiMonitor() + self.specialMonitor.start() # Start the Websocket Client if not self.ws_running: self.ws_running = True From e6520ad2e88ac661997364f4705a910d9ecdc2c3 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 07:59:53 +0100 Subject: [PATCH 192/509] Fix KeyError on playback start --- resources/lib/kodimonitor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4d4410ec..1eb5fe93 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -322,6 +322,13 @@ class KodiMonitor(Monitor): LOG.info('Aborting playback report - item invalid for updates %s', data) return + if playerid == -1: + # Kodi might return -1 for "last player" + try: + playerid = js.get_player_ids()[0] + except IndexError: + LOG.error('Could not retreive active player - aborting') + return # Remember that this player has been active state.ACTIVE_PLAYERS.append(playerid) playqueue = PQ.PLAYQUEUES[playerid] From 66f6605406232d561fe2c2002006675ab8f91145 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 08:07:19 +0100 Subject: [PATCH 193/509] Fix TypeError when logging weird PMS answers --- resources/lib/downloadutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 9c93a3f3..4c33a702 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -314,8 +314,8 @@ class DownloadUtils(): return None else: r.encoding = 'utf-8' - log.warn('Unknown answer from PMS %s with status code %s. ' - 'Message: %s', url, r.status_code, r.text) + log.warn('Unknown answer from PMS %s with status code %s. ', + url, r.status_code) return True # And now deal with the consequences of the exceptions From 4d79a17738ec33f59a8ab9b51e8762ff1592392f Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 19:07:05 +0100 Subject: [PATCH 194/509] Detect resume playback outside of widgets also --- resources/lib/kodimonitor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 1eb5fe93..7a247b84 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -418,7 +418,6 @@ class SpecialMonitor(Thread): if (not is_playing and getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and - not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and getInfoLabel('Control.GetLabel(1002)') == getLocalizedString(12021)): control = int(Window(10106).getFocusId()) if control == 1002: From 4b0fa90f5e09254f0d157e3f88b5e86414ac8067 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 19:10:18 +0100 Subject: [PATCH 195/509] Remove obsolete code --- resources/lib/kodimonitor.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7a247b84..93990f6d 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,12 +10,10 @@ from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, CatchExceptions, plex_command, \ - thread_methods +from utils import window, settings, plex_command, thread_methods from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER -from PlexAPI import API import playqueue as PQ import json_rpc as js import playlist_func as PL @@ -121,7 +119,6 @@ class KodiMonitor(Monitor): state.STOP_SYNC = False state.PATH_VERIFIED = False - @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): """ Called when a bunch of different stuff happens on the Kodi side @@ -383,26 +380,6 @@ class KodiMonitor(Monitor): status['playcount'] = item.playcount LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) - def StartDirectPath(self, plex_id, type, currentFile): - """ - Set some additional stuff if playback was initiated by Kodi, not PKC - """ - xml = self.doUtils('{server}/library/metadata/%s' % plex_id) - try: - xml[0].attrib - except: - LOG.error('Did not receive a valid XML for plex_id %s.' % plex_id) - return False - # Setup stuff, because playback was started by Kodi, not PKC - api = API(xml[0]) - listitem = api.CreateListItemFromPlexItem() - api.set_playback_win_props(currentFile, listitem) - if type == "song" and settings('streamMusic') == "true": - window('plex_%s.playmethod' % currentFile, value="DirectStream") - else: - window('plex_%s.playmethod' % currentFile, value="DirectPlay") - LOG.debug('Window properties set for direct paths!') - @thread_methods class SpecialMonitor(Thread): From e8d9252891337d9cc31b2ecd3b77ac8e452e182a Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 19:13:47 +0100 Subject: [PATCH 196/509] Prettify --- resources/lib/playutils.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index e312232b..89da5e70 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -2,7 +2,7 @@ ############################################################################### from logging import getLogger -from downloadutils import DownloadUtils +from downloadutils import DownloadUtils as DU from utils import window, settings, language as lang, dialog, tryEncode import variables as v @@ -15,9 +15,12 @@ LOG = getLogger("PLEX." + __name__) class PlayUtils(): def __init__(self, api, playqueue_item): + """ + init with api (PlexAPI wrapper of the PMS xml element) and + playqueue_item (Playlist_Item()) + """ self.api = api self.item = playqueue_item - self.doUtils = DownloadUtils().downloadUrl def getPlayUrl(self): """ @@ -322,9 +325,9 @@ class PlayUtils(): 'audioStreamID': audio_streams_list[resp], 'allParts': 1 } - self.doUtils('{server}/library/parts/%s' % part_id, - action_type='PUT', - parameters=args) + DU().downloadUrl('{server}/library/parts/%s' % part_id, + action_type='PUT', + parameters=args) if sub_num == 1: # No subtitles @@ -355,6 +358,6 @@ class PlayUtils(): 'subtitleStreamID': select_subs_index, 'allParts': 1 } - self.doUtils('{server}/library/parts/%s' % part_id, - action_type='PUT', - parameters=args) + DU().downloadUrl('{server}/library/parts/%s' % part_id, + action_type='PUT', + parameters=args) From 307806e65f968c4fdfed6c219b3c9ebbaa5feccd Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 23 Jan 2018 20:38:50 +0100 Subject: [PATCH 197/509] Fix playback starting in the background --- resources/lib/playback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 8e0dd586..a8908743 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -60,6 +60,8 @@ def playback_triage(plex_id=None, plex_type=None, path=None): try: playqueue.items[pos] except IndexError: + # Release our default.py before starting our own Kodi player instance + pickle_me(Playback_Successful()) playback_init(plex_id, plex_type, playqueue) else: # kick off playback on second pass @@ -156,10 +158,8 @@ def playback_init(plex_id, plex_type, playqueue): # LOG.info('Resume detected') # play_resume(playqueue, xml, stack) # return - # Release our default.py before starting our own Kodi player instance - pickle_me(Playback_Successful()) # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(100) + sleep(300) _process_stack(playqueue, stack) # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=Player().play, From db0d629302862ae9f288946aa882f49223148a74 Mon Sep 17 00:00:00 2001 From: Michal Kuncl Date: Tue, 23 Jan 2018 21:39:55 +0100 Subject: [PATCH 198/509] Restore cache directories after deleting. Fixes #392 --- resources/lib/artwork.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 300a6577..2146deb7 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -6,6 +6,7 @@ from Queue import Queue, Empty from shutil import rmtree from urllib import quote_plus, unquote from threading import Thread +from os import makedirs import requests from xbmc import sleep, translatePath @@ -136,6 +137,7 @@ class Artwork(): path = tryDecode(translatePath("special://thumbnails/")) if exists_dir(path): rmtree(path, ignore_errors=True) + self.restoreCacheDirectories() # remove all existing data from texture DB connection = kodiSQL('texture') @@ -335,7 +337,14 @@ class Artwork(): LOG.debug("Deleting cached thumbnail: %s" % path) if exists(path): rmtree(tryDecode(path), ignore_errors=True) + self.restoreCacheDirectories() cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: connection.close() + + def restoreCacheDirectories(self): + LOG.info("Restoring cache directories...") + paths = ("","0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","Video","plex") + for p in paths: + makedirs(tryDecode(translatePath("special://thumbnails/%s" % p))) From 510952f9def9d6184f03fda851f2ce693f732c16 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 24 Jan 2018 07:40:28 +0100 Subject: [PATCH 199/509] Avoid error in log file --- resources/lib/playback.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index a8908743..a146f96c 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -61,7 +61,9 @@ def playback_triage(plex_id=None, plex_type=None, path=None): playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance - pickle_me(Playback_Successful()) + result = Playback_Successful() + result.listitem = PKC_ListItem() + pickle_me(result) playback_init(plex_id, plex_type, playqueue) else: # kick off playback on second pass From cfff75926a7a67017fe8ec8d740dd219b520fa5d Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 25 Jan 2018 17:15:38 +0100 Subject: [PATCH 200/509] Revamp playback start, part 6 --- resources/lib/initialsetup.py | 4 ++ resources/lib/kodimonitor.py | 66 +++++++++++++++--------- resources/lib/playback.py | 13 +++-- resources/lib/player.py | 97 ++++++++++++----------------------- resources/lib/state.py | 6 +++ 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 4a8f9e83..8abdd2d1 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -415,6 +415,10 @@ class InitialSetup(): # Disable cleaning of library - not compatible with PKC xml.set_setting(['videolibrary', 'cleanonupdate'], value='false') + # Set completely watched point same as plex (and not 92%) + xml.set_setting(['video', 'ignorepercentatend'], value='10') + xml.set_setting(['video', 'playcountminimumpercent'], + value='90') reboot = xml.write_xml except etree.ParseError: cache = None diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 93990f6d..102df474 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,6 +14,7 @@ from utils import window, settings, plex_command, thread_methods from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER +from playback import playback_triage import playqueue as PQ import json_rpc as js import playlist_func as PL @@ -60,6 +61,7 @@ class KodiMonitor(Monitor): Monitor.__init__(self) for playerid in state.PLAYER_STATES: state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) + state.OLD_PLAYER_STATES[playerid] = dict(state.PLAYSTATE) LOG.info("Kodi monitor started.") def onScanStarted(self, library): @@ -143,32 +145,34 @@ class KodiMonitor(Monitor): # Manually marking as watched/unwatched playcount = data.get('playcount') item = data.get('item') + if playcount is None or item is None: + return try: kodiid = item['id'] item_type = item['type'] except (KeyError, TypeError): LOG.info("Item is invalid for playstate update.") + return + # Send notification to the server. + with plexdb.Get_Plex_DB() as plexcur: + plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type) + try: + itemid = plex_dbitem[0] + except TypeError: + LOG.error("Could not find itemid in plex database for a " + "video library update") else: - # Send notification to the server. - with plexdb.Get_Plex_DB() as plexcur: - plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type) - try: - itemid = plex_dbitem[0] - except TypeError: - LOG.error("Could not find itemid in plex database for a " - "video library update") + # Stop from manually marking as watched unwatched, with + # actual playback. + if window('plex_skipWatched%s' % itemid) == "true": + # property is set in player.py + window('plex_skipWatched%s' % itemid, clear=True) else: - # Stop from manually marking as watched unwatched, with - # actual playback. - if window('plex_skipWatched%s' % itemid) == "true": - # property is set in player.py - window('plex_skipWatched%s' % itemid, clear=True) + # notify the server + if playcount > 0: + scrobble(itemid, 'watched') else: - # notify the server - if playcount > 0: - scrobble(itemid, 'watched') - else: - scrobble(itemid, 'unwatched') + scrobble(itemid, 'unwatched') elif method == "VideoLibrary.OnRemove": pass elif method == "System.OnSleep": @@ -202,6 +206,21 @@ class KodiMonitor(Monitor): Will NOT be called if playback initiated by Kodi widgets """ playqueue = PQ.PLAYQUEUES[data['playlistid']] + # Kodi remembers the last setResolvedUrl - which is empty in our case + kodi_item = js.get_item(data['playlistid']) + LOG.debug('kodi_item: %s', kodi_item) + # if kodi_item.get('file') == '': + # LOG.info('Detected re-start of playback of last item') + # old = state.OLD_PLAYER_STATES[data['playlistid']] + # kwargs = { + # 'plex_id': old['plex_id'], + # 'plex_type': old['plex_type'], + # 'path': old['file'], + # 'resolve': False + # } + # thread = Thread(target=playback_triage, kwargs=kwargs) + # thread.start() + # return # Have we initiated the playqueue already? If not, ignore this if not playqueue.items: LOG.debug('Playqueue not initiated - ignoring') @@ -211,11 +230,10 @@ class KodiMonitor(Monitor): LOG.debug('PKC added this item to the playqueue - ignoring') return # Check whether we even need to update our known playqueue - kodi_playqueue = js.playlist_get_items(data['playlistid']) - if playqueue.old_kodi_pl == kodi_playqueue: - # We already know the latest playqueue (e.g. because Plex - # initiated playback) - return + # if playqueue.old_kodi_pl == kodi_playqueue: + # # We already know the latest playqueue (e.g. because Plex + # # initiated playback) + # return # Playlist has been updated; need to tell Plex about it if playqueue.id is None: PL.init_Plex_playlist(playqueue, kodi_item=data['item']) @@ -224,7 +242,7 @@ class KodiMonitor(Monitor): data['position'], kodi_item=data['item']) # Make sure that we won't re-add this item - playqueue.old_kodi_pl = kodi_playqueue + # playqueue.old_kodi_pl = kodi_playqueue @LOCKER.lockthis def _playlist_onremove(self, data): diff --git a/resources/lib/playback.py b/resources/lib/playback.py index a146f96c..5cb3b221 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -29,7 +29,7 @@ LOG = getLogger("PLEX." + __name__) @LOCKER.lockthis -def playback_triage(plex_id=None, plex_type=None, path=None): +def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): """ Hit this function for addon path playback, Plex trailers, etc. Will setup playback first, then on second call complete playback. @@ -40,6 +40,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None): If trailers or additional (movie-)parts are added, default.py is released and a completely new player instance is called with a new playlist. This circumvents most issues with Kodi & playqueues + + Set resolve to False if you do not want setResolvedUrl to be called on + the first pass - e.g. if you're calling this function from the original + service.py Python instance """ LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', plex_id, plex_type, path) @@ -61,9 +65,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None): playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance - result = Playback_Successful() - result.listitem = PKC_ListItem() - pickle_me(result) + if resolve is True: + result = Playback_Successful() + result.listitem = PKC_ListItem(path='PKC_Dummy_Path') + pickle_me(result) playback_init(plex_id, plex_type, playqueue) else: # kick off playback on second pass diff --git a/resources/lib/player.py b/resources/lib/player.py index 73ea0463..49436094 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -5,10 +5,8 @@ from logging import getLogger from xbmc import Player -from utils import window, DateToKodi, getUnixTimestamp, kodi_time_to_millis +from utils import window from downloadutils import DownloadUtils as DU -import plexdb_functions as plexdb -import kodidb_functions as kodidb from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -20,6 +18,35 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### +@LOCKER.lockthis +def playback_cleanup(): + """ + PKC cleanup after playback ends/is stopped + """ + # We might have saved a transient token from a user flinging media via + # Companion (if we could not use the playqueue to store the token) + state.PLEX_TRANSIENT_TOKEN = None + for item in ('plex_customplaylist', + 'plex_customplaylist.seektime', + 'plex_forcetranscode'): + window(item, clear=True) + for playerid in state.ACTIVE_PLAYERS: + status = state.PLAYER_STATES[playerid] + # Remember the last played item later + state.OLD_PLAYER_STATES[playerid] = dict(status) + # Stop transcoding + if status['playmethod'] == 'Transcode': + LOG.info('Tell the PMS to stop transcoding') + DU().downloadUrl( + '{server}/video/:/transcode/universal/stop', + parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + # Reset the player's status + status = dict(state.PLAYSTATE) + # As all playback has halted, reset the players that have been active + state.ACTIVE_PLAYERS = [] + LOG.info('Finished PKC playback cleanup') + + class PKC_Player(Player): def __init__(self): Player.__init__(self) @@ -54,71 +81,11 @@ class PKC_Player(Player): Will be called when playback is stopped by the user """ LOG.info("ONPLAYBACK_STOPPED") - self.cleanup_playback() + playback_cleanup() def onPlayBackEnded(self): """ Will be called when playback ends due to the media file being finished """ LOG.info("ONPLAYBACK_ENDED") - self.cleanup_playback() - - @LOCKER.lockthis - def cleanup_playback(self): - """ - PKC cleanup after playback ends/is stopped - """ - # We might have saved a transient token from a user flinging media via - # Companion (if we could not use the playqueue to store the token) - state.PLEX_TRANSIENT_TOKEN = None - for item in ('plex_currently_playing_itemid', - 'plex_customplaylist', - 'plex_customplaylist.seektime', - 'plex_forcetranscode'): - window(item, clear=True) - for playerid in state.ACTIVE_PLAYERS: - status = state.PLAYER_STATES[playerid] - # Check whether we need to mark an item as completely watched - if not status['kodi_id'] or not status['plex_id']: - LOG.info('No PKC info safed for the element just played by Kodi' - ' player %s', playerid) - continue - # Stop transcoding - if status['playmethod'] == 'Transcode': - LOG.info('Tell the PMS to stop transcoding') - DU().downloadUrl( - '{server}/video/:/transcode/universal/stop', - parameters={'session': v.PKC_MACHINE_IDENTIFIER}) - if status['plex_type'] == v.PLEX_TYPE_SONG: - LOG.debug('Song has been played, not cleaning up playstate') - continue - resume = kodi_time_to_millis(status['time']) - runtime = kodi_time_to_millis(status['totaltime']) - LOG.info('Item playback progress %s out of %s', resume, runtime) - if not resume or not runtime: - continue - complete = float(resume) / float(runtime) - LOG.info("Percent complete: %s. Mark played at: %s", - complete, v.MARK_PLAYED_AT) - if complete >= v.MARK_PLAYED_AT: - # Tell Kodi that we've finished watching (Plex knows) - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byId(status['plex_id']) - file_id = plex_dbitem[1] if plex_dbitem else None - if file_id is None: - LOG.error('No file_id found for %s', status) - continue - with kodidb.GetKodiDB('video') as kodi_db: - kodi_db.addPlaystate( - file_id, - None, - None, - status['playcount'] + 1, - DateToKodi(getUnixTimestamp())) - LOG.info('Marked plex element %s as completely watched', - status['plex_id']) - # As all playback has halted, reset the players that have been active - state.ACTIVE_PLAYERS = [] - for playerid in state.PLAYER_STATES: - state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) - LOG.info('Finished PKC playback cleanup') + playback_cleanup() diff --git a/resources/lib/state.py b/resources/lib/state.py index 54c9f9a3..c2a99e09 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -91,6 +91,12 @@ PLAYER_STATES = { 2: {}, 3: {} } +# The LAST playstate once playback is finished +OLD_PLAYER_STATES = { + 1: {}, + 2: {}, + 3: {} +} # "empty" dict for the PLAYER_STATES above PLAYSTATE = { 'type': None, From 6e6fbadb02c906111696e2cf16d991e495cd2aca Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 26 Jan 2018 09:47:58 +0100 Subject: [PATCH 201/509] Fix repeated playback of same resumable item --- resources/lib/kodimonitor.py | 32 ++++++++++++++++++++------------ resources/lib/playback.py | 8 ++++++++ resources/lib/state.py | 2 ++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 102df474..0bebae33 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -209,18 +209,24 @@ class KodiMonitor(Monitor): # Kodi remembers the last setResolvedUrl - which is empty in our case kodi_item = js.get_item(data['playlistid']) LOG.debug('kodi_item: %s', kodi_item) - # if kodi_item.get('file') == '': - # LOG.info('Detected re-start of playback of last item') - # old = state.OLD_PLAYER_STATES[data['playlistid']] - # kwargs = { - # 'plex_id': old['plex_id'], - # 'plex_type': old['plex_type'], - # 'path': old['file'], - # 'resolve': False - # } - # thread = Thread(target=playback_triage, kwargs=kwargs) - # thread.start() - # return + if (state.RESUMABLE is True and + data['position'] == 0 and + data['item'].get('title') is not None and + getCondVisibility('Window.IsVisible(MyVideoNav.xml)')): + # Hack we need for RESUMABLE items because Kodi lost the path of the + # last played item that is now being replayed (see playback.py's + # Player().play()) + LOG.info('Detected re-start of playback of last item') + old = state.OLD_PLAYER_STATES[data['playlistid']] + kwargs = { + 'plex_id': old['plex_id'], + 'plex_type': old['plex_type'], + 'path': old['file'], + 'resolve': False + } + thread = Thread(target=playback_triage, kwargs=kwargs) + thread.start() + return # Have we initiated the playqueue already? If not, ignore this if not playqueue.items: LOG.debug('Playqueue not initiated - ignoring') @@ -414,6 +420,8 @@ class SpecialMonitor(Thread): if (not is_playing and getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and getInfoLabel('Control.GetLabel(1002)') == getLocalizedString(12021)): + # Remember that the item IS indeed resumable + state.RESUMABLE = True control = int(Window(10106).getFocusId()) if control == 1002: # Start from beginning diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 5cb3b221..d6d05219 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -173,6 +173,12 @@ def playback_init(plex_id, plex_type, playqueue): args=(playqueue.kodi_pl, )) thread.setDaemon(True) LOG.info('Done initializing PKC playback, starting Kodi player') + # By design, PKC will start Kodi playback using Player().play(). Kodi + # caches paths like our plugin://pkc. If we use Player().play() between + # 2 consecutive startups of exactly the same Kodi library item, Kodi's + # cache will have been flushed for some reason. Hence the 2nd call for + # plugin://pkc will be lost; Kodi will try to startup playback for an empty + # path: log entry is "CGUIWindowVideoBase::OnPlayMedia " thread.start() @@ -287,6 +293,8 @@ def conclude_playback(playqueue, pos): LOG.info('Resuming playback at %s', item.offset) listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('resumetime', str(item.offset)) + # Reset the resumable flag + state.RESUMABLE = False result.listitem = listitem pickle_me(result) LOG.info('Done concluding playback') diff --git a/resources/lib/state.py b/resources/lib/state.py index c2a99e09..50578541 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -134,6 +134,8 @@ PLAYSTATE = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} +# Flag whether Kodi item where the playback is being started is even resumable +RESUMABLE = False # Set by SpecialMonitor - did user choose to resume playback or start from the # beginning? RESUME_PLAYBACK = False From 906f61a847f569143419ea8bfec19b2ad2ee781f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 12:52:31 +0100 Subject: [PATCH 202/509] Fix resume playback for extended context menu --- resources/lib/kodimonitor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 0bebae33..f1eb6841 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -414,21 +414,23 @@ class SpecialMonitor(Thread): def run(self): LOG.info("----====# Starting Special Monitor #====----") player = Player() + # "Start from beginning", "Play from beginning" + strings = (getLocalizedString(12021), getLocalizedString(12023)) while not self.thread_stopped(): is_playing = player.isPlaying() - if (not is_playing and getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and - getInfoLabel('Control.GetLabel(1002)') == getLocalizedString(12021)): + getInfoLabel('Control.GetLabel(1002)') in strings): # Remember that the item IS indeed resumable state.RESUMABLE = True control = int(Window(10106).getFocusId()) if control == 1002: # Start from beginning - LOG.info("Resume dialog: Start from beginning selected") state.RESUME_PLAYBACK = False - else: - LOG.info("Resume dialog: resume selected") + elif control == 1001: state.RESUME_PLAYBACK = True - sleep(200) + else: + # User chose something else from the context menu + state.RESUME_PLAYBACK = False + sleep(100) LOG.info("#====---- Special Monitor Stopped ----====#") From dde330a704fcb5c3f6806ef5cce6e766e50d3be7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:23:47 +0100 Subject: [PATCH 203/509] Fix requests verify ssl certificate --- resources/lib/downloadutils.py | 19 ++++++++++--------- resources/lib/kodimonitor.py | 6 +++++- resources/lib/librarysync.py | 21 --------------------- resources/lib/state.py | 4 ++++ service.py | 19 +++++++++++++++++++ 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 4c33a702..377df6a3 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -67,15 +67,15 @@ class DownloadUtils(): certificate must be path to certificate or 'None' """ if verifySSL is None: - verifySSL = settings('sslverify') + verifySSL = state.VERIFY_SSL_CERT if certificate is None: - certificate = settings('sslcert') - log.debug("Verify SSL certificates set to: %s" % verifySSL) - log.debug("SSL client side certificate set to: %s" % certificate) - if verifySSL != 'true': - self.s.verify = False - if certificate != 'None': + certificate = state.SSL_CERT_PATH + # Set the session's parameters + self.s.verify = verifySSL + if certificate: self.s.cert = certificate + log.debug("Verify SSL certificates set to: %s", verifySSL) + log.debug("SSL client side certificate set to: %s", certificate) def startSession(self, reset=False): """ @@ -177,8 +177,9 @@ class DownloadUtils(): headerOptions = self.getHeader(options=headerOptions) else: headerOptions = headerOverride - if settings('sslcert') != 'None': - kwargs['cert'] = settings('sslcert') + kwargs['verify'] = state.VERIFY_SSL_CERT + if state.SSL_CERT_PATH: + kwargs['cert'] = state.SSL_CERT_PATH # Set the variables we were passed (fallback to request session # otherwise - faster) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index f1eb6841..7813568e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -46,7 +46,9 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'enableBackgroundSync': 'BACKGROUND_SYNC' + 'enableBackgroundSync': 'BACKGROUND_SYNC', + 'sslverify': 'VERIFY_SSL_CERT', + 'sslcert': 'SSL_CERT_PATH' } ############################################################################### @@ -113,6 +115,8 @@ class KodiMonitor(Monitor): state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) + state.SSL_CERT_PATH = settings('sslcert') \ + if settings('sslcert') != 'None' else None # Never set through the user # state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) if changed is True: diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index f7df635d..8c6dc8c8 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -50,33 +50,12 @@ class LibrarySync(Thread): if settings('FanartTV') == 'true': self.fanartthread = Process_Fanart_Thread(self.fanartqueue) # How long should we wait at least to process new/changed PMS items? - self.user = userclient.UserClient() self.vnodes = videonodes.VideoNodes() self.xbmcplayer = xbmc.Player() - self.installSyncDone = settings('SyncInstallRunDone') == 'true' - - state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 - state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) - state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' - state.ENABLE_MUSIC = settings('enableMusic') == 'true' - state.BACKGROUND_SYNC = settings( - 'enableBackgroundSync') == 'true' - state.BACKGROUNDSYNC_SAFTYMARGIN = int( - settings('backgroundsync_saftyMargin')) - # Show sync dialog even if user deactivated? self.force_dialog = True - # Init for replacing paths - state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' - state.REMAP_PATH = settings('remapSMB') == 'true' - for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): - for arg in ('Org', 'New'): - key = 'remapSMB%s%s' % (typus, arg) - setattr(state, key, settings(key)) - # Just in case a time sync goes wrong - state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) Thread.__init__(self) def showKodiNote(self, message, icon="plex"): diff --git a/resources/lib/state.py b/resources/lib/state.py index 50578541..6ac85f40 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -63,6 +63,10 @@ remapSMBmusicNew = None remapSMBphotoOrg = None remapSMBphotoNew = None +# Shall we verify SSL certificates? +VERIFY_SSL_CERT = False +# Do we have an ssl certificate for PKC we need to use? +SSL_CERT_PATH = None # Along with window('plex_authenticated') AUTHENTICATED = False # plex.tv username diff --git a/service.py b/service.py index 04525b5b..72d7fed3 100644 --- a/service.py +++ b/service.py @@ -120,6 +120,25 @@ class Service(): videonodes.VideoNodes().clearProperties() # Init some stuff + state.VERIFY_SSL_CERT = settings('sslverify') == 'true' + state.SSL_CERT_PATH = settings('sslcert') \ + if settings('sslcert') != 'None' else None + state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 + state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) + state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' + state.ENABLE_MUSIC = settings('enableMusic') == 'true' + state.BACKGROUND_SYNC = settings( + 'enableBackgroundSync') == 'true' + state.BACKGROUNDSYNC_SAFTYMARGIN = int( + settings('backgroundsync_saftyMargin')) + state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' + state.REMAP_PATH = settings('remapSMB') == 'true' + for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + setattr(state, key, settings(key)) + state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) + window('plex_minDBVersion', value="1.5.10") set_webserver() self.monitor = Monitor() From 05f9f56a4d2261f57183d7bcec0b64d8913a714d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:24:42 +0100 Subject: [PATCH 204/509] More logging --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index d6d05219..ad7bc14b 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -67,7 +67,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): # Release our default.py before starting our own Kodi player instance if resolve is True: result = Playback_Successful() - result.listitem = PKC_ListItem(path='PKC_Dummy_Path') + result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) playback_init(plex_id, plex_type, playqueue) else: From 2243bc42aab514a0d13d03d7c5ce0a57ef4d267a Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:26:25 +0100 Subject: [PATCH 205/509] Fix release of default.py --- resources/lib/playback.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ad7bc14b..221371b1 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -49,8 +49,9 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): plex_id, plex_type, path) if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') - # Release default.py - pickle_me(Playback_Successful()) + if resolve is True: + # Release default.py + pickle_me(Playback_Successful()) # "Unauthorized for PMS" dialog('notification', lang(29999), lang(30017)) return From d1fc9c0bfff1c2915689668283f8edf675d15156 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:28:05 +0100 Subject: [PATCH 206/509] Let Kodi decide whether item is resumable --- resources/lib/playback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 221371b1..c9cb8dab 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -132,9 +132,8 @@ def playback_init(plex_id, plex_type, playqueue): dialog('notification', lang(29999), lang(30128), icon='{error}') return api = API(xml[0]) - resume, _ = api.getRuntime() trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not resume and + if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUMABLE and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" From f32d2cfcfc5e806bef5985b28bf0b6e91386c72e Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:28:29 +0100 Subject: [PATCH 207/509] Revert "Let Kodi decide whether item is resumable" This reverts commit d1fc9c0bfff1c2915689668283f8edf675d15156. --- resources/lib/playback.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index c9cb8dab..221371b1 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -132,8 +132,9 @@ def playback_init(plex_id, plex_type, playqueue): dialog('notification', lang(29999), lang(30128), icon='{error}') return api = API(xml[0]) + resume, _ = api.getRuntime() trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUMABLE and + if (plex_type == v.PLEX_TYPE_MOVIE and not resume and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" From 15f6d7bf18f5ca8b4598c6baac88895646eee797 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:29:27 +0100 Subject: [PATCH 208/509] Let Kodi decide whether an item is resumable --- resources/lib/playback.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 221371b1..a8036e50 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -131,10 +131,8 @@ def playback_init(plex_id, plex_type, playqueue): # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') return - api = API(xml[0]) - resume, _ = api.getRuntime() trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not resume and + if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUMABLE and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" From ec0d382206f24ef822aba7e6cf6c7e4a283333be Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:30:50 +0100 Subject: [PATCH 209/509] Fix releasing of default.py --- resources/lib/playback.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index a8036e50..233fe579 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -126,8 +126,6 @@ def playback_init(plex_id, plex_type, playqueue): xml[0].attrib except (IndexError, TypeError, AttributeError): LOG.error('Could not get a PMS xml for plex id %s', plex_id) - # Release default.py - pickle_me(Playback_Successful()) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') return @@ -149,8 +147,6 @@ def playback_init(plex_id, plex_type, playqueue): if xml is None: LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', plex_id, xml.attrib.get('librarySectionUUID')) - # Release default.py - pickle_me(Playback_Successful()) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') return From 8da730ed8da9bcc7bebff6fa7b37ff093711ab6d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:31:35 +0100 Subject: [PATCH 210/509] Wait a bit less --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 233fe579..b9d6a334 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -161,7 +161,7 @@ def playback_init(plex_id, plex_type, playqueue): # play_resume(playqueue, xml, stack) # return # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(300) + sleep(200) _process_stack(playqueue, stack) # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=Player().play, From bc26d53945c58f0bdf83187da49637529f9c19a8 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 13:55:00 +0100 Subject: [PATCH 211/509] Fix Kodi suddenly marking item as played --- resources/lib/librarysync.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 8c6dc8c8..f64014ab 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1290,6 +1290,10 @@ class LibrarySync(Thread): if status == 'buffering': continue ratingKey = str(item['ratingKey']) + for pid in (1, 2, 3): + if ratingKey == state.PLAYER_STATES[pid]['plex_id']: + # Kodi is playing this item - no need to set the playstate + continue with plexdb.Get_Plex_DB() as plex_db: kodi_info = plex_db.getItem_byId(ratingKey) if kodi_info is None: From 0e3a7a1673d4251e1b2d59839597b63e58488782 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 15:24:41 +0100 Subject: [PATCH 212/509] Encode listitem paths --- resources/lib/playback.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index b9d6a334..ea9371bb 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -16,7 +16,7 @@ from playutils import PlayUtils from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful import json_rpc as js -from utils import window, settings, dialog, language as lang +from utils import window, settings, dialog, language as lang, tryEncode from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -99,7 +99,7 @@ def play_resume(playqueue, xml, stack): api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() - listitem.setPath(playurl) + listitem.setPath(tryEncode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: @@ -202,7 +202,7 @@ def _prep_playlist_stack(xml): path = ('plugin://plugin.video.plexkodiconnect?%s' % urlencode(params)) listitem = api.CreateListItemFromPlexItem() - listitem.setPath(path) + listitem.setPath(tryEncode(path)) else: # Will add directly via the Kodi DB path = None @@ -277,12 +277,11 @@ def conclude_playback(playqueue, pos): playurl = playutils.getPlayUrl() else: playurl = item.file - listitem.setPath(playurl) + listitem.setPath(tryEncode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: playutils.audio_subtitle_prefs(listitem) - listitem.setPath(playurl) if state.RESUME_PLAYBACK is True: state.RESUME_PLAYBACK = False LOG.info('Resuming playback at %s', item.offset) From dfd5297cd305c001db922bbed366b2149d9c4e40 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 17:21:28 +0100 Subject: [PATCH 213/509] Revamp playback start, part 7 --- resources/lib/entrypoint.py | 7 +- resources/lib/playback.py | 72 ++++++++++++++++-- resources/lib/playback_starter.py | 120 ++---------------------------- 3 files changed, 75 insertions(+), 124 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index a4913489..7305a3cd 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -820,7 +820,7 @@ def __build_item(xml_element): params = { 'mode': 'plex_node', 'key': xml_element.attrib.get('key'), - 'view_offset': xml_element.attrib.get('viewOffset', '0'), + 'offset': xml_element.attrib.get('viewOffset', '0'), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) elif api.getType() == v.PLEX_TYPE_PHOTO: @@ -828,9 +828,8 @@ def __build_item(xml_element): else: params = { 'mode': 'play', - 'filename': api.getKey(), - 'id': api.getRatingKey(), - 'dbid': listitem.getProperty('dbid') + 'plex_id': api.getRatingKey(), + 'plex_type': api.getType(), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) xbmcplugin.addDirectoryItem(handle=HANDLE, diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ea9371bb..76b06077 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -9,6 +9,7 @@ from xbmc import Player, sleep from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue +from downloadutils import DownloadUtils as DU import plexdb_functions as plexdb import playlist_func as PL import playqueue as PQ @@ -28,6 +29,71 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### +def process_indirect(key, offset, resolve=True): + """ + Called e.g. for Plex "Play later" - Plex items where we need to fetch an + additional xml for the actual playurl. In the PMS metadata, indirect="1" is + set. + + Will release default.py with setResolvedUrl + + Set resolve to False if playback should be kicked off directly, not via + setResolvedUrl + """ + LOG.info('process_indirect called with key: %s, offset: %s', key, offset) + result = Playback_Successful() + if key.startswith('http') or key.startswith('{server}'): + xml = DU().downloadUrl(key) + else: + xml = DU().downloadUrl('{server}%s' % key) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download PMS metadata') + if resolve is True: + # Release default.py + pickle_me(result) + return + if offset != '0': + offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) + # Todo: implement offset + api = API(xml[0]) + listitem = PKC_ListItem() + api.CreateListItemFromPlexItem(listitem) + playqueue = PQ.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + playqueue.clear() + item = PL.Playlist_Item() + item.xml = xml[0] + item.offset = int(offset) + item.plex_type = v.PLEX_TYPE_CLIP + item.playmethod = 'DirectStream' + item.init_done = True + # Need to get yet another xml to get the final playback url + xml = DU().downloadUrl('http://node.plexapp.com:32400%s' + % xml[0][0][0].attrib['key']) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download last xml for playurl') + if resolve is True: + # Release default.py + pickle_me(result) + return + playurl = xml[0].attrib['key'] + item.file = playurl + listitem.setPath(tryEncode(playurl)) + playqueue.items.append(item) + if resolve is True: + result.listitem = listitem + pickle_me(result) + else: + thread = Thread(target=Player().play, + args={'item': tryEncode(playurl), 'listitem': listitem}) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + thread.start() + @LOCKER.lockthis def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): """ @@ -154,12 +220,6 @@ def playback_init(plex_id, plex_type, playqueue): playqueue.clear() PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) - # if resume: - # # Need to handle this differently so only 1 dialog is displayed whether - # # user wants to resume to start at the beginning - # LOG.info('Resume detected') - # play_resume(playqueue, xml, stack) - # return # Sleep a bit to let setResolvedUrl do its thing - bit ugly sleep(200) _process_stack(playqueue, stack) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index b1491ee8..dcc7a59f 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -4,20 +4,8 @@ from logging import getLogger from threading import Thread from urlparse import parse_qsl -from xbmc import Player - -from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful -from playbackutils import PlaybackUtils import playback -from utils import window -from PlexFunctions import GetPlexMetadata -from PlexAPI import API -import playqueue as PQ -import variables as v -from downloadutils import DownloadUtils -from PKC_listitem import convert_PKC_to_listitem -import plexdb_functions as plexdb from context_entry import ContextMenu import state @@ -32,118 +20,22 @@ class Playback_Starter(Thread): """ Processes new plays """ - 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 not state.AUTHENTICATED: - LOG.error('Not yet authenticated for PMS, abort starting playback') - # Todo: Warn user with dialog - return - xml = GetPlexMetadata(plex_id) - try: - xml[0].attrib - except (IndexError, TypeError, AttributeError): - LOG.error('Could not get a PMS xml for plex id %s', plex_id) - return - api = API(xml[0]) - if api.getType() == v.PLEX_TYPE_PHOTO: - # Photo - result = Playback_Successful() - listitem = PKC_ListItem() - listitem = api.CreateListItemFromPlexItem(listitem) - result.listitem = listitem - else: - # Video and Music - playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - with PQ.LOCK: - result = PlaybackUtils(xml, playqueue).play( - plex_id, - kodi_id, - xml.attrib.get('librarySectionUUID')) - LOG.info('Done process_play, playqueues: %s', PQ.PLAYQUEUES) - return result - - def process_plex_node(self, url, viewOffset, directplay=False, - node=True): - """ - Called for Plex directories or redirect for playback (e.g. trailers, - clips, watchlater) - """ - LOG.info('process_plex_node called with url: %s, viewOffset: %s', - url, viewOffset) - # Plex redirect, e.g. watch later. Need to get actual URLs - if url.startswith('http') or url.startswith('{server}'): - xml = DownloadUtils().downloadUrl(url) - else: - xml = DownloadUtils().downloadUrl('{server}%s' % url) - try: - xml[0].attrib - except: - LOG.error('Could not download PMS metadata') - return - if viewOffset != '0': - try: - viewOffset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(viewOffset)) - except: - pass - else: - window('plex_customplaylist.seektime', value=str(viewOffset)) - LOG.info('Set resume point to %s', viewOffset) - api = API(xml[0]) - typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()] - if node is True: - plex_id = None - kodi_id = 'plexnode' - else: - plex_id = api.getRatingKey() - kodi_id = None - with plexdb.Get_Plex_DB() as plex_db: - plexdb_item = plex_db.getItem_byId(plex_id) - try: - kodi_id = plexdb_item[0] - except TypeError: - LOG.info('Couldnt find item %s in Kodi db', - api.getRatingKey()) - playqueue = PQ.get_playqueue_from_type(typus) - with PQ.LOCK: - result = PlaybackUtils(xml, playqueue).play( - plex_id, - kodi_id=kodi_id, - plex_lib_UUID=xml.attrib.get('librarySectionUUID')) - if directplay: - if result.listitem: - listitem = convert_PKC_to_listitem(result.listitem) - Player().play(listitem.getfilename(), listitem) - return Playback_Successful() - else: - return result - def triage(self, item): _, params = item.split('?', 1) params = dict(parse_qsl(params)) mode = params.get('mode') LOG.debug('Received mode: %s, params: %s', mode, params) if mode == 'play': - result = playback.playback_triage(plex_id=params.get('plex_id'), - plex_type=params.get('plex_type'), - path=params.get('path')) - elif mode == 'companion': - result = self.process_companion() + playback.playback_triage(plex_id=params.get('plex_id'), + plex_type=params.get('plex_type'), + path=params.get('path')) elif mode == 'plex_node': - result = self.process_plex_node( - params.get('key'), - params.get('view_offset'), - directplay=True if params.get('play_directly') else False, - node=False if params.get('node') == 'false' else True) + playback.process_indirect(params['key'], params['offset']) elif mode == 'context_menu': ContextMenu() result = Playback_Successful() - # Let default.py know! - # pickle_me(result) + # Let default.py know! + pickle_me(result) def run(self): queue = state.COMMAND_PIPELINE_QUEUE From 83833d76b3d8651360d9a78b832c05997bef9d67 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 17:28:02 +0100 Subject: [PATCH 214/509] Fix missing resume points --- resources/lib/entrypoint.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 7305a3cd..37c3bda2 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -815,6 +815,9 @@ def __build_folder(xml_element, plex_section_id=None): def __build_item(xml_element): api = API(xml_element) listitem = api.CreateListItemFromPlexItem() + resume = api.getResume() + if resume: + listitem.setProperty('resumetime', str(resume)) if (api.getKey().startswith('/system/services') or api.getKey().startswith('http')): params = { From 2d8bd3051a15224836ff47ede8f9ed09c0010b74 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 17:36:36 +0100 Subject: [PATCH 215/509] Fix PKC restoring cache directories --- resources/lib/artwork.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 2146deb7..61768b04 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -337,13 +337,13 @@ class Artwork(): LOG.debug("Deleting cached thumbnail: %s" % path) if exists(path): rmtree(tryDecode(path), ignore_errors=True) - self.restoreCacheDirectories() cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: connection.close() - def restoreCacheDirectories(self): + @staticmethod + def restoreCacheDirectories(): LOG.info("Restoring cache directories...") paths = ("","0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","Video","plex") for p in paths: From e6612364402c5a699933066c32e2c2cac0eb9d59 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 17:53:51 +0100 Subject: [PATCH 216/509] Fix resume playback outside Kodi library --- resources/lib/kodimonitor.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7813568e..766587a2 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -210,10 +210,12 @@ class KodiMonitor(Monitor): Will NOT be called if playback initiated by Kodi widgets """ playqueue = PQ.PLAYQUEUES[data['playlistid']] - # Kodi remembers the last setResolvedUrl - which is empty in our case + # Did PKC cause this add? Then lets not do anything + if playqueue.is_kodi_onadd() is False: + LOG.debug('PKC added this item to the playqueue - ignoring') + return kodi_item = js.get_item(data['playlistid']) - LOG.debug('kodi_item: %s', kodi_item) - if (state.RESUMABLE is True and + if (state.RESUMABLE is True and not kodi_item['file'] and data['position'] == 0 and data['item'].get('title') is not None and getCondVisibility('Window.IsVisible(MyVideoNav.xml)')): @@ -235,15 +237,6 @@ class KodiMonitor(Monitor): if not playqueue.items: LOG.debug('Playqueue not initiated - ignoring') return - # Did PKC cause this add? Then lets not do anything - if playqueue.is_kodi_onadd() is False: - LOG.debug('PKC added this item to the playqueue - ignoring') - return - # Check whether we even need to update our known playqueue - # if playqueue.old_kodi_pl == kodi_playqueue: - # # We already know the latest playqueue (e.g. because Plex - # # initiated playback) - # return # Playlist has been updated; need to tell Plex about it if playqueue.id is None: PL.init_Plex_playlist(playqueue, kodi_item=data['item']) From fd2c6115fca84e22e4ea1dfe8b428f7eaca27bf9 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 17:56:33 +0100 Subject: [PATCH 217/509] Reduce logging --- resources/lib/kodimonitor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 766587a2..2b033690 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -46,9 +46,7 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'enableBackgroundSync': 'BACKGROUND_SYNC', - 'sslverify': 'VERIFY_SSL_CERT', - 'sslcert': 'SSL_CERT_PATH' + 'enableBackgroundSync': 'BACKGROUND_SYNC' } ############################################################################### From a6e9869a14ab055b6160228f5d7b222fdc4fdf5d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 28 Jan 2018 18:06:30 +0100 Subject: [PATCH 218/509] Fix channels playback --- resources/lib/playback.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 76b06077..e208bfdd 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -44,6 +44,8 @@ def process_indirect(key, offset, resolve=True): result = Playback_Successful() if key.startswith('http') or key.startswith('{server}'): xml = DU().downloadUrl(key) + elif key.startswith('/system/services'): + xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key) else: xml = DU().downloadUrl('{server}%s' % key) try: From fc9ea2444e3f78ab65a9e231ff05f4d7c95a71ed Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 30 Jan 2018 07:50:44 +0100 Subject: [PATCH 219/509] Introduce PlaylistError exception --- resources/lib/playlist_func.py | 115 +++++++++++++++------------------ 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 77ec1bf5..e9bd004d 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -21,8 +21,12 @@ LOG = getLogger("PLEX." + __name__) REGEX = re_compile(r'''metadata%2F(\d+)''') ############################################################################### -# kodi_item dict: -# {u'type': u'movie', u'id': 3, 'file': path-to-file} + +class PlaylistError(Exception): + """ + Exception for our playlist constructs + """ + pass class PlaylistObjectBaseclase(object): @@ -361,30 +365,36 @@ def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, 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 + playQueueVersion). + + Raises PlaylistError if unsuccessful """ 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) - return False - return True + raise PlaylistError('Could not get new playlist Version for playlist ' + '%s', playlist) 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 + + Raises PlaylistError if something went wrong. """ - playlist.id = xml.attrib['%sID' % playlist.kind] - playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] - playlist.selectedItemID = xml.attrib.get( - '%sSelectedItemID' % playlist.kind) - playlist.selectedItemOffset = xml.attrib.get( - '%sSelectedItemOffset' % playlist.kind) - LOG.debug('Updated playlist from xml: %s', playlist) + try: + playlist.id = xml.attrib['%sID' % playlist.kind] + playlist.version = xml.attrib['%sVersion' % playlist.kind] + playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] + playlist.selectedItemID = xml.attrib.get( + '%sSelectedItemID' % playlist.kind) + playlist.selectedItemOffset = xml.attrib.get( + '%sSelectedItemOffset' % playlist.kind) + LOG.debug('Updated playlist from xml: %s', playlist) + except (TypeError, KeyError, AttributeError) as msg: + raise PlaylistError('Could not get playlist details from xml: %s', + msg) def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): @@ -399,11 +409,7 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): # Clear our existing playlist and the associated Kodi playlist playlist.clear() # Set new values - try: - get_playlist_details_from_xml(playlist, xml) - except KeyError: - LOG.error('Could not update playlist from PMS') - return + get_playlist_details_from_xml(playlist, xml) for plex_item in xml: playlist_item = add_to_Kodi_playlist(playlist, plex_item) if playlist_item is not None: @@ -415,7 +421,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Initializes the Plex side without changing the Kodi playlists WILL ALSO UPDATE OUR PLAYLISTS. - Returns True if successful, False otherwise + Returns the first PKC playlist item or raises PlaylistError """ LOG.debug('Initializing the playlist %s on the Plex side', playlist) playlist.clear() @@ -436,12 +442,11 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): # Need to get the details for the playlist item item = playlist_item_from_xml(playlist, xml[0]) except (KeyError, IndexError, TypeError): - LOG.error('Could not init Plex playlist with plex_id %s and ' - 'kodi_item %s', plex_id, kodi_item) - return False + raise PlaylistError('Could not init Plex playlist with plex_id %s and ' + 'kodi_item %s', plex_id, kodi_item) playlist.items.append(item) LOG.debug('Initialized the playlist on the Plex side: %s', playlist) - return True + return item def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None, @@ -479,19 +484,16 @@ 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] + file: str! - file: str! + Raises PlaylistError if something went wrong """ 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: - success = init_Plex_playlist(playlist, plex_id, kodi_item) + item = init_Plex_playlist(playlist, plex_id, kodi_item) else: - success = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) - if success is False: - return False - # Now add the item to the Kodi playlist - WITHOUT adding it to our PKC pl - item = playlist.items[pos] + item = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item) params = { 'playlistid': playlist.playlistid, 'position': pos @@ -503,10 +505,10 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: - LOG.error('Could not add item to playlist. Kodi reply. %s', reply) playlist.is_kodi_onadd() - return False - return True + raise PlaylistError('Could not add item to playlist. Kodi reply. %s', + reply) + return item def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): @@ -515,14 +517,10 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): side of things (e.g. because the user changed the Kodi side) WILL ALSO UPDATE OUR PLAYLISTS - Returns True if successful, False otherwise + Returns the PKC PlayList item or raises PlaylistError """ if plex_id: - try: - item = playlist_item_from_plex(plex_id) - except KeyError: - LOG.error('Could not add new item to the PMS playlist') - return False + 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) @@ -534,8 +532,8 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): 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) - return False + raise PlaylistError('Could not add item %s to playlist %s', + kodi_item, playlist) # Get the guid for this item for plex_item in xml: if plex_item.attrib['%sItemID' % playlist.kind] == item.id: @@ -550,7 +548,7 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): len(playlist.items) - 1, pos) LOG.debug('Successfully added item on the Plex side: %s', playlist) - return True + return item def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, @@ -558,7 +556,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, """ Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS - Returns the playlist item that was just added or None + Returns the playlist item that was just added or raises PlaylistError file: str! """ @@ -576,9 +574,9 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: - LOG.error('Could not add item to playlist. Kodi reply. %s', reply) playlist.is_kodi_onadd() - return + raise PlaylistError('Could not add item to playlist. Kodi reply. %s', + reply) if xml_video_element is not None: item = playlist_item_from_xml(playlist, xml_video_element) item.kodi_id = kodi_id @@ -598,7 +596,7 @@ def move_playlist_item(playlist, before_pos, after_pos): """ Moves playlist item from before_pos [int] to after_pos [int] for Plex only. - WILL ALSO CHANGE OUR PLAYLISTS. Returns True if successful + WILL ALSO CHANGE OUR PLAYLISTS. """ LOG.debug('Moving item from %s to %s on the Plex side for %s', before_pos, after_pos, playlist) @@ -614,13 +612,11 @@ def move_playlist_item(playlist, before_pos, after_pos): playlist.items[before_pos].id, playlist.items[after_pos - 1].id) # We need to increment the playlistVersion - if _get_playListVersion_from_xml( - playlist, DU().downloadUrl(url, action_type="PUT")) is False: - return False + _get_playListVersion_from_xml( + playlist, DU().downloadUrl(url, action_type="PUT")) # 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) - return True + LOG.debug('Done moving for %s', playlist) def get_PMS_playlist(playlist, playlist_id=None): @@ -646,11 +642,7 @@ 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: - get_playlist_details_from_xml(playlist, xml) - except KeyError: - LOG.error('Could not refresh playlist from PMS') + get_playlist_details_from_xml(playlist, get_PMS_playlist(playlist)) def delete_playlist_item_from_PMS(playlist, pos): @@ -675,7 +667,7 @@ def add_to_Kodi_playlist(playlist, xml_video_element): Adds a new item to the Kodi playlist via JSON (at the end of the playlist). Pass in the PMS xml's video element (one level underneath MediaContainer). - Returns a Playlist_Item or None if it did not work + Returns a Playlist_Item or raises PlaylistError """ item = playlist_item_from_xml(playlist, xml_video_element) if item.kodi_id: @@ -684,9 +676,8 @@ def add_to_Kodi_playlist(playlist, xml_video_element): json_item = {'file': item.file} reply = js.playlist_add(playlist.playlistid, json_item) if reply.get('error') is not None: - LOG.error('Could not add item %s to Kodi playlist. Error: %s', - xml_video_element, reply) - return None + raise PlaylistError('Could not add item %s to Kodi playlist. Error: ' + '%s', xml_video_element, reply) return item From 336d50cd3af1f714e72f419ed2269f136b16067e Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 30 Jan 2018 07:51:14 +0100 Subject: [PATCH 220/509] Fix UnboundLocalError for Direct Paths --- resources/lib/kodimonitor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 2b033690..08ae61e7 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -371,12 +371,12 @@ class KodiMonitor(Monitor): return LOG.info('Need to initialize Plex and PKC playqueue') if plex_id: - PL.init_Plex_playlist(playqueue, plex_id=plex_id) + item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) else: - PL.init_Plex_playlist(playqueue, - kodi_item={'id': kodi_id, - 'type': kodi_type, - 'file': path}) + item = PL.init_Plex_playlist(playqueue, + kodi_item={'id': kodi_id, + 'type': kodi_type, + 'file': path}) # Set the Plex container key (e.g. using the Plex playqueue) container_key = None if info['playlistid'] != -1: From 0eb526add451df4bcf822b35781681180b93606c Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 31 Jan 2018 07:42:23 +0100 Subject: [PATCH 221/509] Enable Kodi playback for an entire PMS xml --- resources/lib/PlexAPI.py | 16 ++-- resources/lib/playback.py | 165 +++++++++++++++++++++---------------- resources/lib/playqueue.py | 23 +----- 3 files changed, 109 insertions(+), 95 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f5337836..08e6483d 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1653,13 +1653,19 @@ class API(): url = "%s&X-Plex-Token=%s" % (url, window('pms_token')) return url - def GetPlayQueueItemID(self): + def getItemId(self): """ - Returns current playQueueItemID for the item. - - If not found, empty str is returned + Returns current playQueueItemID or if unsuccessful the playListItemID + If not found, None is returned """ - return self.item.attrib.get('playQueueItemID') + try: + answ = self.item.attrib['playQueueItemID'] + except KeyError: + try: + answ = self.item.attrib['playListItemID'] + except KeyError: + answ = None + return answ def getDataFromPartOrMedia(self, key): """ diff --git a/resources/lib/playback.py b/resources/lib/playback.py index e208bfdd..ad4bea61 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -29,73 +29,6 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### -def process_indirect(key, offset, resolve=True): - """ - Called e.g. for Plex "Play later" - Plex items where we need to fetch an - additional xml for the actual playurl. In the PMS metadata, indirect="1" is - set. - - Will release default.py with setResolvedUrl - - Set resolve to False if playback should be kicked off directly, not via - setResolvedUrl - """ - LOG.info('process_indirect called with key: %s, offset: %s', key, offset) - result = Playback_Successful() - if key.startswith('http') or key.startswith('{server}'): - xml = DU().downloadUrl(key) - elif key.startswith('/system/services'): - xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key) - else: - xml = DU().downloadUrl('{server}%s' % key) - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not download PMS metadata') - if resolve is True: - # Release default.py - pickle_me(result) - return - if offset != '0': - offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) - # Todo: implement offset - api = API(xml[0]) - listitem = PKC_ListItem() - api.CreateListItemFromPlexItem(listitem) - playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - playqueue.clear() - item = PL.Playlist_Item() - item.xml = xml[0] - item.offset = int(offset) - item.plex_type = v.PLEX_TYPE_CLIP - item.playmethod = 'DirectStream' - item.init_done = True - # Need to get yet another xml to get the final playback url - xml = DU().downloadUrl('http://node.plexapp.com:32400%s' - % xml[0][0][0].attrib['key']) - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not download last xml for playurl') - if resolve is True: - # Release default.py - pickle_me(result) - return - playurl = xml[0].attrib['key'] - item.file = playurl - listitem.setPath(tryEncode(playurl)) - playqueue.items.append(item) - if resolve is True: - result.listitem = listitem - pickle_me(result) - else: - thread = Thread(target=Player().play, - args={'item': tryEncode(playurl), 'listitem': listitem}) - thread.setDaemon(True) - LOG.info('Done initializing PKC playback, starting Kodi player') - thread.start() - @LOCKER.lockthis def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): """ @@ -277,14 +210,16 @@ def _prep_playlist_stack(xml): 'listitem': listitem, 'part': part, 'playcount': api.getViewCount(), - 'offset': api.getResume() + 'offset': api.getResume(), + 'id': api.getItemId() }) return stack -def _process_stack(playqueue, stack): +def _process_stack(playqueue, stack, fill_queue=False): """ Takes our stack and adds the items to the PKC and Kodi playqueues. + Pass fill_queue=True in order to append Playlist_Items to playqueue.items """ # getposition() can return -1 pos = max(playqueue.kodi_pl.getposition(), 0) + 1 @@ -307,8 +242,11 @@ def _process_stack(playqueue, stack): playlist_item.playcount = item['playcount'] playlist_item.offset = item['offset'] playlist_item.part = item['part'] + playlist_item.id = item['id'] playlist_item.init_done = True pos += 1 + if fill_queue: + playqueue.items.append(playlist_item) def conclude_playback(playqueue, pos): @@ -354,3 +292,92 @@ def conclude_playback(playqueue, pos): result.listitem = listitem pickle_me(result) LOG.info('Done concluding playback') + + +def process_indirect(key, offset, resolve=True): + """ + Called e.g. for Plex "Play later" - Plex items where we need to fetch an + additional xml for the actual playurl. In the PMS metadata, indirect="1" is + set. + + Will release default.py with setResolvedUrl + + Set resolve to False if playback should be kicked off directly, not via + setResolvedUrl + """ + LOG.info('process_indirect called with key: %s, offset: %s', key, offset) + result = Playback_Successful() + if key.startswith('http') or key.startswith('{server}'): + xml = DU().downloadUrl(key) + elif key.startswith('/system/services'): + xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key) + else: + xml = DU().downloadUrl('{server}%s' % key) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download PMS metadata') + if resolve is True: + # Release default.py + pickle_me(result) + return + if offset != '0': + offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) + # Todo: implement offset + api = API(xml[0]) + listitem = PKC_ListItem() + api.CreateListItemFromPlexItem(listitem) + playqueue = PQ.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + playqueue.clear() + item = PL.Playlist_Item() + item.xml = xml[0] + item.offset = int(offset) + item.plex_type = v.PLEX_TYPE_CLIP + item.playmethod = 'DirectStream' + item.init_done = True + # Need to get yet another xml to get the final playback url + xml = DU().downloadUrl('http://node.plexapp.com:32400%s' + % xml[0][0][0].attrib['key']) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download last xml for playurl') + if resolve is True: + # Release default.py + pickle_me(result) + return + playurl = xml[0].attrib['key'] + item.file = playurl + listitem.setPath(tryEncode(playurl)) + playqueue.items.append(item) + if resolve is True: + result.listitem = listitem + pickle_me(result) + else: + thread = Thread(target=Player().play, + args={'item': tryEncode(playurl), 'listitem': listitem}) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + thread.start() + + +def play_xml(playqueue, xml): + """ + Play all items contained in the xml passed in. Called by Plex Companion. + """ + LOG.info("play_xml called") + stack = _prep_playlist_stack(xml) + _process_stack(playqueue, stack, fill_queue=True) + LOG.debug('Playqueue after play_xml update: %s', playqueue) + for startpos, item in enumerate(playqueue.items): + if item.id == playqueue.selectedItemID: + break + else: + startpos = 0 + # New thread to release this one sooner (e.g. harddisk spinning up) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, None, False, startpos)) + thread.setDaemon(True) + LOG.info('Done play_xml, starting Kodi player at position %s', startpos) + thread.start() diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index aa9529dc..0109e073 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -10,7 +10,7 @@ from utils import window import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API -from playbackutils import PlaybackUtils +from playback import play_xml import json_rpc as js import variables as v @@ -124,23 +124,4 @@ def update_playqueue_from_PMS(playqueue, return playqueue.repeat = 0 if not repeat else int(repeat) playqueue.plex_transient_token = transient_token - PlaybackUtils(xml, playqueue).play_all() - window('plex_customplaylist', value="true") - if offset not in (None, "0"): - window('plex_customplaylist.seektime', - str(ConvertPlexToKodiTime(offset))) - for startpos, item in enumerate(playqueue.items): - if item.id == playqueue.selectedItemID: - break - else: - startpos = 0 - # Start playback. Player does not return in time - LOG.debug('Playqueues after Plex Companion update are now: %s', - PLAYQUEUES) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, - None, - False, - startpos)) - thread.setDaemon(True) - thread.start() + play_xml(playqueue, xml) From 3aa5ee04088869da898e6908969ee7afad05e7b9 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 31 Jan 2018 07:47:43 +0100 Subject: [PATCH 222/509] Remove playbackutils.py --- resources/lib/playbackutils.py | 362 --------------------------------- 1 file changed, 362 deletions(-) delete mode 100644 resources/lib/playbackutils.py diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py deleted file mode 100644 index aedb0e6f..00000000 --- a/resources/lib/playbackutils.py +++ /dev/null @@ -1,362 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################### -from logging import getLogger -from urllib import urlencode -from threading import Thread - -from xbmc import getCondVisibility, Player -import xbmcgui - -import playutils as putils -from utils import window, settings, tryEncode, tryDecode, language as lang -import downloadutils - -from PlexAPI import API -from PlexFunctions import init_plex_playqueue -from PKC_listitem import PKC_ListItem as ListItem, convert_PKC_to_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, playlist_item_from_xml -from pickler import Playback_Successful -from plexdb_functions import Get_Plex_DB -import variables as v -import state - -############################################################################### - -LOG = getLogger("PLEX." + __name__) - -############################################################################### - - -class PlaybackUtils(): - - def __init__(self, xml, playqueue): - self.xml = xml - self.playqueue = playqueue - - def play(self, plex_id, kodi_id=None, plex_lib_UUID=None): - """ - plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting - to the PMS - """ - LOG.info("Playbackutils called") - item = self.xml[0] - api = API(item) - playqueue = self.playqueue - xml = None - result = Playback_Successful() - listitem = ListItem() - playutils = putils.PlayUtils(item) - playurl = playutils.getPlayUrl() - if not playurl: - LOG.error('No playurl found, aborting') - return - - 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.set_listitem_artwork(listitem) - if kodi_id == 'plexnode': - # Need to get yet another xml to get final url - window('plex_%s.playmethod' % playurl, clear=True) - xml = downloadutils.DownloadUtils().downloadUrl( - '{server}%s' % item[0][0].attrib.get('key')) - try: - xml[0].attrib - except (TypeError, AttributeError): - LOG.error('Could not download %s', - item[0][0].attrib.get('key')) - return - playurl = tryEncode(xml[0].attrib.get('key')) - window('plex_%s.playmethod' % playurl, value='DirectStream') - - playmethod = window('plex_%s.playmethod' % playurl) - if playmethod == "Transcode": - playutils.audioSubsPref(listitem, tryDecode(playurl)) - listitem.setPath(playurl) - api.set_playback_win_props(playurl, listitem) - result.listitem = listitem - return result - - kodi_type = v.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 = 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(playqueue.kodi_pl.getposition(), 0) - self.currentPosition = startPos - - propertiesPlayback = window('plex_playbackProps') == "true" - introsPlaylist = False - dummyPlaylist = False - - LOG.info("Playing from contextmenu: %s", contextmenu_play) - LOG.info("Playlist start position: %s", startPos) - LOG.info("Playlist plugin position: %s", self.currentPosition) - LOG.info("Playlist size: %s", sizePlaylist) - - # RESUME POINT ################ - seektime, runtime = api.getRuntime() - if window('plex_customplaylist.seektime'): - # Already got seektime, e.g. from playqueue & Plex companion - seektime = int(window('plex_customplaylist.seektime')) - - # We need to ensure we add the intro and additional parts only once. - # Otherwise we get a loop. - if not propertiesPlayback: - window('plex_playbackProps', value="true") - LOG.info("Setting up properties in playlist.") - # Where will the player need to start? - # Do we need to get trailers? - trailers = False - if (api.getType() == v.PLEX_TYPE_MOVIE and - not seektime and - sizePlaylist < 2 and - settings('enableCinema') == "true"): - if settings('askCinema') == "true": - trailers = xbmcgui.Dialog().yesno( - lang(29999), - "Play trailers?") - trailers = True if trailers else False - else: - trailers = True - # Post to the PMS. REUSE THE PLAYQUEUE! - xml = init_plex_playqueue(plex_id, - plex_lib_UUID, - mediatype=api.getType(), - trailers=trailers) - try: - get_playlist_details_from_xml(playqueue, xml=xml) - except KeyError: - return - if (not homeScreen and not seektime and sizePlaylist < 2 and - window('plex_customplaylist') != "true" and - not contextmenu_play): - # Need to add a dummy file because the first item will fail - LOG.debug("Adding dummy file to playlist.") - dummyPlaylist = True - add_listitem_to_Kodi_playlist( - playqueue, - startPos, - xbmcgui.ListItem(), - playurl, - xml[0]) - # Remove the original item from playlist - remove_from_kodi_playlist( - playqueue, - startPos+1) - # Readd the original item to playlist - via jsonrpc so we have - # full metadata - add_item_to_kodi_playlist( - playqueue, - self.currentPosition+1, - kodi_id=kodi_id, - kodi_type=kodi_type, - file=playurl) - self.currentPosition += 1 - - # -- ADD TRAILERS ################ - if trailers: - for i, item in enumerate(xml): - if i == len(xml) - 1: - # Don't add the main movie itself - break - self.add_trailer(item) - introsPlaylist = True - - # -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############## - if homeScreen and not seektime and not sizePlaylist: - # Extend our current playlist with the actual item to play - # only if there's no playlist first - LOG.info("Adding main item to playlist.") - add_item_to_kodi_playlist( - playqueue, - self.currentPosition, - kodi_id, - kodi_type) - - elif contextmenu_play: - if state.DIRECT_PATHS: - # 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('plex_%s.playmethod' % playurl) == "Transcode": - playutils.audioSubsPref(listitem, tryDecode(playurl)) - api.CreateListItemFromPlexItem(listitem) - api.set_playback_win_props(playurl, listitem) - api.set_listitem_artwork(listitem) - add_listitem_to_Kodi_playlist( - playqueue, - self.currentPosition+1, - convert_PKC_to_listitem(listitem), - file=playurl, - kodi_item={'id': kodi_id, 'type': kodi_type}) - else: - # Full metadata$ - add_item_to_kodi_playlist( - playqueue, - self.currentPosition+1, - kodi_id, - kodi_type) - self.currentPosition += 1 - if seektime: - window('plex_customplaylist.seektime', value=str(seektime)) - - # Ensure that additional parts are played after the main item - self.currentPosition += 1 - - # -- CHECK FOR ADDITIONAL PARTS ################ - if len(item[0]) > 1: - self.add_part(item, api, kodi_id, kodi_type) - - if dummyPlaylist: - # Added a dummy file to the playlist, - # because the first item is going to fail automatically. - LOG.info("Processed as a playlist. First item is skipped.") - # Delete the item that's gonna fail! - del playqueue.items[startPos] - # Don't attach listitem - return result - - # We just skipped adding properties. Reset flag for next time. - elif propertiesPlayback: - LOG.debug("Resetting properties playback flag.") - window('plex_playbackProps', clear=True) - - # SETUP MAIN ITEM ########## - # For transcoding only, ask for audio/subs pref - if (window('plex_%s.playmethod' % playurl) == "Transcode" and - not contextmenu_play): - playutils.audioSubsPref(listitem, tryDecode(playurl)) - - listitem.setPath(playurl) - api.set_playback_win_props(playurl, listitem) - api.set_listitem_artwork(listitem) - - # PLAYBACK ################ - if (homeScreen and seektime and window('plex_customplaylist') != "true" - and not contextmenu_play): - 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 from starting position %s", startPos) - # Need a separate thread because Player won't return in time - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, None, False, startPos)) - thread.setDaemon(True) - thread.start() - # Don't attach listitem - return result - else: - LOG.info("Play as a regular item") - result.listitem = listitem - return result - - def play_all(self): - """ - Play all items contained in the xml passed in. Called by Plex Companion - """ - LOG.info("Playbackutils play_all called") - window('plex_playbackProps', value="true") - self.currentPosition = 0 - for item in self.xml: - api = API(item) - successful = True - if api.getType() == v.PLEX_TYPE_CLIP: - self.add_trailer(item) - else: - with Get_Plex_DB() as plex_db: - db_item = plex_db.getItem_byId(api.getRatingKey()) - if db_item is not None: - successful = add_item_to_kodi_playlist( - self.playqueue, - self.currentPosition, - kodi_id=db_item[0], - kodi_type=db_item[4]) - if successful is True: - self.currentPosition += 1 - if len(item[0]) > 1: - self.add_part(item, - api, - db_item[0], - db_item[4]) - else: - # Item not in Kodi DB - self.add_trailer(item) - if successful is True: - self.playqueue.items[self.currentPosition - 1].id = item.get( - '%sItemID' % self.playqueue.kind) - - 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' - } - 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, - additionalListItem, - kodi_id=kodi_id, - kodi_type=kodi_type, - plex_id=api.getRatingKey(), - file=additionalPlayurl) - self.currentPosition += 1 - api.setPartNumber(0) From a95e07d32b80123291ad3b990d31ebb4bf071b77 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 31 Jan 2018 20:54:11 +0100 Subject: [PATCH 223/509] Enable resume for playback initiated by Companion --- resources/lib/playback.py | 25 ++++++++++++++++++++----- resources/lib/playqueue.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ad4bea61..1faebebf 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -362,7 +362,7 @@ def process_indirect(key, offset, resolve=True): thread.start() -def play_xml(playqueue, xml): +def play_xml(playqueue, xml, offset=None): """ Play all items contained in the xml passed in. Called by Plex Companion. """ @@ -375,9 +375,24 @@ def play_xml(playqueue, xml): break else: startpos = 0 - # New thread to release this one sooner (e.g. harddisk spinning up) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, None, False, startpos)) - thread.setDaemon(True) + thread = Thread(target=threaded_playback, + args=(playqueue.kodi_pl, startpos, offset)) LOG.info('Done play_xml, starting Kodi player at position %s', startpos) thread.start() + + +def threaded_playback(kodi_playlist, startpos, offset): + """ + Seek immediately after kicking off playback is not reliable. + """ + player = Player() + player.play(kodi_playlist, None, False, startpos) + if offset: + i = 0 + while not player.isPlaying(): + sleep(100) + i += 1 + if i > 100: + LOG.error('Could not seek to %s', offset) + return + js.seek_to(int(offset)) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 0109e073..36d45804 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -124,4 +124,4 @@ def update_playqueue_from_PMS(playqueue, return playqueue.repeat = 0 if not repeat else int(repeat) playqueue.plex_transient_token = transient_token - play_xml(playqueue, xml) + play_xml(playqueue, xml, offset) From c5a374128953a46042d94fbfe63cadfe731885c1 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 07:15:22 +0100 Subject: [PATCH 224/509] Fix Kodi player seeking too often --- resources/lib/playback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1faebebf..c80ebc7a 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -381,13 +381,13 @@ def play_xml(playqueue, xml, offset=None): thread.start() -def threaded_playback(kodi_playlist, startpos, offset): +def threaded_playback(kodi_playlist, startpos, offset=None): """ Seek immediately after kicking off playback is not reliable. """ player = Player() player.play(kodi_playlist, None, False, startpos) - if offset: + if offset and if offset != '0': i = 0 while not player.isPlaying(): sleep(100) From ef1baa2d1d9449b747e114a40cfd3505a7f9812c Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 07:15:37 +0100 Subject: [PATCH 225/509] Revert "Fix Kodi player seeking too often" This reverts commit c5a374128953a46042d94fbfe63cadfe731885c1. --- resources/lib/playback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index c80ebc7a..1faebebf 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -381,13 +381,13 @@ def play_xml(playqueue, xml, offset=None): thread.start() -def threaded_playback(kodi_playlist, startpos, offset=None): +def threaded_playback(kodi_playlist, startpos, offset): """ Seek immediately after kicking off playback is not reliable. """ player = Player() player.play(kodi_playlist, None, False, startpos) - if offset and if offset != '0': + if offset: i = 0 while not player.isPlaying(): sleep(100) From 128582bf96212fa479f0ba12b50e50722bd0670f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 07:16:09 +0100 Subject: [PATCH 226/509] Fix Kodi player seeking too often --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1faebebf..b00904bf 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -387,7 +387,7 @@ def threaded_playback(kodi_playlist, startpos, offset): """ player = Player() player.play(kodi_playlist, None, False, startpos) - if offset: + if offset and offset != '0': i = 0 while not player.isPlaying(): sleep(100) From ff09ae64572d7ec3c7106997f31c8a480c923b1e Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 07:44:12 +0100 Subject: [PATCH 227/509] Force lowercase protocol for plugin playback --- resources/lib/kodimonitor.py | 4 +++- resources/lib/utils.py | 31 ++++++++++++++++++++++--------- service.py | 8 +++----- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 08ae61e7..e54a2948 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,7 +10,8 @@ from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, plex_command, thread_methods +from utils import window, settings, plex_command, thread_methods, \ + set_replace_paths from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER @@ -109,6 +110,7 @@ class KodiMonitor(Monitor): settings_value, getattr(state, state_name), new) setattr(state, state_name, new) # Special cases, overwrite all internal settings + set_replace_paths() state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 990f3f5c..641d18a3 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -21,9 +21,7 @@ import xbmc import xbmcaddon import xbmcgui from xbmcvfs import exists, delete - -from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \ - DB_PLEX_PATH, KODI_PROFILE, KODIVERSION +import variables as v import state ############################################################################### @@ -105,7 +103,7 @@ def exists_dir(path): Feed with encoded string or unicode """ - if KODIVERSION >= 17: + if v.KODIVERSION >= 17: answ = exists(tryEncode(path)) else: dummyfile = join(tryDecode(path), 'dummyfile.txt') @@ -336,13 +334,13 @@ def getUnixTimestamp(secondsIntoTheFuture=None): def kodiSQL(media_type="video"): if media_type == "plex": - dbPath = DB_PLEX_PATH + dbPath = v.DB_PLEX_PATH elif media_type == "music": - dbPath = DB_MUSIC_PATH + dbPath = v.DB_MUSIC_PATH elif media_type == "texture": - dbPath = DB_TEXTURE_PATH + dbPath = v.DB_TEXTURE_PATH else: - dbPath = DB_VIDEO_PATH + dbPath = v.DB_VIDEO_PATH return connect(dbPath, timeout=60.0) @@ -364,6 +362,21 @@ def create_actor_db_index(): conn.close() +def set_replace_paths(): + """ + Sets our values for direct paths correctly (including using lower-case + protocols like smb:// and NOT SMB://) + """ + for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + value = settings(key) + if '://' in value: + protocol = value.split('://', 1)[0] + value = value.replace(protocol, protocol.lower()) + setattr(state, key, value) + + def reset(): # Are you sure you want to reset your local Kodi database? if not dialog('yesno', @@ -650,7 +663,7 @@ class XmlKodiSetting(object): top_element=None): self.filename = filename if path is None: - self.path = join(KODI_PROFILE, filename) + self.path = join(v.KODI_PROFILE, filename) else: self.path = join(path, filename) self.force_create = force_create diff --git a/service.py b/service.py index 72d7fed3..2b83adbd 100644 --- a/service.py +++ b/service.py @@ -28,7 +28,8 @@ sys_path.append(_base_resource) ############################################################################### -from utils import settings, window, language as lang, dialog, tryDecode +from utils import settings, window, language as lang, dialog, tryDecode, \ + set_replace_paths from userclient import UserClient import initialsetup from kodimonitor import KodiMonitor, SpecialMonitor @@ -133,10 +134,7 @@ class Service(): settings('backgroundsync_saftyMargin')) state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' state.REMAP_PATH = settings('remapSMB') == 'true' - for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): - for arg in ('Org', 'New'): - key = 'remapSMB%s%s' % (typus, arg) - setattr(state, key, settings(key)) + set_replace_paths() state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) window('plex_minDBVersion', value="1.5.10") From 3fe1f184d6a86b4b9d107d9218eddb0b1cb8cc9c Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 07:56:54 +0100 Subject: [PATCH 228/509] Prettify --- resources/lib/PlexAPI.py | 154 +++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 08e6483d..a10d59e0 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -37,7 +37,6 @@ import socket from threading import Thread import xml.etree.ElementTree as etree from re import compile as re_compile, sub -from json import dumps from urllib import urlencode, quote_plus, unquote from os.path import basename, join from os import makedirs @@ -56,8 +55,7 @@ import variables as v import state ############################################################################### - -log = getLogger("PLEX." + __name__) +LOG = getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') @@ -129,8 +127,8 @@ class PlexAPI(): plexLogin, plexPassword, {'X-Plex-Client-Identifier': window('plex_client_Id')}) - log.debug("plex.tv username and token: %s, %s" - % (plexLogin, authtoken)) + LOG.debug("plex.tv username and token: %s, %s", + plexLogin, authtoken) if plexLogin == '': # Could not sign in user dialog.ok(lang(29999), lang(39302) + plexLogin) @@ -223,7 +221,7 @@ class PlexAPI(): try: temp_token = xml.find('auth_token').text except: - log.error("Could not find token in plex.tv answer") + LOG.error("Could not find token in plex.tv answer") return False if not temp_token: return False @@ -246,11 +244,11 @@ class PlexAPI(): try: xml.attrib except: - log.error("Error, no PIN from plex.tv provided") + LOG.error("Error, no PIN from plex.tv provided") return None, None code = xml.find('code').text identifier = xml.find('id').text - log.info('Successfully retrieved code and id from plex.tv') + LOG.info('Successfully retrieved code and id from plex.tv') return code, identifier def CheckConnection(self, url, token=None, verifySSL=None): @@ -283,8 +281,8 @@ class PlexAPI(): url = 'https://plex.tv/api/home/users' else: url = url + '/library/onDeck' - log.debug("Checking connection to server %s with verifySSL=%s" - % (url, verifySSL)) + LOG.debug("Checking connection to server %s with verifySSL=%s", + url, verifySSL) count = 0 while count < 1: answer = self.doUtils(url, @@ -293,7 +291,7 @@ class PlexAPI(): verifySSL=verifySSL, timeout=10) if answer is None: - log.debug("Could not connect to %s" % url) + LOG.debug("Could not connect to %s", url) count += 1 sleep(500) continue @@ -308,9 +306,9 @@ class PlexAPI(): # Success - we downloaded an xml! answer = 200 # We could connect but maybe were not authenticated. No worries - log.debug("Checking connection successfull. Answer: %s" % answer) + LOG.debug("Checking connection successfull. Answer: %s", answer) return answer - log.debug('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): @@ -359,7 +357,7 @@ class PlexAPI(): try: self.g_PMS[uuid][tag] = value except: - log.error('%s has not yet been declared ' % uuid) + LOG.error('%s has not yet been declared', uuid) return False def getPMSProperty(self, uuid, tag): @@ -367,7 +365,7 @@ class PlexAPI(): try: answ = self.g_PMS[uuid].get(tag, '') except: - log.error('%s not found in PMS catalogue' % uuid) + LOG.error('%s not found in PMS catalogue', uuid) answ = False return answ @@ -409,9 +407,9 @@ class PlexAPI(): break except Exception as e: # Probably error: (101, 'Network is unreachable') - log.error(e) + LOG.error(e) import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + LOG.error("Traceback:\n%s", traceback.format_exc()) finally: GDM.close() @@ -457,20 +455,20 @@ class PlexAPI(): # Look first for local PMS in the LAN pmsList = self.PlexGDM() - log.debug('PMS found in the local LAN via GDM: %s' % pmsList) + LOG.debug('PMS found in the local LAN via GDM: %s', pmsList) # Get PMS from plex.tv if plexToken: - log.info('Checking with plex.tv for more PMS to connect to') + LOG.info('Checking with plex.tv for more PMS to connect to') self.getPMSListFromMyPlex(plexToken) else: - log.info('No plex token supplied, only checked LAN for PMS') + LOG.info('No plex token supplied, only checked LAN for PMS') for uuid in pmsList: PMS = pmsList[uuid] if PMS['uuid'] in self.g_PMS: - log.debug('We already know of PMS %s from plex.tv' - % PMS['serverName']) + LOG.debug('We already know of PMS %s from plex.tv', + PMS['serverName']) # Update with GDM data - potentially more reliable than plex.tv self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip']) self.updatePMSProperty(PMS['uuid'], 'port', PMS['port']) @@ -519,7 +517,7 @@ class PlexAPI(): try: xml.attrib except AttributeError: - log.error('Could not get list of PMS from plex.tv') + LOG.error('Could not get list of PMS from plex.tv') return import Queue @@ -540,8 +538,8 @@ class PlexAPI(): PMS['name'] = Dir.get('name') infoAge = time() - int(Dir.get('lastSeenAt')) if infoAge > maxAgeSeconds: - log.debug("Server %s not seen for 2 days - skipping." - % PMS['name']) + LOG.debug("Server %s not seen for 2 days - skipping.", + PMS['name']) continue PMS['uuid'] = Dir.get('clientIdentifier') @@ -607,8 +605,8 @@ class PlexAPI(): PMS['uuid'], 'baseURL', PMS['baseURL']) self.updatePMSProperty( PMS['uuid'], 'ownername', PMS['ownername']) - log.debug('Found PMS %s: %s' - % (PMS['uuid'], self.g_PMS[PMS['uuid']])) + LOG.debug('Found PMS %s: %s', + PMS['uuid'], self.g_PMS[PMS['uuid']]) queue.task_done() def pokePMS(self, PMS, queue): @@ -649,9 +647,9 @@ class PlexAPI(): PMS['port'] = port queue.put(PMS) return - log.info('Found a PMS at %s, but the expected machineIdentifier of ' - '%s did not match the one we found: %s' - % (url, PMS['uuid'], xml.get('machineIdentifier'))) + LOG.info('Found a PMS at %s, but the expected machineIdentifier of ' + '%s did not match the one we found: %s', + url, PMS['uuid'], xml.get('machineIdentifier')) def MyPlexSignIn(self, username, password, options): """ @@ -685,7 +683,7 @@ class PlexAPI(): response = urlopener.open(request).read() except urllib2.HTTPError as e: if e.code == 401: - log.info("Authentication failed") + LOG.info("Authentication failed") return ('', '') else: raise @@ -731,12 +729,12 @@ class PlexAPI(): url = '' # If an error is encountered, set to False if not users: - log.info("Couldnt get user from plex.tv. No URL for user avatar") + LOG.info("Couldnt get user from plex.tv. No URL for user avatar") return False for user in users: if username in user['title']: url = user['thumb'] - log.debug("Avatar url for user %s is: %s" % (username, url)) + LOG.debug("Avatar url for user %s is: %s", username, url) return url def ChoosePlexHomeUser(self, plexToken): @@ -759,7 +757,7 @@ class PlexAPI(): # Get list of Plex home users users = self.MyPlexListHomeUsers(plexToken) if not users: - log.error("User download failed.") + LOG.error("User download failed.") return False userlist = [] @@ -781,7 +779,7 @@ class PlexAPI(): lang(29999) + lang(39306), userlistCoded) if user_select == -1: - log.info("No user selected.") + LOG.info("No user selected.") settings('username', value='') executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) @@ -790,12 +788,12 @@ class PlexAPI(): else: user_select = 0 selected_user = userlist[user_select] - log.info("Selected user: %s" % selected_user) + LOG.info("Selected user: %s", selected_user) user = users[user_select] # Ask for PIN, if protected: pin = None if user['protected'] == '1': - log.debug('Asking for users PIN') + LOG.debug('Asking for users PIN') pin = dialog.input( lang(39307) + selected_user, '', @@ -827,7 +825,7 @@ class PlexAPI(): # User chose to cancel break if not username: - log.error('Failed signing in a user to plex.tv') + LOG.error('Failed signing in a user to plex.tv') executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) return False return { @@ -856,7 +854,7 @@ class PlexAPI(): settings('userid') and settings('username') with new plex token """ - log.info('Switching to user %s' % userId) + LOG.info('Switching to user %s', userId) url = 'https://plex.tv/api/home/users/' + userId + '/switch' if pin: url += '?pin=' + pin @@ -867,7 +865,7 @@ class PlexAPI(): try: answer.attrib except: - log.error('Error: plex.tv switch HomeUser change failed') + LOG.error('Error: plex.tv switch HomeUser change failed') return False username = answer.attrib.get('title', '') @@ -891,15 +889,15 @@ class PlexAPI(): try: xml.attrib except: - log.error('Answer from plex.tv not as excepted') + LOG.error('Answer from plex.tv not as excepted') # Set to empty iterable list for loop xml = [] found = 0 - log.debug('Our machineIdentifier is %s' % machineIdentifier) + LOG.debug('Our machineIdentifier is %s', machineIdentifier) for device in xml: identifier = device.attrib.get('clientIdentifier') - log.debug('Found a Plex machineIdentifier: %s' % identifier) + LOG.debug('Found a Plex machineIdentifier: %s', identifier) if (identifier in machineIdentifier or machineIdentifier in identifier): found += 1 @@ -909,12 +907,12 @@ class PlexAPI(): 'username': username, } if found == 0: - log.info('No tokens found for your server! Using empty string') + LOG.info('No tokens found for your server! Using empty string') result['usertoken'] = '' else: result['usertoken'] = token - log.info('Plex.tv switch HomeUser change successfull for user %s' - % username) + LOG.info('Plex.tv switch HomeUser change successfull for user %s', + username) return result def MyPlexListHomeUsers(self, token): @@ -944,7 +942,7 @@ class PlexAPI(): try: xml.attrib except: - log.error('Download of Plex home users failed.') + LOG.error('Download of Plex home users failed.') return False users = [] for user in xml: @@ -1372,8 +1370,8 @@ class API(): elif child.tag == 'Producer': producer.append(child.attrib['tag']) except KeyError: - log.warn('Malformed PMS answer for getPeople: %s: %s' - % (child.tag, child.attrib)) + LOG.warn('Malformed PMS answer for getPeople: %s: %s', + child.tag, child.attrib) return { 'Director': director, 'Writer': writer, @@ -1943,11 +1941,11 @@ class API(): mediaId = self.getProvider('tvdb') if mediaId is not None: return mediaId - log.info('Plex did not provide ID for IMDB or TVDB. Start ' + LOG.info('Plex did not provide ID for IMDB or TVDB. Start ' 'lookup process') else: - log.info('Start movie set/collection lookup on themoviedb using %s' - % item.get('title', '')) + LOG.info('Start movie set/collection lookup on themoviedb using %s', + item.get('title', '')) apiKey = settings('themoviedbAPIKey') if media_type == v.PLEX_TYPE_SHOW: @@ -1970,11 +1968,11 @@ class API(): try: data.get('test') except: - log.error('Could not download data from FanartTV') + LOG.error('Could not download data from FanartTV') return if data.get('results') is None: - log.info('No match found on themoviedb for type: %s, title: %s' - % (media_type, title)) + LOG.info('No match found on themoviedb for type: %s, title: %s', + media_type, title) return year = item.get('year') @@ -1990,7 +1988,7 @@ class API(): break # find exact match based on title, if we haven't found a year match if matchFound is None: - log.info('No themoviedb match found using year %s' % year) + LOG.info('No themoviedb match found using year %s', year) replacements = ( ' ', '-', @@ -2021,21 +2019,21 @@ class API(): # if a match was not found, we accept the closest match from TMDB if matchFound is None and len(data.get("results")) > 0: - log.info('Using very first match from themoviedb') + LOG.info('Using very first match from themoviedb') matchFound = entry = data.get("results")[0] if matchFound is None: - log.info('Still no themoviedb match for type: %s, title: %s, ' - 'year: %s' % (media_type, title, year)) - log.debug('themoviedb answer was %s' % data['results']) + LOG.info('Still no themoviedb match for type: %s, title: %s, ' + 'year: %s', media_type, title, year) + LOG.debug('themoviedb answer was %s', data['results']) return - log.info('Found themoviedb match for %s: %s' - % (item.get('title'), matchFound)) + LOG.info('Found themoviedb match for %s: %s', + item.get('title'), matchFound) tmdbId = str(entry.get("id", "")) if tmdbId == '': - log.error('No themoviedb ID found, aborting') + LOG.error('No themoviedb ID found, aborting') return if media_type == "multi" and entry.get("media_type"): @@ -2061,8 +2059,8 @@ class API(): try: data.get('test') except: - log.error('Could not download %s with parameters %s' - % (url, parameters)) + LOG.error('Could not download %s with parameters %s', + url, parameters) continue if collection is False: if data.get("imdb_id") is not None: @@ -2075,8 +2073,8 @@ class API(): if data.get("belongs_to_collection") is None: continue mediaId = str(data.get("belongs_to_collection").get("id")) - log.debug('Retrieved collections tmdb id %s for %s' - % (mediaId, title)) + LOG.debug('Retrieved collections tmdb id %s for %s', + mediaId, title) url = 'https://api.themoviedb.org/3/collection/%s' % mediaId data = DownloadUtils().downloadUrl( url, @@ -2086,8 +2084,8 @@ class API(): try: data.get('poster_path') except AttributeError: - log.info('Could not find TheMovieDB poster paths for %s in' - 'the language %s' % (title, language)) + LOG.info('Could not find TheMovieDB poster paths for %s in' + 'the language %s', title, language) continue else: poster = 'https://image.tmdb.org/t/p/original%s' % data.get('poster_path') @@ -2124,7 +2122,7 @@ class API(): try: data.get('test') except: - log.error('Could not download data from FanartTV') + LOG.error('Could not download data from FanartTV') return allartworks # we need to use a little mapping between fanart.tv arttypes and kodi @@ -2234,8 +2232,8 @@ class API(): allartworks['Backdrop'].append(background) allartworks = self.getFanartTVArt(externalId, allartworks, True) else: - log.info('Did not find a set/collection ID on TheMovieDB using %s.' - ' Artwork will be missing.' % self.getTitle()[0]) + LOG.info('Did not find a set/collection ID on TheMovieDB using %s.' + ' Artwork will be missing.', self.getTitle()[0]) return allartworks def shouldStream(self): @@ -2370,7 +2368,7 @@ class API(): } # Look like Android to let the PMS use the transcoding profile xargs.update(headers) - log.debug("Setting transcode quality to: %s" % quality) + LOG.debug("Setting transcode quality to: %s", quality) args.update(quality) url = transcodePath + urlencode(xargs) + '&' + urlencode(args) return url @@ -2408,7 +2406,7 @@ class API(): "%s%s" % (self.server, key)) externalsubs.append(path) kodiindex += 1 - log.info('Found external subs: %s', externalsubs) + LOG.info('Found external subs: %s', externalsubs) return externalsubs @staticmethod @@ -2426,15 +2424,15 @@ class API(): try: r.status_code except AttributeError: - log.error('Could not temporarily download subtitle %s' % url) + LOG.error('Could not temporarily download subtitle %s', url) return else: - log.debug('Writing temp subtitle to %s', path) + LOG.debug('Writing temp subtitle to %s', path) try: with open(path, 'wb') as f: f.write(r.content) except UnicodeEncodeError: - log.debug('Need to slugify the filename %s', path) + LOG.debug('Need to slugify the filename %s', path) path = slugify(path) with open(path, 'wb') as f: f.write(r.content) @@ -2662,7 +2660,7 @@ class API(): Returns True if sync should stop, else False """ - log.warn('Cannot access file: %s' % url) + LOG.warn('Cannot access file: %s', url) resp = xbmcgui.Dialog().yesno( heading=lang(29999), line1=lang(39031) + url, From 187a6131f043a657726013b152331e46050d69e2 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 08:19:51 +0100 Subject: [PATCH 229/509] Prettify --- resources/lib/dialogs/context.py | 46 +++++++++++++------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/resources/lib/dialogs/context.py b/resources/lib/dialogs/context.py index 4ee9f038..ef9f67c9 100644 --- a/resources/lib/dialogs/context.py +++ b/resources/lib/dialogs/context.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- - ############################################################################### - -import logging -import os +from logging import getLogger +from os.path import join import xbmcgui -import xbmcaddon +from xbmcaddon import Addon from utils import window ############################################################################### -log = logging.getLogger("PLEX."+__name__) -addon = xbmcaddon.Addon('plugin.video.plexkodiconnect') +LOG = getLogger("PLEX." + __name__) +ADDON = Addon('plugin.video.plexkodiconnect') ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -27,16 +25,16 @@ USER_IMAGE = 150 class ContextMenu(xbmcgui.WindowXMLDialog): - - _options = [] - selected_option = None - - def __init__(self, *args, **kwargs): - + self._options = [] + self.selected_option = None + self.list_ = None + self.background = None xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_options(self, options=[]): + def set_options(self, options=None): + if not options: + options = [] self._options = options def is_selected(self): @@ -46,17 +44,13 @@ class ContextMenu(xbmcgui.WindowXMLDialog): return self.selected_option def onInit(self): - if window('PlexUserImage'): self.getControl(USER_IMAGE).setImage(window('PlexUserImage')) - height = 479 + (len(self._options) * 55) - log.info("options: %s", self._options) + LOG.debug("options: %s", self._options) self.list_ = self.getControl(LIST) - for option in self._options: self.list_.addItem(self._add_listitem(option)) - self.background = self._add_editcontrol(730, height, 30, 450) self.setFocus(self.list_) @@ -64,27 +58,23 @@ class ContextMenu(xbmcgui.WindowXMLDialog): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): self.close() - if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): - if self.getFocusId() == LIST: option = self.list_.getSelectedItem() self.selected_option = option.getLabel() - log.info('option selected: %s', self.selected_option) - + LOG.info('option selected: %s', self.selected_option) self.close() - def _add_editcontrol(self, x, y, height, width, password=0): - - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + def _add_editcontrol(self, x, y, height, width, password=None): + media = join(ADDON.getAddonInfo('path'), + 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlImage(0, 0, 0, 0, - filename=os.path.join(media, "white.png"), + filename=join(media, "white.png"), aspectRatio=0, colorDiffuse="ff111111") control.setPosition(x, y) control.setHeight(height) control.setWidth(width) - self.addControl(control) return control From 0daf18d5d456e7893834c574e99403f33db8f309 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 08:23:31 +0100 Subject: [PATCH 230/509] Prettify --- contextmenu.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index 1ab7f553..1d49233f 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -8,19 +8,19 @@ from xbmcaddon import Addon from xbmc import translatePath, sleep, log, LOGERROR from xbmcgui import Window -_addon = Addon(id='plugin.video.plexkodiconnect') +_ADDON = Addon(id='plugin.video.plexkodiconnect') try: - _addon_path = _addon.getAddonInfo('path').decode('utf-8') + _ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8') except TypeError: - _addon_path = _addon.getAddonInfo('path').decode() + _ADDON_PATH = _ADDON.getAddonInfo('path').decode() try: _base_resource = translatePath(os_path.join( - _addon_path, + _ADDON_PATH, 'resources', 'lib')).decode('utf-8') except TypeError: _base_resource = translatePath(os_path.join( - _addon_path, + _ADDON_PATH, 'resources', 'lib')).decode() sys_path.append(_base_resource) @@ -30,12 +30,12 @@ from pickler import unpickle_me, pickl_window ############################################################################### if __name__ == "__main__": - win = Window(10000) - while win.getProperty('plex_command'): + WINDOW = Window(10000) + while WINDOW.getProperty('plex_command'): sleep(20) - win.setProperty('plex_command', 'CONTEXT_menu') + WINDOW.setProperty('plex_command', 'CONTEXT_menu') while not pickl_window('plex_result'): sleep(50) - result = unpickle_me() - if result is None: + RESULT = unpickle_me() + if RESULT is None: log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR) From 16510092e87a79626f85d07b723617d02136adb1 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 08:23:55 +0100 Subject: [PATCH 231/509] Prettify --- contextmenu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index 1d49233f..138ad83a 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -14,16 +14,16 @@ try: except TypeError: _ADDON_PATH = _ADDON.getAddonInfo('path').decode() try: - _base_resource = translatePath(os_path.join( + _BASE_RESOURCE = translatePath(os_path.join( _ADDON_PATH, 'resources', 'lib')).decode('utf-8') except TypeError: - _base_resource = 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) from pickler import unpickle_me, pickl_window From a6a8c187113df48861f5b124017bd60b3f80d407 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 1 Feb 2018 13:55:40 +0100 Subject: [PATCH 232/509] Prettify --- resources/lib/context_entry.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index c3aa0598..2428847f 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -13,7 +13,7 @@ import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +LOG = logging.getLogger("PLEX." + __name__) OPTIONS = { 'Refresh': lang(30410), @@ -38,8 +38,8 @@ class ContextMenu(object): self.item_type = self._get_item_type() self.item_id = self._get_item_id(self.kodi_id, self.item_type) - log.info("Found item_id: %s item_type: %s" - % (self.item_id, self.item_type)) + LOG.info("Found item_id: %s item_type: %s", + self.item_id, self.item_type) if not self.item_id: return @@ -49,7 +49,7 @@ class ContextMenu(object): if self._selected_option in (OPTIONS['Delete'], OPTIONS['Refresh']): - log.info("refreshing container") + LOG.info("refreshing container") xbmc.sleep(500) xbmc.executebuiltin('Container.Refresh') @@ -66,7 +66,7 @@ class ContextMenu(object): elif xbmc.getCondVisibility('Container.Content(pictures)'): item_type = "picture" else: - log.info("item_type is unknown") + LOG.info("item_type is unknown") return item_type @classmethod @@ -78,7 +78,7 @@ class ContextMenu(object): try: item_id = item[0] except TypeError: - log.error('Could not get the Plex id for context menu') + LOG.error('Could not get the Plex id for context menu') return item_id def _select_menu(self): @@ -190,11 +190,11 @@ class ContextMenu(object): if settings('skipContextMenu') != "true": if not dialog("yesno", heading=lang(29999), line1=lang(33041)): - log.info("User skipped deletion for: %s", self.item_id) + LOG.info("User skipped deletion for: %s", self.item_id) delete = False if delete: - log.info("Deleting Plex item with id %s", self.item_id) + LOG.info("Deleting Plex item with id %s", self.item_id) if delete_item_from_pms(self.item_id) is False: dialog("ok", heading="{plex}", line1=lang(30414)) From bd85bb445e466cc282a68272b37b7cdecde69760 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 12:45:48 +0100 Subject: [PATCH 233/509] Enable context menu playback --- resources/lib/context_entry.py | 182 ++++++++++++++------------------- resources/lib/playback.py | 16 ++- resources/lib/player.py | 5 - resources/lib/playlist_func.py | 2 + resources/lib/playutils.py | 5 +- resources/lib/state.py | 4 + 6 files changed, 94 insertions(+), 120 deletions(-) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 2428847f..c96a469e 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- ############################################################################### -import logging +from logging import getLogger -import xbmc -import xbmcaddon +from xbmc import getInfoLabel, sleep, executebuiltin, getCondVisibility +from xbmcaddon import Addon import plexdb_functions as plexdb -from utils import window, settings, dialog, language as lang, kodiSQL +from utils import window, settings, dialog, language as lang from dialogs import context from PlexFunctions import delete_item_from_pms import variables as v +import state ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = getLogger("PLEX." + __name__) OPTIONS = { 'Refresh': lang(30410), @@ -30,69 +31,74 @@ OPTIONS = { class ContextMenu(object): - + """ + Class initiated if user opens "Plex options" on a PLEX item using the Kodi + context menu + """ _selected_option = None def __init__(self): - self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') - self.item_type = self._get_item_type() - self.item_id = self._get_item_id(self.kodi_id, self.item_type) - - LOG.info("Found item_id: %s item_type: %s", - self.item_id, self.item_type) - - if not self.item_id: + """ + Simply instantiate with ContextMenu() - no need to call any methods + """ + self.kodi_id = getInfoLabel('ListItem.DBID').decode('utf-8') + self.kodi_type = self._get_kodi_type() + self.plex_id = self._get_plex_id(self.kodi_id, self.kodi_type) + if self.kodi_type: + self.plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[self.kodi_type] + else: + self.plex_type = None + LOG.debug("Found plex_id: %s plex_type: %s", + self.plex_id, self.plex_type) + if not self.plex_id: return - if self._select_menu(): self._action_menu() - if self._selected_option in (OPTIONS['Delete'], OPTIONS['Refresh']): LOG.info("refreshing container") - xbmc.sleep(500) - xbmc.executebuiltin('Container.Refresh') + sleep(500) + executebuiltin('Container.Refresh') - @classmethod - def _get_item_type(cls): - item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') - if not item_type: - if xbmc.getCondVisibility('Container.Content(albums)'): - item_type = "album" - elif xbmc.getCondVisibility('Container.Content(artists)'): - item_type = "artist" - elif xbmc.getCondVisibility('Container.Content(songs)'): - item_type = "song" - elif xbmc.getCondVisibility('Container.Content(pictures)'): - item_type = "picture" + @staticmethod + def _get_kodi_type(): + kodi_type = getInfoLabel('ListItem.DBTYPE').decode('utf-8') + if not kodi_type: + if getCondVisibility('Container.Content(albums)'): + kodi_type = v.KODI_TYPE_ALBUM + elif getCondVisibility('Container.Content(artists)'): + kodi_type = v.KODI_TYPE_ARTIST + elif getCondVisibility('Container.Content(songs)'): + kodi_type = v.KODI_TYPE_SONG + elif getCondVisibility('Container.Content(pictures)'): + kodi_type = v.KODI_TYPE_PHOTO else: - LOG.info("item_type is unknown") - return item_type + LOG.info("kodi_type is unknown") + kodi_type = None + return kodi_type - @classmethod - 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: + @staticmethod + def _get_plex_id(kodi_id, kodi_type): + plex_id = getInfoLabel('ListItem.Property(plexid)') or None + if not plex_id and kodi_id and kodi_type: with plexdb.Get_Plex_DB() as plexcursor: - item = plexcursor.getItem_byKodiId(kodi_id, item_type) + item = plexcursor.getItem_byKodiId(kodi_id, kodi_type) try: - item_id = item[0] + plex_id = item[0] except TypeError: - LOG.error('Could not get the Plex id for context menu') - return item_id + LOG.info('Could not get the Plex id for context menu') + return plex_id def _select_menu(self): - # Display select dialog + """ + Display select dialog + """ options = [] - # if user uses direct paths, give option to initiate playback via PMS - if (window('useDirectPaths') == 'true' and - self.item_type in v.KODI_VIDEOTYPES): + if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES: options.append(OPTIONS['PMS_Play']) - - if self.item_type in v.KODI_VIDEOTYPES: + if self.kodi_type in v.KODI_VIDEOTYPES: options.append(OPTIONS['Transcode']) - # userdata = self.api.getUserData() # if userdata['Favorite']: # # Remove from emby favourites @@ -100,11 +106,9 @@ class ContextMenu(object): # else: # # Add to emby favourites # options.append(OPTIONS['AddFav']) - - # if self.item_type == "song": + # if self.kodi_type == "song": # # Set custom song rating # options.append(OPTIONS['RateSong']) - # Refresh item # options.append(OPTIONS['Refresh']) # Delete item, only if the Plex Home main user is logged in @@ -113,103 +117,65 @@ class ContextMenu(object): options.append(OPTIONS['Delete']) # Addon settings options.append(OPTIONS['Addon']) - context_menu = context.ContextMenu( "script-emby-context.xml", - xbmcaddon.Addon( - 'plugin.video.plexkodiconnect').getAddonInfo('path'), - "default", "1080i") + Addon('plugin.video.plexkodiconnect').getAddonInfo('path'), + "default", + "1080i") context_menu.set_options(options) context_menu.doModal() - if context_menu.is_selected(): self._selected_option = context_menu.get_selected() - return self._selected_option def _action_menu(self): - + """ + Do whatever the user selected to do + """ selected = self._selected_option - if selected == OPTIONS['Transcode']: - window('plex_forcetranscode', value='true') + state.FORCE_TRANSCODE = True self._PMS_play() - elif selected == OPTIONS['PMS_Play']: self._PMS_play() - # elif selected == OPTIONS['Refresh']: # self.emby.refreshItem(self.item_id) - # elif selected == OPTIONS['AddFav']: # self.emby.updateUserRating(self.item_id, favourite=True) - # elif selected == OPTIONS['RemoveFav']: # self.emby.updateUserRating(self.item_id, favourite=False) - # elif selected == OPTIONS['RateSong']: # self._rate_song() - elif selected == OPTIONS['Addon']: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') - + executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') elif selected == OPTIONS['Delete']: self._delete_item() - def _rate_song(self): - - conn = kodiSQL('music') - cursor = conn.cursor() - query = "SELECT rating FROM song WHERE idSong = ?" - cursor.execute(query, (self.kodi_id,)) - try: - value = cursor.fetchone()[0] - current_value = int(round(float(value), 0)) - except TypeError: - pass - else: - new_value = dialog("numeric", 0, lang(30411), str(current_value)) - if new_value > -1: - - new_value = int(new_value) - if new_value > 5: - new_value = 5 - - if settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(new_value, self.api.get_file_path()) - - query = "UPDATE song SET rating = ? WHERE idSong = ?" - cursor.execute(query, (new_value, self.kodi_id,)) - conn.commit() - finally: - cursor.close() - def _delete_item(self): - + """ + Delete item on PMS + """ delete = True if settings('skipContextMenu') != "true": - - if not dialog("yesno", heading=lang(29999), line1=lang(33041)): - LOG.info("User skipped deletion for: %s", self.item_id) + if not dialog("yesno", heading="{plex}", line1=lang(33041)): + LOG.info("User skipped deletion for: %s", self.plex_id) delete = False - if delete: - LOG.info("Deleting Plex item with id %s", self.item_id) - if delete_item_from_pms(self.item_id) is False: + LOG.info("Deleting Plex item with id %s", self.plex_id) + if delete_item_from_pms(self.plex_id) is False: dialog("ok", heading="{plex}", line1=lang(30414)) def _PMS_play(self): """ For using direct paths: Initiates playback using the PMS """ - window('plex_contextplay', value='true') + state.CONTEXT_MENU_PLAY = True params = { - 'filename': '/library/metadata/%s' % self.item_id, - 'id': self.item_id, - 'dbid': self.kodi_id, - 'mode': "play" + 'mode': 'play', + 'plex_id': self.plex_id, + 'plex_type': self.plex_type } from urllib import urlencode handle = ("plugin://plugin.video.plexkodiconnect/movies?%s" % urlencode(params)) - xbmc.executebuiltin('RunPlugin(%s)' % handle) + executebuiltin('RunPlugin(%s)' % handle) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index b00904bf..8934ccd9 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -17,7 +17,7 @@ from playutils import PlayUtils from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful import json_rpc as js -from utils import window, settings, dialog, language as lang, tryEncode +from utils import settings, dialog, language as lang, tryEncode from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -120,8 +120,6 @@ def playback_init(plex_id, plex_type, playqueue): Playback setup if Kodi starts playing an item for the first time. """ LOG.info('Initializing PKC playback') - contextmenu_play = window('plex_contextplay') == 'true' - window('plex_contextplay', clear=True) xml = GetPlexMetadata(plex_id) try: xml[0].attrib @@ -158,6 +156,9 @@ def playback_init(plex_id, plex_type, playqueue): # Sleep a bit to let setResolvedUrl do its thing - bit ugly sleep(200) _process_stack(playqueue, stack) + # Reset some playback variables + state.CONTEXT_MENU_PLAY = False + state.FORCE_TRANSCODE = False # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=Player().play, args=(playqueue.kodi_pl, )) @@ -176,7 +177,10 @@ def _prep_playlist_stack(xml): stack = [] for item in xml: api = API(item) - if api.getType() != v.PLEX_TYPE_CLIP: + if (state.CONTEXT_MENU_PLAY is False or + api.getType() != v.PLEX_TYPE_CLIP): + # If user chose to play via PMS or force transcode, do not + # use the item path stored in the Kodi DB with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) kodi_id = plex_dbitem[0] if plex_dbitem else None @@ -243,6 +247,7 @@ def _process_stack(playqueue, stack, fill_queue=False): playlist_item.offset = item['offset'] playlist_item.part = item['part'] playlist_item.id = item['id'] + playlist_item.force_transcode = state.FORCE_TRANSCODE playlist_item.init_done = True pos += 1 if fill_queue: @@ -356,7 +361,8 @@ def process_indirect(key, offset, resolve=True): pickle_me(result) else: thread = Thread(target=Player().play, - args={'item': tryEncode(playurl), 'listitem': listitem}) + args={'item': tryEncode(playurl), + 'listitem': listitem}) thread.setDaemon(True) LOG.info('Done initializing PKC playback, starting Kodi player') thread.start() diff --git a/resources/lib/player.py b/resources/lib/player.py index 49436094..6e1363de 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -5,7 +5,6 @@ from logging import getLogger from xbmc import Player -from utils import window from downloadutils import DownloadUtils as DU from plexbmchelper.subscribers import LOCKER import variables as v @@ -26,10 +25,6 @@ def playback_cleanup(): # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) state.PLEX_TRANSIENT_TOKEN = None - for item in ('plex_customplaylist', - 'plex_customplaylist.seektime', - 'plex_forcetranscode'): - window(item, clear=True) for playerid in state.ACTIVE_PLAYERS: status = state.PLAYER_STATES[playerid] # Remember the last played item later diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index e9bd004d..5c2d67ba 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -197,6 +197,7 @@ class Playlist_Item(object): playcount = None [int] how many times the item has already been played offset = None [int] the item's view offset UPON START in Plex time part = 0 [int] part number if Plex video consists of mult. parts + force_transcode [bool] defaults to False init_done = False Set to True only if run through playback init """ def __init__(self): @@ -215,6 +216,7 @@ class Playlist_Item(object): self.offset = None # If Plex video consists of several parts; part number self.part = 0 + self.force_transcode = False self.init_done = False def __repr__(self): diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 89da5e70..11cfe418 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -87,7 +87,8 @@ class PlayUtils(): - codec is in h265 - 10bit video codec - HEVC codec - - window variable 'plex_forcetranscode' set to 'true' + - playqueue_item force_transcode is set to True + - state variable FORCE_TRANSCODE set to True (excepting trailers etc.) - video bitrate above specified settings bitrate if the corresponding file settings are set to 'true' @@ -97,7 +98,7 @@ class PlayUtils(): return False videoCodec = self.api.getVideoCodec() LOG.info("videoCodec: %s" % videoCodec) - if window('plex_forcetranscode') == 'true': + if self.item.force_transcode is True: LOG.info('User chose to force-transcode') return True codec = videoCodec['videocodec'] diff --git a/resources/lib/state.py b/resources/lib/state.py index 6ac85f40..1da14c2d 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -143,6 +143,10 @@ RESUMABLE = False # Set by SpecialMonitor - did user choose to resume playback or start from the # beginning? RESUME_PLAYBACK = False +# Was the playback initiated by the user using the Kodi context menu? +CONTEXT_MENU_PLAY = False +# Set by context menu - shall we force-transcode the next playing item? +FORCE_TRANSCODE = False # Kodi webserver details WEBSERVER_PORT = 8080 From a2193ab01f55f2fe4c2559b8ac50cc30dc9906d3 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 13:44:16 +0100 Subject: [PATCH 234/509] Prettify --- resources/lib/downloadutils.py | 80 +++++++++++++++++----------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 377df6a3..ff23b7c1 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -5,7 +5,7 @@ from logging import getLogger import xml.etree.ElementTree as etree import requests -from utils import settings, window, language as lang, dialog +from utils import window, language as lang, dialog import clientinfo as client import state @@ -16,7 +16,7 @@ import state import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -46,7 +46,7 @@ class DownloadUtils(): Reserved for userclient only """ self.server = server - log.debug("Set server: %s" % server) + LOG.debug("Set server: %s", server) def setToken(self, token): """ @@ -54,9 +54,9 @@ class DownloadUtils(): """ self.token = token if token == '': - log.debug('Set token: empty token!') + LOG.debug('Set token: empty token!') else: - log.debug("Set token: xxxxxxx") + LOG.debug("Set token: xxxxxxx") def setSSL(self, verifySSL=None, certificate=None): """ @@ -74,8 +74,8 @@ class DownloadUtils(): self.s.verify = verifySSL if certificate: self.s.cert = certificate - log.debug("Verify SSL certificates set to: %s", verifySSL) - log.debug("SSL client side certificate set to: %s", certificate) + LOG.debug("Verify SSL certificates set to: %s", verifySSL) + LOG.debug("SSL client side certificate set to: %s", certificate) def startSession(self, reset=False): """ @@ -107,18 +107,18 @@ class DownloadUtils(): self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - log.info("Requests session started on: %s" % self.server) + LOG.info("Requests session started on: %s", self.server) def stopSession(self): try: self.s.close() except: - log.info("Requests session already closed") + LOG.info("Requests session already closed") try: del self.s except: pass - log.info('Request session stopped') + LOG.info('Request session stopped') def getHeader(self, options=None): header = client.getXArgsDeviceInfo() @@ -164,7 +164,7 @@ class DownloadUtils(): try: s = self.s except AttributeError: - log.info("Request session does not exist: start one") + LOG.info("Request session does not exist: start one") self.startSession() s = self.s # Replace for the real values @@ -201,38 +201,38 @@ class DownloadUtils(): # THE EXCEPTIONS except requests.exceptions.SSLError as e: - log.warn("Invalid SSL certificate for: %s" % url) - log.warn(e) + LOG.warn("Invalid SSL certificate for: %s", url) + LOG.warn(e) except requests.exceptions.ConnectionError as e: # Connection error - log.warn("Server unreachable at: %s" % url) - log.warn(e) + LOG.warn("Server unreachable at: %s", url) + LOG.warn(e) except requests.exceptions.Timeout as e: - log.warn("Server timeout at: %s" % url) - log.warn(e) + LOG.warn("Server timeout at: %s", url) + LOG.warn(e) except requests.exceptions.HTTPError as e: - log.warn('HTTP Error at %s' % url) - log.warn(e) + LOG.warn('HTTP Error at %s', url) + LOG.warn(e) except requests.exceptions.TooManyRedirects as e: - log.warn("Too many redirects connecting to: %s" % url) - log.warn(e) + LOG.warn("Too many redirects connecting to: %s", url) + LOG.warn(e) except requests.exceptions.RequestException as e: - log.warn("Unknown error connecting to: %s" % url) - log.warn(e) + LOG.warn("Unknown error connecting to: %s", url) + LOG.warn(e) except SystemExit: - log.info('SystemExit detected, aborting download') + LOG.info('SystemExit detected, aborting download') self.stopSession() except: - log.warn('Unknown error while downloading. Traceback:') + LOG.warn('Unknown error while downloading. Traceback:') import traceback - log.warn(traceback.format_exc()) + LOG.warn(traceback.format_exc()) # THE RESPONSE ##### else: @@ -254,19 +254,19 @@ class DownloadUtils(): # Called when checking a connect - no need for rash action return 401 r.encoding = 'utf-8' - log.warn('HTTP error 401 from PMS %s' % url) - log.info(r.text) + LOG.warn('HTTP error 401 from PMS %s', url) + LOG.info(r.text) if '401 Unauthorized' in r.text: # Truly unauthorized window('countUnauthorized', value=str(int(window('countUnauthorized')) + 1)) if (int(window('countUnauthorized')) >= self.unauthorizedAttempts): - log.warn('We seem to be truly unauthorized for PMS' - ' %s ' % url) + LOG.warn('We seem to be truly unauthorized for PMS' + ' %s ', url) if state.PMS_STATUS not in ('401', 'Auth'): # Tell userclient token has been revoked. - log.debug('Setting PMS server status to ' + LOG.debug('Setting PMS server status to ' 'unauthorized') state.PMS_STATUS = '401' window('plex_serverStatus', value="401") @@ -276,7 +276,7 @@ class DownloadUtils(): icon='{error}') else: # there might be other 401 where e.g. PMS under strain - log.info('PMS might only be under strain') + LOG.info('PMS might only be under strain') return 401 elif r.status_code in (200, 201): @@ -304,18 +304,18 @@ class DownloadUtils(): # update pass else: - log.warn("Unable to convert the response for: " - "%s" % url) - log.warn("Received headers were: %s" % r.headers) - log.warn('Received text: %s', r.text) + LOG.warn("Unable to convert the response for: " + "%s", url) + LOG.warn("Received headers were: %s", r.headers) + LOG.warn('Received text: %s', r.text) return True elif r.status_code == 403: # E.g. deleting a PMS item - log.warn('PMS sent 403: Forbidden error for url %s' % url) + LOG.warn('PMS sent 403: Forbidden error for url %s', url) return None else: r.encoding = 'utf-8' - log.warn('Unknown answer from PMS %s with status code %s. ', + LOG.warn('Unknown answer from PMS %s with status code %s. ', url, r.status_code) return True @@ -326,8 +326,8 @@ class DownloadUtils(): window('countError', value=str(int(window('countError')) + 1)) if int(window('countError')) >= self.connectionAttempts: - log.warn('Failed to connect to %s too many times. ' - 'Declare PMS dead' % url) + LOG.warn('Failed to connect to %s too many times. ' + 'Declare PMS dead', url) window('plex_online', value="false") except: # 'countError' not yet set From 73f7fc7644ba49ca703884f79c35474a50cc484d Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 13:54:39 +0100 Subject: [PATCH 235/509] Less logging --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 8934ccd9..23d2e947 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -61,7 +61,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): pos = js.get_position(playqueue.playlistid) # Can return -1 (as in "no playlist") pos = pos if pos != -1 else 0 - LOG.info('playQueue position: %s for %s', pos, playqueue) + LOG.debug('playQueue position: %s for %s', pos, playqueue) # Have we already initiated playback? try: playqueue.items[pos] From 0d11c6db5870b38f4abc83af09ee5fe0b5f29eb6 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:04:17 +0100 Subject: [PATCH 236/509] Fix context menu playback --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 23d2e947..5a936bcb 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -177,7 +177,7 @@ def _prep_playlist_stack(xml): stack = [] for item in xml: api = API(item) - if (state.CONTEXT_MENU_PLAY is False or + if (state.CONTEXT_MENU_PLAY is False and api.getType() != v.PLEX_TYPE_CLIP): # If user chose to play via PMS or force transcode, do not # use the item path stored in the Kodi DB From 5613d76d959db5fb26276fc5b6c1dcb6349e03dc Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:05:18 +0100 Subject: [PATCH 237/509] Less logging --- resources/lib/player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 6e1363de..ee1600af 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -31,7 +31,7 @@ def playback_cleanup(): state.OLD_PLAYER_STATES[playerid] = dict(status) # Stop transcoding if status['playmethod'] == 'Transcode': - LOG.info('Tell the PMS to stop transcoding') + LOG.debug('Tell the PMS to stop transcoding') DU().downloadUrl( '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) @@ -39,7 +39,7 @@ def playback_cleanup(): status = dict(state.PLAYSTATE) # As all playback has halted, reset the players that have been active state.ACTIVE_PLAYERS = [] - LOG.info('Finished PKC playback cleanup') + LOG.debug('Finished PKC playback cleanup') class PKC_Player(Player): @@ -75,12 +75,12 @@ class PKC_Player(Player): """ Will be called when playback is stopped by the user """ - LOG.info("ONPLAYBACK_STOPPED") + LOG.debug("ONPLAYBACK_STOPPED") playback_cleanup() def onPlayBackEnded(self): """ Will be called when playback ends due to the media file being finished """ - LOG.info("ONPLAYBACK_ENDED") + LOG.debug("ONPLAYBACK_ENDED") playback_cleanup() From 6cf5a08038f3de394c1bda49620218d747753892 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:09:29 +0100 Subject: [PATCH 238/509] Less logging --- resources/lib/pickler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index 6e660dd0..490da9d4 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -46,7 +46,7 @@ def unpickle_me(window_var='plex_result'): pickl_window(window_var, clear=True) log('%sStart unpickling' % PREFIX, level=LOGDEBUG) obj = loads(result) - log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG) + log('%sSuccessfully unpickled' % PREFIX, level=LOGDEBUG) return obj From c8c453c0318ac8d7e51378a6f7347f720996f227 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:26:58 +0100 Subject: [PATCH 239/509] Clear playqueue if there was only 1 item in it --- resources/lib/player.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/lib/player.py b/resources/lib/player.py index ee1600af..29d4b660 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -7,6 +7,7 @@ from xbmc import Player from downloadutils import DownloadUtils as DU from plexbmchelper.subscribers import LOCKER +import playqueue as PQ import variables as v import state @@ -24,6 +25,7 @@ def playback_cleanup(): """ # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) + LOG.debug('playback_cleanup called') state.PLEX_TRANSIENT_TOKEN = None for playerid in state.ACTIVE_PLAYERS: status = state.PLAYER_STATES[playerid] @@ -35,6 +37,10 @@ def playback_cleanup(): DU().downloadUrl( '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + # Kodi will not clear the playqueue (because there is not really any) + # if there is only 1 item in it + if len(PQ.PLAYQUEUES[playerid].items) == 1: + PQ.PLAYQUEUES[playerid].clear() # Reset the player's status status = dict(state.PLAYSTATE) # As all playback has halted, reset the players that have been active From 6075642e9e4319177860b6b333ac520e583f5811 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:49:56 +0100 Subject: [PATCH 240/509] 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 5c2d67ba..58fb76cc 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -445,7 +445,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): item = playlist_item_from_xml(playlist, xml[0]) except (KeyError, IndexError, TypeError): raise PlaylistError('Could not init Plex playlist with plex_id %s and ' - 'kodi_item %s', plex_id, kodi_item) + 'kodi_item %s' % (plex_id, kodi_item)) playlist.items.append(item) LOG.debug('Initialized the playlist on the Plex side: %s', playlist) return item From 76bd6e934a7d26d4aa0aab357d0746502fd525d2 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:54:54 +0100 Subject: [PATCH 241/509] Fix PlaylistError --- resources/lib/kodimonitor.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index e54a2948..7a027283 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -238,12 +238,15 @@ class KodiMonitor(Monitor): LOG.debug('Playqueue not initiated - ignoring') return # Playlist has been updated; need to tell Plex about it - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, kodi_item=data['item']) - else: - PL.add_item_to_PMS_playlist(playqueue, - data['position'], - kodi_item=data['item']) + try: + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, kodi_item=data['item']) + else: + PL.add_item_to_PMS_playlist(playqueue, + data['position'], + kodi_item=data['item']) + except PL.PlaylistError: + pass # Make sure that we won't re-add this item # playqueue.old_kodi_pl = kodi_playqueue From 48dc22ee353be2552bb266cfb1186483d7d5f20e Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 14:59:43 +0100 Subject: [PATCH 242/509] Fix PlaylistError --- resources/lib/kodimonitor.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7a027283..c0fe71fc 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -375,13 +375,18 @@ class KodiMonitor(Monitor): except RuntimeError: return LOG.info('Need to initialize Plex and PKC playqueue') - if plex_id: - item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) - else: - item = PL.init_Plex_playlist(playqueue, - kodi_item={'id': kodi_id, - 'type': kodi_type, - 'file': path}) + try: + if plex_id: + item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) + else: + item = PL.init_Plex_playlist(playqueue, + kodi_item={'id': kodi_id, + 'type': kodi_type, + 'file': path}) + except PL.PlaylistError: + LOG.info('Could not initialize our playlist') + # Avoid errors + item = PL.Playlist_Item() # Set the Plex container key (e.g. using the Plex playqueue) container_key = None if info['playlistid'] != -1: From 35536fdc2fcfcf3a680b81d16a418d0b30f80978 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 15:04:05 +0100 Subject: [PATCH 243/509] Remove obsolete playlistitem attribute --- resources/lib/kodimonitor.py | 8 -------- resources/lib/playlist_func.py | 3 --- 2 files changed, 11 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c0fe71fc..b31f777b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -247,8 +247,6 @@ class KodiMonitor(Monitor): kodi_item=data['item']) except PL.PlaylistError: pass - # Make sure that we won't re-add this item - # playqueue.old_kodi_pl = kodi_playqueue @LOCKER.lockthis def _playlist_onremove(self, data): @@ -264,13 +262,7 @@ class KodiMonitor(Monitor): if playqueue.is_kodi_onremove() is False: LOG.debug('PKC removed this item already from playqueue - ignoring') return - # Check whether we even need to update our known playqueue - kodi_playqueue = js.playlist_get_items(data['playlistid']) - if playqueue.old_kodi_pl == kodi_playqueue: - # We already know the latest playqueue - nothing to do - return PL.delete_playlist_item_from_PMS(playqueue, data['position']) - playqueue.old_kodi_pl = kodi_playqueue @LOCKER.lockthis def _playlist_onclear(self, data): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 58fb76cc..de004c9d 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -38,7 +38,6 @@ class PlaylistObjectBaseclase(object): self.type = None self.kodi_pl = None self.items = [] - self.old_kodi_pl = [] self.id = None self.version = None self.selectedItemID = None @@ -137,7 +136,6 @@ class PlaylistObjectBaseclase(object): self.kodi_onclear() self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] - self.old_kodi_pl = [] self.id = None self.version = None self.selectedItemID = None @@ -163,7 +161,6 @@ class Playqueue_Object(PlaylistObjectBaseclase): type = None [str] Kodi type: 'audio', 'video', 'picture' kodi_pl = None Kodi xbmc.PlayList object items = [] [list] of Playlist_Items - old_kodi_pl = [] [list] store old Kodi JSON result with all pl items id = None [str] Plex playQueueID, unique Plex identifier version = None [int] Plex version of the playQueue selectedItemID = None From bee845ca95561f5a4a5d1b3237d1604b4f4c5cf3 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 15:12:10 +0100 Subject: [PATCH 244/509] Fix PKC clearing Kodi playlist --- resources/lib/kodimonitor.py | 2 +- resources/lib/playlist_func.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index b31f777b..26900e57 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -276,7 +276,7 @@ class KodiMonitor(Monitor): if playqueue.is_kodi_onclear() is False: LOG.debug('PKC already cleared the playqueue - ignoring') return - playqueue.clear() + playqueue.clear(kodi=False) def _get_ids(self, json_item): """ diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index de004c9d..ad2d1fa7 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -126,13 +126,15 @@ class PlaylistObjectBaseclase(object): return True return False - def clear(self): + def clear(self, kodi=True): """ - Resets the playlist object to an empty playlist + Resets the playlist object to an empty playlist. + + Pass kodi=False in order to NOT clear the Kodi playqueue """ # kodi monitor's on_clear method will only be called if there were some # items to begin with - if self.kodi_pl.size() != 0: + if kodi and self.kodi_pl.size() != 0: self.kodi_onclear() self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] @@ -423,7 +425,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Returns the first PKC playlist item or raises PlaylistError """ LOG.debug('Initializing the playlist %s on the Plex side', playlist) - playlist.clear() + playlist.clear(kodi=False) try: if plex_id: item = playlist_item_from_plex(plex_id) From c6edaf4304649fb0fcd9b7223fc8ab70d0d59340 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 15:46:41 +0100 Subject: [PATCH 245/509] Fix wrong exception type --- resources/lib/playqueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 36d45804..5c50761c 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -119,7 +119,7 @@ def update_playqueue_from_PMS(playqueue, playqueue.clear() try: PL.get_playlist_details_from_xml(playqueue, xml) - except KeyError: + except PL.PlaylistError: LOG.error('Could not get playqueue ID %s', playqueue_id) return playqueue.repeat = 0 if not repeat else int(repeat) From 698217d374785e917a0872b898ff48455f48eca2 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 15:54:00 +0100 Subject: [PATCH 246/509] Fix items being added twice to playqueue --- resources/lib/playback.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 5a936bcb..5cd34455 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -220,10 +220,9 @@ def _prep_playlist_stack(xml): return stack -def _process_stack(playqueue, stack, fill_queue=False): +def _process_stack(playqueue, stack): """ Takes our stack and adds the items to the PKC and Kodi playqueues. - Pass fill_queue=True in order to append Playlist_Items to playqueue.items """ # getposition() can return -1 pos = max(playqueue.kodi_pl.getposition(), 0) + 1 @@ -250,8 +249,6 @@ def _process_stack(playqueue, stack, fill_queue=False): playlist_item.force_transcode = state.FORCE_TRANSCODE playlist_item.init_done = True pos += 1 - if fill_queue: - playqueue.items.append(playlist_item) def conclude_playback(playqueue, pos): @@ -374,7 +371,7 @@ def play_xml(playqueue, xml, offset=None): """ LOG.info("play_xml called") stack = _prep_playlist_stack(xml) - _process_stack(playqueue, stack, fill_queue=True) + _process_stack(playqueue, stack) LOG.debug('Playqueue after play_xml update: %s', playqueue) for startpos, item in enumerate(playqueue.items): if item.id == playqueue.selectedItemID: From a2b4b48ddcc702c67e69fefebffcfd86b1452e23 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 15:57:37 +0100 Subject: [PATCH 247/509] Less logging --- resources/lib/kodimonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 26900e57..e3d3867a 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -349,7 +349,7 @@ class KodiMonitor(Monitor): json_item = js.get_item(playerid) path = json_item.get('file') pos = info['position'] if info['position'] != -1 else 0 - LOG.info('Detected position %s for %s', pos, playqueue) + LOG.debug('Detected position %s for %s', pos, playqueue) try: item = playqueue.items[pos] # See if playback.py already initiated playback From 68887772dfe9622ed1c3d94f135680640fbd50fc Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 16:16:53 +0100 Subject: [PATCH 248/509] Fix missing containerKey --- resources/lib/kodimonitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index e3d3867a..952f3bb9 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -350,6 +350,7 @@ class KodiMonitor(Monitor): path = json_item.get('file') pos = info['position'] if info['position'] != -1 else 0 LOG.debug('Detected position %s for %s', pos, playqueue) + status = state.PLAYER_STATES[playerid] try: item = playqueue.items[pos] # See if playback.py already initiated playback @@ -361,6 +362,7 @@ class KodiMonitor(Monitor): kodi_type = item.kodi_type plex_id = item.plex_id plex_type = item.plex_type + container_key = '/playQueues/%s' % playqueue.id else: try: kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) @@ -388,10 +390,9 @@ class KodiMonitor(Monitor): container_key = '/playQueues/%s' % container_key elif plex_id is not None: container_key = '/library/metadata/%s' % plex_id - state.PLAYER_STATES[playerid]['container_key'] = container_key LOG.debug('Set the Plex container_key to: %s', container_key) - status = state.PLAYER_STATES[playerid] status.update(info) + status['container_key'] = container_key status['file'] = path status['kodi_id'] = kodi_id status['kodi_type'] = kodi_type From adb43b2bbfbb0e9db683154d5ad820f68ac59552 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 16:20:10 +0100 Subject: [PATCH 249/509] Prettify --- resources/lib/kodimonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 952f3bb9..a2ae9a8f 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -400,7 +400,7 @@ class KodiMonitor(Monitor): status['plex_type'] = plex_type status['playmethod'] = item.playmethod status['playcount'] = item.playcount - LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) + LOG.debug('Set the player state: %s', status) @thread_methods From fd4422fa65c6e5c61e5b512a0da7cd076a7ac7e0 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 16:40:24 +0100 Subject: [PATCH 250/509] Fix Kodi queueing several items --- resources/lib/kodimonitor.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index a2ae9a8f..50600ac3 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -317,6 +317,24 @@ class KodiMonitor(Monitor): pass return kodi_id, kodi_type, plex_id, plex_type + @staticmethod + def _add_remaining_items_to_playlist(playqueue): + """ + Adds all but the very first item of the Kodi playlist to the Plex + playqueue + """ + items = js.playlist_get_items(playqueue.playlistid) + if not items: + LOG.error('Could not retrieve Kodi playlist items') + return + # Remove first item + items.pop(0) + try: + for i, item in enumerate(items): + PL.add_item_to_PMS_playlist(playqueue, i + 1, kodi_item=item) + except PL.PlaylistError: + LOG.info('Could not build Plex playlist for: %s', items) + @LOCKER.lockthis def PlayBackStart(self, data): """ @@ -381,6 +399,9 @@ class KodiMonitor(Monitor): LOG.info('Could not initialize our playlist') # Avoid errors item = PL.Playlist_Item() + # Make sure we've added all items of the Kodi playqueue + if playqueue.kodi_pl.size() > 1: + self._add_remaining_items_to_playlist(playqueue) # Set the Plex container key (e.g. using the Plex playqueue) container_key = None if info['playlistid'] != -1: From a6ce6ae8d2f828d855bea047aaedb1cfd2623bf4 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 16:43:39 +0100 Subject: [PATCH 251/509] Fix KeyError for Kodi playerids --- resources/lib/state.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/state.py b/resources/lib/state.py index 1da14c2d..9e371232 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -91,15 +91,15 @@ ACTIVE_PLAYERS = [] # Kodi player states - here, initial values are set PLAYER_STATES = { + 0: {}, 1: {}, - 2: {}, - 3: {} + 2: {} } # The LAST playstate once playback is finished OLD_PLAYER_STATES = { + 0: {}, 1: {}, - 2: {}, - 3: {} + 2: {} } # "empty" dict for the PLAYER_STATES above PLAYSTATE = { From 8d1bd5232897f28273507aa8070885b98d5230cb Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 16:49:58 +0100 Subject: [PATCH 252/509] Fix Companion KeyError for music playback --- resources/lib/plexbmchelper/subscribers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 3ba4ebed..c09b76a0 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -241,13 +241,12 @@ class SubscriptionMgr(object): elif playqueue.plex_transient_token: answ['token'] = playqueue.plex_transient_token # Process audio and subtitle streams - if ptype != v.PLEX_PLAYLIST_TYPE_PHOTO: + if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO: strm_id = self._plex_stream_index(playerid, 'audio') if strm_id: answ['audioStreamID'] = strm_id else: LOG.error('We could not select a Plex audiostream') - if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO: strm_id = self._plex_stream_index(playerid, 'video') if strm_id: answ['videoStreamID'] = strm_id From 199939c8b77b19471fec35ff0030771db5287dc7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Feb 2018 17:03:36 +0100 Subject: [PATCH 253/509] Fix skipping back in Kodi playlist --- resources/lib/json_rpc.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index a2ef0b03..9e1df360 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -183,6 +183,18 @@ def skipnext(): def skipprevious(): + """ + Skips to the previous item to play for all Kodi players + Using a HACK to make sure we're not just starting same item over again + """ + for playerid in get_player_ids(): + try: + skipto(get_position(playerid) - 1) + except (KeyError, TypeError): + pass + + +def wont_work_skipprevious(): """ Skips to the previous item to play for all Kodi players """ From 3174521475bea34b64299666cd14c64a920eeaa7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 12:06:39 +0100 Subject: [PATCH 254/509] Reintroduce Kodi playlist polling There is no way around it - Kodi does not tell if the user swaps items in the Kodi playlist, unfortunately --- resources/lib/kodimonitor.py | 41 ++------------- resources/lib/playlist_func.py | 71 ++----------------------- resources/lib/playqueue.py | 95 +++++++++++++++++++++++++++++++--- service.py | 3 ++ 4 files changed, 99 insertions(+), 111 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 50600ac3..6f05d127 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -69,7 +69,7 @@ class KodiMonitor(Monitor): """ Will be called when Kodi starts scanning the library """ - LOG.debug("Kodi library scan %s running." % library) + LOG.debug("Kodi library scan %s running.", library) if library == "video": window('plex_kodiScan', value="true") @@ -77,7 +77,7 @@ class KodiMonitor(Monitor): """ Will be called when Kodi finished scanning the library """ - LOG.debug("Kodi library scan %s finished." % library) + LOG.debug("Kodi library scan %s finished.", library) if library == "video": window('plex_kodiScan', clear=True) @@ -209,11 +209,6 @@ class KodiMonitor(Monitor): } Will NOT be called if playback initiated by Kodi widgets """ - playqueue = PQ.PLAYQUEUES[data['playlistid']] - # Did PKC cause this add? Then lets not do anything - if playqueue.is_kodi_onadd() is False: - LOG.debug('PKC added this item to the playqueue - ignoring') - return kodi_item = js.get_item(data['playlistid']) if (state.RESUMABLE is True and not kodi_item['file'] and data['position'] == 0 and @@ -233,22 +228,7 @@ class KodiMonitor(Monitor): thread = Thread(target=playback_triage, kwargs=kwargs) thread.start() return - # Have we initiated the playqueue already? If not, ignore this - if not playqueue.items: - LOG.debug('Playqueue not initiated - ignoring') - return - # Playlist has been updated; need to tell Plex about it - try: - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, kodi_item=data['item']) - else: - PL.add_item_to_PMS_playlist(playqueue, - data['position'], - kodi_item=data['item']) - except PL.PlaylistError: - pass - @LOCKER.lockthis def _playlist_onremove(self, data): """ Called if an item is removed from a Kodi playlist. Example data dict: @@ -257,14 +237,8 @@ class KodiMonitor(Monitor): u'position': 0 } """ - playqueue = PQ.PLAYQUEUES[data['playlistid']] - # Did PKC cause this add? Then lets not do anything - if playqueue.is_kodi_onremove() is False: - LOG.debug('PKC removed this item already from playqueue - ignoring') - return - PL.delete_playlist_item_from_PMS(playqueue, data['position']) + pass - @LOCKER.lockthis def _playlist_onclear(self, data): """ Called if a Kodi playlist is cleared. Example data dict: @@ -272,11 +246,7 @@ class KodiMonitor(Monitor): u'playlistid': 1, } """ - playqueue = PQ.PLAYQUEUES[data['playlistid']] - if playqueue.is_kodi_onclear() is False: - LOG.debug('PKC already cleared the playqueue - ignoring') - return - playqueue.clear(kodi=False) + pass def _get_ids(self, json_item): """ @@ -399,9 +369,6 @@ class KodiMonitor(Monitor): LOG.info('Could not initialize our playlist') # Avoid errors item = PL.Playlist_Item() - # Make sure we've added all items of the Kodi playqueue - if playqueue.kodi_pl.size() > 1: - self._add_remaining_items_to_playlist(playqueue) # Set the Plex container key (e.g. using the Plex playqueue) container_key = None if info['playlistid'] != -1: diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index ad2d1fa7..4c42fa4a 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -45,11 +45,8 @@ class PlaylistObjectBaseclase(object): self.shuffled = 0 self.repeat = 0 self.plex_transient_token = None - # Needed to not add an item twice (first through PKC, then the kodi - # monitor) - self._onadd_queue = [] - self._onremove_queue = [] - self._onclear_queue = [] + # Need a hack for detecting swaps of elements + self.old_kodi_pl = [] def __repr__(self): """ @@ -69,63 +66,6 @@ class PlaylistObjectBaseclase(object): answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) return answ + '\'items\': %s}}' % self.items - def kodi_onadd(self): - """ - Call this before adding an item to the Kodi playqueue - """ - self._onadd_queue.append(None) - - def is_kodi_onadd(self): - """ - Returns False if the last kodimonitor on_add was caused by PKC - so that - we are not adding a playlist item twice. - - Calling this function will remove the item from our "checklist" - """ - try: - self._onadd_queue.pop() - except IndexError: - return True - return False - - def kodi_onremove(self): - """ - Call this before removing an item from the Kodi playqueue - """ - self._onremove_queue.append(None) - - def is_kodi_onremove(self): - """ - Returns False if the last kodimonitor on_remove was caused by PKC - so - that we are not adding a playlist item twice. - - Calling this function will remove the item from our "checklist" - """ - try: - self._onremove_queue.pop() - except IndexError: - return True - return False - - def kodi_onclear(self): - """ - Call this before clearing the Kodi playqueue IF it was not empty - """ - self._onclear_queue.append(None) - - def is_kodi_onclear(self): - """ - Returns False if the last kodimonitor on_remove was caused by PKC - so - that we are not clearing the playlist twice. - - Calling this function will remove the item from our "checklist" - """ - try: - self._onclear_queue.pop() - except IndexError: - return True - return False - def clear(self, kodi=True): """ Resets the playlist object to an empty playlist. @@ -135,7 +75,6 @@ class PlaylistObjectBaseclase(object): # kodi monitor's on_clear method will only be called if there were some # items to begin with if kodi and self.kodi_pl.size() != 0: - self.kodi_onclear() self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] self.id = None @@ -145,6 +84,7 @@ class PlaylistObjectBaseclase(object): self.shuffled = 0 self.repeat = 0 self.plex_transient_token = None + self.old_kodi_pl = [] LOG.debug('Playlist cleared: %s', self) @@ -503,10 +443,8 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)} else: params['item'] = {'file': item.file} - playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: - playlist.is_kodi_onadd() raise PlaylistError('Could not add item to playlist. Kodi reply. %s', reply) return item @@ -572,10 +510,8 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'%sid' % kodi_type: int(kodi_id)} else: params['item'] = {'file': file} - playlist.kodi_onadd() reply = js.playlist_insert(params) if reply.get('error') is not None: - playlist.is_kodi_onadd() raise PlaylistError('Could not add item to playlist. Kodi reply. %s', reply) if xml_video_element is not None: @@ -694,7 +630,6 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, LOG.debug('Insert listitem at position %s for Kodi only for %s', pos, playlist) # Add the item into Kodi playlist - playlist.kodi_onadd() playlist.kodi_pl.add(url=file, listitem=listitem, index=pos) # We need to add this to our internal queue as well if xml_video_element is not None: diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 5c50761c..00a29f73 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -2,14 +2,15 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly """ from logging import getLogger -from threading import RLock, Thread +from threading import Thread -from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO +from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep -from utils import window +from utils import thread_methods import playlist_func as PL -from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren +from PlexFunctions import GetAllPlexChildren from PlexAPI import API +from plexbmchelper.subscribers import LOCK from playback import play_xml import json_rpc as js import variables as v @@ -17,8 +18,6 @@ import variables as v ############################################################################### LOG = getLogger("PLEX." + __name__) -# lock used for playqueue manipulations -LOCK = RLock() PLUGIN = 'plugin://%s' % v.ADDON_ID # Our PKC playqueues (3 instances of Playqueue_Object()) @@ -125,3 +124,87 @@ def update_playqueue_from_PMS(playqueue, playqueue.repeat = 0 if not repeat else int(repeat) playqueue.plex_transient_token = transient_token play_xml(playqueue, xml, offset) + + +@thread_methods(add_suspends=['PMS_STATUS']) +class PlayqueueMonitor(Thread): + """ + Unfortunately, Kodi does not tell if items within a Kodi playqueue + (playlist) are swapped. This is what this monitor is for. Don't replace + this mechanism till Kodi's implementation of playlists has improved + """ + def _compare_playqueues(self, playqueue, new): + """ + Used to poll the Kodi playqueue and update the Plex playqueue if needed + """ + old = list(playqueue.items) + index = list(range(0, len(old))) + LOG.debug('Comparing new Kodi playqueue %s with our play queue %s', + new, old) + for i, new_item in enumerate(new): + if (new_item['file'].startswith('plugin://') and + not new_item['file'].startswith(PLUGIN)): + # Ignore new media added by other addons + continue + for j, old_item in enumerate(old): + if self.thread_stopped(): + # Chances are that we got an empty Kodi playlist due to + # Kodi exit + return + try: + if (old_item.file.startswith('plugin://') and + not old_item.file.startswith(PLUGIN)): + # Ignore media by other addons + continue + except AttributeError: + # were not passed a filename; ignore + pass + if new_item.get('id') is None: + identical = old_item.file == new_item['file'] + else: + identical = (old_item.kodi_id == new_item['id'] and + old_item.kodi_type == new_item['type']) + if j == 0 and identical: + del old[j], index[j] + break + elif identical: + LOG.debug('Detected playqueue item %s moved to position %s', + i + j, i) + PL.move_playlist_item(playqueue, i + j, i) + del old[j], index[j] + break + else: + LOG.debug('Detected new Kodi element at position %s: %s ', + i, new_item) + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, + kodi_item=new_item) + else: + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + for j in range(i, len(index)): + index[j] += 1 + for i in reversed(index): + LOG.debug('Detected deletion of playqueue element at pos %s', i) + PL.delete_playlist_item_from_PMS(playqueue, i) + LOG.debug('Done comparing playqueues') + + def run(self): + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended + LOG.info("----===## Starting PlayqueueMonitor ##===----") + while not thread_stopped(): + while thread_suspended(): + if thread_stopped(): + break + sleep(1000) + with LOCK: + for playqueue in PLAYQUEUES: + kodi_pl = js.playlist_get_items(playqueue.playlistid) + if playqueue.old_kodi_pl != kodi_pl: + # compare old and new playqueue + self._compare_playqueues(playqueue, kodi_pl) + playqueue.old_kodi_pl = list(kodi_pl) + sleep(200) + LOG.info("----===## PlayqueueMonitor stopped ##===----") diff --git a/service.py b/service.py index 2b83adbd..fa695159 100644 --- a/service.py +++ b/service.py @@ -43,6 +43,7 @@ import PlexAPI from PlexCompanion import PlexCompanion from command_pipeline import Monitor_Window from playback_starter import Playback_Starter +from playqueue import PlayqueueMonitor from artwork import Image_Cache_Thread from json_rpc import get_setting, set_setting import variables as v @@ -174,6 +175,7 @@ class Service(): self.plexCompanion = PlexCompanion() self.specialMonitor = SpecialMonitor() self.playback_starter = Playback_Starter() + self.playqueue = PlayqueueMonitor() if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() @@ -236,6 +238,7 @@ class Service(): if not self.playback_starter_running: self.playback_starter_running = True self.playback_starter.start() + self.playqueue.start() if (not self.image_cache_thread_running and settings('enableTextureCache') == "true"): self.image_cache_thread_running = True From 57ec06ae4d507415af20fea598f991b400105cd7 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 12:17:05 +0100 Subject: [PATCH 255/509] Don't empty entire playqueue on Kodi exit --- resources/lib/playqueue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 00a29f73..cf5a85e5 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -186,6 +186,10 @@ class PlayqueueMonitor(Thread): for j in range(i, len(index)): index[j] += 1 for i in reversed(index): + if self.thread_stopped(): + # Chances are that we got an empty Kodi playlist due to + # Kodi exit + return LOG.debug('Detected deletion of playqueue element at pos %s', i) PL.delete_playlist_item_from_PMS(playqueue, i) LOG.debug('Done comparing playqueues') From 820b5147403a002b0aba1efea60cc816a7d0929d Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 12:22:10 +0100 Subject: [PATCH 256/509] Fix KeyErrors --- resources/lib/librarysync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index f64014ab..cb2f9286 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1290,7 +1290,7 @@ class LibrarySync(Thread): if status == 'buffering': continue ratingKey = str(item['ratingKey']) - for pid in (1, 2, 3): + for pid in (0, 1, 2): if ratingKey == state.PLAYER_STATES[pid]['plex_id']: # Kodi is playing this item - no need to set the playstate continue From a1d790c741aef0ce7a8b2974ea43650182be8422 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 12:31:28 +0100 Subject: [PATCH 257/509] Signal stop to Companion clients correctly --- resources/lib/plexbmchelper/subscribers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index c09b76a0..f83b05f0 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -337,11 +337,8 @@ class SubscriptionMgr(object): self._notify_server(players) if self.subscribers: msg = self.msg(players) - if self.isplaying is True: - # If we don't check here, Plex Companion devices will simply - # drop out of the Plex Companion playback screen - for subscriber in self.subscribers.values(): - subscriber.send_update(msg) + for subscriber in self.subscribers.values(): + subscriber.send_update(msg) self.lastplayers = players def _notify_server(self, players): From b9c1bbd8d3a6f6b411c5d2da78a853155d47664f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 15:36:30 +0100 Subject: [PATCH 258/509] Fix Companion playback for Plex Web --- resources/lib/plexbmchelper/listener.py | 86 ++++++++++++++++++---- resources/lib/plexbmchelper/subscribers.py | 3 + 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index d4aa46bc..5e86257e 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -7,7 +7,8 @@ from SocketServer import ThreadingMixIn from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urlparse import urlparse, parse_qs -from xbmc import sleep +from xbmc import sleep, Player, Monitor + from companion import process_command import json_rpc as js from clientinfo import getXArgsDeviceInfo @@ -16,6 +17,11 @@ import variables as v ############################################################################### LOG = getLogger("PLEX." + __name__) +PLAYER = Player() +MONITOR = Monitor() + +# Hack we need in order to keep track of the open connections from Plex Web +CLIENT_DICT = {} ############################################################################### @@ -42,8 +48,8 @@ class MyHandler(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' def __init__(self, *args, **kwargs): - BaseHTTPRequestHandler.__init__(self, *args, **kwargs) self.serverlist = [] + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def do_HEAD(self): LOG.debug("Serving HEAD request...") @@ -117,21 +123,69 @@ class MyHandler(BaseHTTPRequestHandler): title=v.DEVICENAME, machineIdentifier=v.PKC_MACHINE_IDENTIFIER), getXArgsDeviceInfo(include_token=False)) - elif "/poll" in request_path: + elif request_path == 'player/timeline/poll': + # Plex web does polling if connected to PKC via Companion + # Only reply if there is indeed something playing + # Otherwise, all clients seem to keep connection open if params.get('wait') == '1': - sleep(950) - self.response( - sub_mgr.msg(js.get_players()).format( - command_id=params.get('commandID', 0)), - { - 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'X-Plex-Protocol': '1.0', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Max-Age': '1209600', - 'Access-Control-Expose-Headers': - 'X-Plex-Client-Identifier', - 'Content-Type': 'text/xml;charset=utf-8' - }) + MONITOR.waitForAbort(0.95) + if self.client_address[0] not in CLIENT_DICT: + CLIENT_DICT[self.client_address[0]] = [] + tracker = CLIENT_DICT[self.client_address[0]] + tracker.append(self.client_address[1]) + while (not PLAYER.isPlaying() and + not MONITOR.abortRequested() and + sub_mgr.stop_sent_to_web and not + (len(tracker) >= 4 and + tracker[0] == self.client_address[1])): + # Keep at most 3 connections open, then drop the first one + # Doesn't need to be thread-save + # Silly stuff really + MONITOR.waitForAbort(1) + # Let PKC know that we're releasing this connection + tracker.pop(0) + msg = sub_mgr.msg(js.get_players()).format( + command_id=params.get('commandID', 0)) + if sub_mgr.isplaying: + self.response( + msg, + { + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', + 'Access-Control-Expose-Headers': + 'X-Plex-Client-Identifier', + 'Content-Type': 'text/xml;charset=utf-8' + }) + elif not sub_mgr.stop_sent_to_web: + sub_mgr.stop_sent_to_web = True + LOG.debug('Signaling STOP to Plex Web') + self.response( + msg, + { + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', + 'Access-Control-Expose-Headers': + 'X-Plex-Client-Identifier', + 'Content-Type': 'text/xml;charset=utf-8' + }) + else: + # Fail connection with HTTP 500 error - has been open too long + self.response( + 'Need to close this connection on the PKC side', + { + 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, + 'X-Plex-Protocol': '1.0', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': '1209600', + 'Access-Control-Expose-Headers': + 'X-Plex-Client-Identifier', + 'Content-Type': 'text/xml;charset=utf-8' + }, + code=500) elif "/subscribe" in request_path: self.response(v.COMPANION_OK_MESSAGE, getXArgsDeviceInfo(include_token=False)) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index f83b05f0..66ecd99e 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -123,6 +123,8 @@ class SubscriptionMgr(object): # In order to be able to signal a stop at the end self.last_params = {} self.lastplayers = {} + # In order to signal a stop to Plex Web ONCE on playback stop + self.stop_sent_to_web = True self.xbmcplayer = player self.request_mgr = request_mgr @@ -190,6 +192,7 @@ class SubscriptionMgr(object): 'state': 'stopped' } self.isplaying = True + self.stop_sent_to_web = False pbmc_server = window('pms_server') if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') From 14635fea4d02053c7c5afd87928fc99033d1d496 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 16:58:10 +0100 Subject: [PATCH 259/509] Ignore first 60s of playback like Plex --- resources/lib/initialsetup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 8abdd2d1..9cb25373 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -419,6 +419,8 @@ class InitialSetup(): xml.set_setting(['video', 'ignorepercentatend'], value='10') xml.set_setting(['video', 'playcountminimumpercent'], value='90') + xml.set_setting(['video', 'ignoresecondsatstart'], + value='60') reboot = xml.write_xml except etree.ParseError: cache = None From e82d5fec6c687ed7b18a615eb4d57670212c0962 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 17:36:18 +0100 Subject: [PATCH 260/509] Clear playqueue on using context menu --- resources/lib/context_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index c96a469e..b86fffe1 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -9,6 +9,7 @@ import plexdb_functions as plexdb from utils import window, settings, dialog, language as lang from dialogs import context from PlexFunctions import delete_item_from_pms +import playqueue as PQ import variables as v import state @@ -169,6 +170,9 @@ class ContextMenu(object): """ For using direct paths: Initiates playback using the PMS """ + playqueue = PQ.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type]) + playqueue.clear() state.CONTEXT_MENU_PLAY = True params = { 'mode': 'play', From dea8e6d5f56f64169b124145775690535fdb5e5c Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Feb 2018 17:42:15 +0100 Subject: [PATCH 261/509] Use languageCode, not language for temp subtitles - Fixes #394 --- resources/lib/playutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 11cfe418..7fbadede 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -298,7 +298,7 @@ class PlayUtils(): if 'language' in stream.attrib: path = self.api.download_external_subtitles( '{server}%s' % stream.attrib['key'], - "subtitle.%s.%s" % (stream.attrib['language'], + "subtitle.%s.%s" % (stream.attrib['languageCode'], stream.attrib['codec'])) # We don't know the language - no need to download else: From 6cb69ada3f9c86ccc23ed9b02b74452dcaa52115 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 17:48:50 +0100 Subject: [PATCH 262/509] Rework websocket playstate updates from the PMS - Should fix #362 --- resources/lib/itemtypes.py | 39 ++++------ resources/lib/librarysync.py | 144 +++++++++++++++++------------------ resources/lib/variables.py | 6 ++ 3 files changed, 92 insertions(+), 97 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9b0a14e6..63c83910 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -149,37 +149,26 @@ class Items(object): db_item[4], userdata['UserRating']) - def updatePlaystate(self, item): + def updatePlaystate(self, mark_played, view_count, resume, duration, + file_id, lastViewedAt): """ Use with websockets, not xml """ # If the playback was stopped, check whether we need to increment the # playcount. PMS won't tell us the playcount via websockets - if item['state'] in ('stopped', 'ended'): - - # If offset exceeds duration skip update - if item['viewOffset'] > item['duration']: - log.error("Error while updating play state, viewOffset " - "exceeded duration") - return - - complete = float(item['viewOffset']) / float(item['duration']) - log.info('Item %s stopped with completion rate %s percent.' - 'Mark item played at %s percent.' - % (item['ratingKey'], str(complete), v.MARK_PLAYED_AT), 1) - if complete >= v.MARK_PLAYED_AT: - log.info('Marking as completely watched in Kodi') - try: - item['viewCount'] += 1 - except TypeError: - item['viewCount'] = 1 - item['viewOffset'] = 0 + if mark_played: + log.info('Marking as completely watched in Kodi') + try: + view_count += 1 + except TypeError: + view_count = 1 + resume = 0 # Do the actual update - self.kodi_db.addPlaystate(item['file_id'], - item['viewOffset'], - item['duration'], - item['viewCount'], - item['lastViewedAt']) + self.kodi_db.addPlaystate(file_id, + resume, + duration, + view_count, + lastViewedAt) class Movies(Items): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index cb2f9286..7ccec2f3 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -45,7 +45,7 @@ class LibrarySync(Thread): """ def __init__(self): self.itemsToProcess = [] - self.sessionKeys = [] + self.sessionKeys = {} self.fanartqueue = Queue.Queue() if settings('FanartTV') == 'true': self.fanartthread = Process_Fanart_Thread(self.fanartqueue) @@ -223,6 +223,8 @@ class LibrarySync(Thread): """ repair=True: force sync EVERY item """ + # Reset our keys + self.sessionKeys = {} # self.compare == False: we're syncing EVERY item # True: we're syncing only the delta, e.g. different checksum self.compare = not repair @@ -1283,110 +1285,108 @@ class LibrarySync(Thread): Someone (not necessarily the user signed in) is playing something some- where """ - items = [] for item in data: - # Drop buffering messages immediately status = item['state'] if status == 'buffering': + # Drop buffering messages immediately continue - ratingKey = str(item['ratingKey']) + plex_id = str(item['ratingKey']) for pid in (0, 1, 2): - if ratingKey == state.PLAYER_STATES[pid]['plex_id']: + if plex_id == state.PLAYER_STATES[pid]['plex_id']: # Kodi is playing this item - no need to set the playstate continue - with plexdb.Get_Plex_DB() as plex_db: - kodi_info = plex_db.getItem_byId(ratingKey) - if kodi_info is None: - # Item not (yet) in Kodi library - continue sessionKey = item['sessionKey'] # Do we already have a sessionKey stored? if sessionKey not in self.sessionKeys: + with plexdb.Get_Plex_DB() as plex_db: + kodi_info = plex_db.getItem_byId(plex_id) + if kodi_info is None: + # Item not (yet) in Kodi library + continue if settings('plex_serverowned') == 'false': - # Not our PMS, we are not authorized to get the - # sessions + # Not our PMS, we are not authorized to get the sessions # On the bright side, it must be us playing :-) - self.sessionKeys = { - sessionKey: {} - } + self.sessionKeys[sessionKey] = {} else: # PMS is ours - get all current sessions - self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) - log.debug('Updated current sessions. They are: %s' - % self.sessionKeys) + self.sessionKeys.update(GetPMSStatus(state.PLEX_TOKEN)) + log.debug('Updated current sessions. They are: %s', + self.sessionKeys) if sessionKey not in self.sessionKeys: - log.warn('Session key %s still unknown! Skip ' - 'item' % sessionKey) + log.info('Session key %s still unknown! Skip ' + 'playstate update', sessionKey) continue - - currSess = self.sessionKeys[sessionKey] + # Attach Kodi info to the session + self.sessionKeys[sessionKey]['kodi_id'] = kodi_info[0] + self.sessionKeys[sessionKey]['file_id'] = kodi_info[1] + self.sessionKeys[sessionKey]['kodi_type'] = kodi_info[4] + session = self.sessionKeys[sessionKey] if settings('plex_serverowned') != 'false': # Identify the user - same one as signed on with PKC? Skip # update if neither session's username nor userid match # (Owner sometime's returns id '1', not always) - if (not state.PLEX_TOKEN and currSess['userId'] == '1'): + if not state.PLEX_TOKEN and session['userId'] == '1': # PKC not signed in to plex.tv. Plus owner of PMS is # playing (the '1'). # Hence must be us (since several users require plex.tv # token for PKC) pass - elif not (currSess['userId'] == state.PLEX_USER_ID - or - currSess['username'] == state.PLEX_USERNAME): + elif not (session['userId'] == state.PLEX_USER_ID or + session['username'] == state.PLEX_USERNAME): log.debug('Our username %s, userid %s did not match ' - 'the session username %s with userid %s' - % (state.PLEX_USERNAME, - state.PLEX_USER_ID, - currSess['username'], - currSess['userId'])) + 'the session username %s with userid %s', + state.PLEX_USERNAME, + state.PLEX_USER_ID, + session['username'], + session['userId']) continue - - # Get an up-to-date XML from the PMS - # because PMS will NOT directly tell us: - # duration of item - # viewCount - if currSess.get('duration') is None: - xml = GetPlexMetadata(ratingKey) + # Get an up-to-date XML from the PMS because PMS will NOT directly + # tell us: duration of item viewCount + if session.get('duration') is None: + xml = GetPlexMetadata(plex_id) if xml in (None, 401): - log.error('Could not get up-to-date xml for item %s' - % ratingKey) + log.error('Could not get up-to-date xml for item %s', + plex_id) continue - API = PlexAPI.API(xml[0]) - userdata = API.getUserData() - currSess['duration'] = userdata['Runtime'] - currSess['viewCount'] = userdata['PlayCount'] + api = PlexAPI.API(xml[0]) + userdata = api.getUserData() + session['duration'] = userdata['Runtime'] + session['viewCount'] = userdata['PlayCount'] # Sometimes, Plex tells us resume points in milliseconds and # not in seconds - thank you very much! - if item.get('viewOffset') > currSess['duration']: - resume = item.get('viewOffset') / 1000 + if item['viewOffset'] > session['duration']: + resume = item['viewOffset'] / 1000 else: - resume = item.get('viewOffset') - if resume >= v.MARK_PLAYED_AT and status not in ('stopped', 'ended'): - # We need to drop these as we'll otherwise NOT mark an item as - # completely watched after having seen >90% + resume = item['viewOffset'] + if resume < v.IGNORE_SECONDS_AT_START: continue - # Append to list that we need to process - items.append({ - 'ratingKey': ratingKey, - 'kodi_id': kodi_info[0], - 'file_id': kodi_info[1], - 'kodi_type': kodi_info[4], - 'viewOffset': resume, - 'state': status, - 'duration': currSess['duration'], - 'viewCount': currSess['viewCount'], - 'lastViewedAt': DateToKodi(getUnixTimestamp()) - }) - log.debug('Update playstate for user %s with id %s: %s' - % (state.PLEX_USERNAME, - state.PLEX_USER_ID, - items[-1])) - # Now tell Kodi where we are - for item in items: - itemFkt = getattr(itemtypes, - v.ITEMTYPE_FROM_KODITYPE[item['kodi_type']]) - with itemFkt() as Fkt: - Fkt.updatePlaystate(item) + try: + completed = float(resume) / float(session['duration']) + except (ZeroDivisionError, TypeError): + log.error('Could not mark playstate for %s and session %s', + data, session) + continue + if completed >= v.MARK_PLAYED_AT: + # Only mark completely watched ONCE + if session.get('marked_played') is None: + session['marked_played'] = True + mark_played = True + else: + # Don't mark it as completely watched again + continue + else: + mark_played = False + log.debug('Update playstate for user %s with id %s for plex id %s', + state.PLEX_USERNAME, state.PLEX_USER_ID, plex_id) + item_fkt = getattr(itemtypes, + v.ITEMTYPE_FROM_KODITYPE[session['kodi_type']]) + with item_fkt() as fkt: + fkt.updatePlaystate(mark_played, + session['viewCount'], + resume, + session['duration'], + session['file_id'], + DateToKodi(getUnixTimestamp())) def fanartSync(self, refresh=False): """ diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 96996c67..a83d9c40 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -21,7 +21,13 @@ def tryDecode(string, encoding='utf-8'): string = string.decode() return string + +# Percent of playback progress for watching item as partially watched. Anything +# more and item will NOT be marked as partially, but fully watched MARK_PLAYED_AT = 0.9 +# How many seconds of playback do we ignore before marking an item as partially +# watched? +IGNORE_SECONDS_AT_START = 60 _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' From 66cdd4b176e348d367c5cd49a61499ba76406659 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 18:20:33 +0100 Subject: [PATCH 263/509] Fix IntegrityError when adding missing album - Should fix #395 --- resources/lib/itemtypes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 63c83910..45f4b7da 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1360,7 +1360,8 @@ class Music(Items): artwork.addArtwork(artworks, artistid, v.KODI_TYPE_ARTIST, kodicursor) @CatchExceptions(warnuser=True) - def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None): + def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None, + scan_children=True): """ children: list of child xml's, so in this case songs """ @@ -1550,8 +1551,9 @@ class Music(Items): # Update artwork artwork.addArtwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) # Add all children - all tracks - for child in children: - self.add_updateSong(child, viewtag, viewid) + if scan_children: + for child in children: + self.add_updateSong(child, viewtag, viewid) @CatchExceptions(warnuser=True) def add_updateSong(self, item, viewtag=None, viewid=None): @@ -1716,7 +1718,9 @@ class Music(Items): if album is None or album == 401: log.error('Could not download album, abort') return - self.add_updateAlbum(album[0], children=[item]) + self.add_updateAlbum(album[0], + children=[item], + scan_children=False) plex_dbalbum = plex_db.getItem_byId(plex_albumId) try: albumid = plex_dbalbum[0] From b4f8b435fb0b588468eae67b51c8001ef5d1af90 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 19:40:49 +0100 Subject: [PATCH 264/509] Fix trailers for widget items with resume point --- resources/lib/playback.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 5cd34455..e10f578c 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -129,7 +129,8 @@ def playback_init(plex_id, plex_type, playqueue): dialog('notification', lang(29999), lang(30128), icon='{error}') return trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUMABLE and + api = API(xml[0]) + if (plex_type == v.PLEX_TYPE_MOVIE and not api.getResume() and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" From ed4ae181ecc92ce50e8932c861a07253299bc7de Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 20:23:35 +0100 Subject: [PATCH 265/509] Fix weird resume from playback behavior --- resources/lib/kodidb_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 653a019a..465c7174 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -983,7 +983,7 @@ class Kodidb_Functions(): ''' ) self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, - "DVDPlayer", 1)) + "VideoPlayer", 1)) def addTags(self, kodiid, tags, mediatype): # First, delete any existing tags associated to the id From 0111b66cd1a3382a17bfe070acb1a7fc25ca330d Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 20:44:41 +0100 Subject: [PATCH 266/509] Fix some addon paths --- resources/lib/entrypoint.py | 4 ++-- resources/lib/itemtypes.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 37c3bda2..e38a4a98 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -566,8 +566,8 @@ def getOnDeck(viewid, mediatype, tagname, limit): else: params = { 'mode': "play", - 'id': api.getRatingKey(), - 'dbid': listitem.getProperty('dbid') + 'plex_id': api.getRatingKey(), + 'plex_type': api.getType() } url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ % urlencode(params) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 45f4b7da..3576baa4 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -255,8 +255,9 @@ class Movies(Items): for extra in extras: # Only get 1st trailer element if extra['extraType'] == 1: - trailer = ("plugin://plugin.video.plexkodiconnect/trailer/?" - "id=%s&mode=play") % extra['key'] + trailer = ("plugin://plugin.video.plexkodiconnect?" + "plex_id=%s&plex_type=%s&mode=play" + % (extra['key'], v.PLEX_TYPE_CLIP)) break # GET THE FILE AND PATH ##### @@ -913,10 +914,9 @@ class TVShows(Items): filename = 'file_not_found.mkv' path = "plugin://plugin.video.plexkodiconnect/tvshows/%s/" % seriesId params = { - 'filename': tryEncode(filename), - 'id': itemid, - 'dbid': episodeid, - 'mode': "play" + 'plex_id': itemid, + 'plex_type': v.PLEX_TYPE_EPISODE, + 'mode': 'play' } filename = "%s?%s" % (path, tryDecode(urlencode(params))) playurl = filename From a4f4d0b7a7787e7d84b23b2fefea36932976a180 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 20:49:17 +0100 Subject: [PATCH 267/509] Remove obsolete code --- resources/lib/itemtypes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 3576baa4..d1a73f72 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -5,10 +5,9 @@ from logging import getLogger from urllib import urlencode from ntpath import dirname from datetime import datetime -from xbmc import sleep import artwork -from utils import tryEncode, tryDecode, window, kodiSQL, CatchExceptions +from utils import window, kodiSQL, CatchExceptions import plexdb_functions as plexdb import kodidb_functions as kodidb @@ -918,7 +917,7 @@ class TVShows(Items): 'plex_type': v.PLEX_TYPE_EPISODE, 'mode': 'play' } - filename = "%s?%s" % (path, tryDecode(urlencode(params))) + filename = "%s?%s" % (path, urlencode(params)) playurl = filename parentPathId = self.kodi_db.addPath( 'plugin://plugin.video.plexkodiconnect/tvshows/') From 764937e0b56f01b2d15676f11921f31d03a181ef Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 21:00:40 +0100 Subject: [PATCH 268/509] Fix Companion client resuming playback --- resources/lib/PlexCompanion.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 653c9ca3..f8437d5f 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -106,16 +106,12 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) - if container_key == playqueue.id: - LOG.info('Already know this playqueue - ignoring') - playqueue.transient_token = data.get('token') - else: - PQ.update_playqueue_from_PMS( - playqueue, - playqueue_id=container_key, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('token')) + PQ.update_playqueue_from_PMS( + playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('token')) @LOCKER.lockthis def _process_streams(self, data): From 01cbc0d6cbb3962c9276c9f619ec014060b17fe6 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Feb 2018 21:13:53 +0100 Subject: [PATCH 269/509] Version bump --- README.md | 2 +- addon.xml | 2 +- changelog.txt | 7 +++++++ service.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 22a955c3..eca71d11 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.8.18-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.0-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 16721c24..004e69fe 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 85ca2adb..81b6a12c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 2.0.0 (beta only): +- HUGE code overhaul - Remember that you can go back to earlier version ;-) +- Completely rewritten Plex Companion +- Completely rewritten playback startup +- Tons of fixes, see the Github changelog for more details +- WARNING: You will need to reset the Kodi database! + version 1.8.18: - Russian translation, thanks @UncleStark, @xom2000, @AlexFreit - Fix Plex context menu not showing up diff --git a/service.py b/service.py index fa695159..1ddd791c 100644 --- a/service.py +++ b/service.py @@ -138,7 +138,7 @@ class Service(): set_replace_paths() state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) - window('plex_minDBVersion', value="1.5.10") + window('plex_minDBVersion', value="2.0.0") set_webserver() self.monitor = Monitor() window('plex_kodiProfile', From a2c2649bc9cad2ea0b91d07d1374b2422c4abb27 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 07:43:22 +0100 Subject: [PATCH 270/509] Fix TypeError for trailers You need to resync the Kodi database --- resources/lib/PlexAPI.py | 29 +++++++++++------------------ resources/lib/itemtypes.py | 2 +- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index a10d59e0..01ca1714 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1729,7 +1729,7 @@ class API(): 5: Behind the scenes Output: list of dicts with one entry of the form: - 'key': e.g. /library/metadata/xxxx + 'ratingKey': e.g. '12345' 'title': 'thumb': artwork 'duration': @@ -1744,27 +1744,20 @@ class API(): for extra in extras: try: extraType = int(extra.attrib['extraType']) - except: + except (KeyError, TypeError): extraType = None if extraType != 1: continue - key = extra.attrib.get('key', None) - title = extra.attrib.get('title', None) - thumb = extra.attrib.get('thumb', None) duration = float(extra.attrib.get('duration', 0.0)) - year = extra.attrib.get('year', None) - originallyAvailableAt = extra.attrib.get( - 'originallyAvailableAt', None) - elements.append( - { - 'key': key, - 'title': title, - 'thumb': thumb, - 'duration': int(duration * v.PLEX_TO_KODI_TIMEFACTOR), - 'extraType': extraType, - 'originallyAvailableAt': originallyAvailableAt, - 'year': year - }) + elements.append({ + 'ratingKey': extra.attrib.get('ratingKey'), + 'title': extra.attrib.get('title'), + 'thumb': extra.attrib.get('thumb'), + 'duration': int(duration * v.PLEX_TO_KODI_TIMEFACTOR), + 'extraType': extraType, + 'originallyAvailableAt': extra.attrib.get('originallyAvailableAt'), + 'year': extra.attrib.get('year') + }) break return elements diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index d1a73f72..3ca61e2a 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -256,7 +256,7 @@ class Movies(Items): if extra['extraType'] == 1: trailer = ("plugin://plugin.video.plexkodiconnect?" "plex_id=%s&plex_type=%s&mode=play" - % (extra['key'], v.PLEX_TYPE_CLIP)) + % (extra['ratingKey'], v.PLEX_TYPE_CLIP)) break # GET THE FILE AND PATH ##### From d8de492d97eeab33035c4c4c0c05b9abd02cc853 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 20:12:44 +0100 Subject: [PATCH 271/509] Fix trailers not playing --- resources/lib/playback.py | 27 ++++++++++++++------------- resources/lib/playlist_func.py | 5 ++++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index e10f578c..1dfcafc2 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -139,20 +139,21 @@ def playback_init(plex_id, plex_type, playqueue): else: trailers = True LOG.info('Playing trailers: %s', trailers) - # Post to the PMS to create a playqueue - in any case due to Plex Companion - xml = init_plex_playqueue(plex_id, - xml.attrib.get('librarySectionUUID'), - mediatype=plex_type, - trailers=trailers) - if xml is None: - LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', - plex_id, xml.attrib.get('librarySectionUUID')) - # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') - return - # Should already be empty, but just in case playqueue.clear() - PL.get_playlist_details_from_xml(playqueue, xml) + if plex_type != v.PLEX_TYPE_CLIP: + # Post to the PMS to create a playqueue - in any case due to Companion + xml = init_plex_playqueue(plex_id, + xml.attrib.get('librarySectionUUID'), + mediatype=plex_type, + trailers=trailers) + if xml is None: + LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', + plex_id, xml.attrib.get('librarySectionUUID')) + # "Play error" + dialog('notification', lang(29999), lang(30128), icon='{error}') + return + # Should already be empty, but just in case + PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) # Sleep a bit to let setResolvedUrl do its thing - bit ugly sleep(200) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 4c42fa4a..0bb0c984 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -284,7 +284,10 @@ def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, api = API(xml_video_element) item.plex_id = api.getRatingKey() item.plex_type = api.getType() - item.id = xml_video_element.attrib['%sItemID' % playlist.kind] + try: + item.id = xml_video_element.attrib['%sItemID' % playlist.kind] + except KeyError: + pass item.guid = xml_video_element.attrib.get('guid') if item.guid is not None: item.guid = escape_html(item.guid) From 0d5d35c26389469333aa4bb1a9c233d7db07bc47 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 20:16:02 +0100 Subject: [PATCH 272/509] Less logging --- resources/lib/playback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1dfcafc2..d20016bb 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -138,7 +138,7 @@ def playback_init(plex_id, plex_type, playqueue): trailers = True if trailers else False else: trailers = True - LOG.info('Playing trailers: %s', trailers) + LOG.debug('Playing trailers: %s', trailers) playqueue.clear() if plex_type != v.PLEX_TYPE_CLIP: # Post to the PMS to create a playqueue - in any case due to Companion From a9fb2e127e7dcbf45bf407a82ba376dc099841f0 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 20:26:10 +0100 Subject: [PATCH 273/509] Fix TypeError when navigating - Fixes #398 --- resources/lib/json_rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 9e1df360..82a96b40 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -363,7 +363,7 @@ def get_tv_shows(params): """ ret = JsonRPC('VideoLibrary.GetTVShows').execute(params) try: - ret['result']['tvshows'] + ret = ret['result']['tvshows'] except (KeyError, TypeError): ret = [] return ret @@ -375,7 +375,7 @@ def get_episodes(params): """ ret = JsonRPC('VideoLibrary.GetEpisodes').execute(params) try: - ret['result']['episodes'] + ret = ret['result']['episodes'] except (KeyError, TypeError): ret = [] return ret From c6ba6b42a8dfcc6231ef884bab10e7ecbac32d9b Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 21:00:32 +0100 Subject: [PATCH 274/509] Fix empty On Deck for tv shows - Fixes #398 --- resources/lib/entrypoint.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index e38a4a98..fe669e58 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -593,6 +593,12 @@ def getOnDeck(viewid, mediatype, tagname, limit): {'operator': "is", 'field': "tag", 'value': "%s" % tagname} ]} } + items = js.get_tv_shows(params) + if not items: + # Now items retrieved - empty directory + xbmcplugin.endOfDirectory(handle=HANDLE) + return + params = { 'sort': {'method': "episode"}, 'limits': {"end": 1}, @@ -617,6 +623,7 @@ def getOnDeck(viewid, mediatype, tagname, limit): {'operator': "true", 'field': "inprogress", 'value': ""} ] } + # Are there any episodes still in progress/not yet finished watching?!? # Then we should show this episode, NOT the "next up" inprog_params = { @@ -626,23 +633,25 @@ def getOnDeck(viewid, mediatype, tagname, limit): } count = 0 - for item in js.get_tv_shows(params): + for item in items: inprog_params['tvshowid'] = item['tvshowid'] episodes = js.get_episodes(inprog_params) if not episodes: # No, there are no episodes not yet finished. Get "next up" params['tvshowid'] = item['tvshowid'] episodes = js.get_episodes(params) + if not episodes: + # Also no episodes currently coming up + continue for episode in episodes: # There will always be only 1 episode ('limit=1') listitem = createListItem(episode, appendShowTitle=appendShowTitle, appendSxxExx=appendSxxExx) - xbmcplugin.addDirectoryItem( - handle=HANDLE, - url=episode['file'], - listitem=listitem, - isFolder=False) + xbmcplugin.addDirectoryItem(handle=HANDLE, + url=episode['file'], + listitem=listitem, + isFolder=False) count += 1 if count >= limit: break From a1672b62db11aa51e0d1199359f1f743891101b4 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Feb 2018 21:02:59 +0100 Subject: [PATCH 275/509] Version bump --- README.md | 2 +- addon.xml | 15 +++++++++++++-- changelog.txt | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eca71d11..82af42d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.0-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.1-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 004e69fe..d538a6fb 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,18 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 1.8.18: + version 2.0.1 (beta only): +- Fix empty On Deck for tv shows +- Fix trailers not playing + +version 2.0.0 (beta only): +- HUGE code overhaul - Remember that you can go back to earlier version ;-) +- Completely rewritten Plex Companion +- Completely rewritten playback startup +- Tons of fixes, see the Github changelog for more details +- WARNING: You will need to reset the Kodi database! + +version 1.8.18: - Russian translation, thanks @UncleStark, @xom2000, @AlexFreit - Fix Plex context menu not showing up - Deal better with missing stream info (e.g. channels) diff --git a/changelog.txt b/changelog.txt index 81b6a12c..1c006c67 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +version 2.0.1 (beta only): +- Fix empty On Deck for tv shows +- Fix trailers not playing + version 2.0.0 (beta only): - HUGE code overhaul - Remember that you can go back to earlier version ;-) - Completely rewritten Plex Companion From 39b729a8046d6165c41081b2a3fd91fa88e5490a Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:10:26 +0100 Subject: [PATCH 276/509] Add ETH donations --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82af42d7..cd7ba130 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,18 @@ PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.or * 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-Explained) ### 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. +I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal, Bitcoin or Ether 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. [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) + + +**Ethereum:** +[![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] +ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F + + ### Request a New Feature [![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect) From 6a9ef8cbaa05f129ca1e4a1db3143b5529eef522 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:12:51 +0100 Subject: [PATCH 277/509] Prettify --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index cd7ba130..2868d5f5 100644 --- a/README.md +++ b/README.md @@ -102,11 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) - - -**Ethereum:** -[![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] -ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F +| [![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] | ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F | ### Request a New Feature From 90dc7e52724e0dca1b533981e4df7d647d9d93f6 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:15:07 +0100 Subject: [PATCH 278/509] Prettify --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2868d5f5..56f029b5 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,9 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -| [![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] | ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F | + +**Ether**: +[Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F ### Request a New Feature From 2544f18c24d79a80b1c9574996fb259397906ce4 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:15:41 +0100 Subject: [PATCH 279/509] Revert "Prettify" This reverts commit 90dc7e52724e0dca1b533981e4df7d647d9d93f6. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 56f029b5..2868d5f5 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) - -**Ether**: -[Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F +| [![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] | ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F | ### Request a New Feature From 88a3a79ef6f03eae969e25f144717e3df58eae2f Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:16:17 +0100 Subject: [PATCH 280/509] Prettify --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2868d5f5..d8676044 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,9 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -| [![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] | ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F | +**Ether** +[![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] +ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F ### Request a New Feature From e7e6b67a6c5cb0e0618636fac76cd446d5b26638 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:18:21 +0100 Subject: [PATCH 281/509] Prettify --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d8676044..844f943a 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,8 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -**Ether** -[![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)] -ETH address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F +![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +**Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** ### Request a New Feature From 22c387093de9e35021971b61fd530db7e2747f61 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:19:57 +0100 Subject: [PATCH 282/509] Prettify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 844f943a..d11cff2a 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +![Ether-Donations](https://chart.googleapis.com/chart?cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) **Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** From 16f26b3f2ef90e2cac312b9f138b23dd8c964f2b Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:20:46 +0100 Subject: [PATCH 283/509] Revert "Prettify" This reverts commit 22c387093de9e35021971b61fd530db7e2747f61. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d11cff2a..844f943a 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -![Ether-Donations](https://chart.googleapis.com/chart?cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) **Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** From 724b17d25c5dca5535e93191368bf0b1f04d8b6f Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:24:13 +0100 Subject: [PATCH 284/509] Prettify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 844f943a..979c0a91 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -![Ether-Donations](https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +![Ether-Donations](https://chart.googleapis.com/chart?chs=50x50&cht=qr&chld=L1&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) **Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** From ee130a89a6bee0d44054b6966b36f945833839c3 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 11:24:46 +0100 Subject: [PATCH 285/509] Prettify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 979c0a91..2145a28f 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -![Ether-Donations](https://chart.googleapis.com/chart?chs=50x50&cht=qr&chld=L1&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +![Ether-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) **Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** From dd3f7f49157b672343bd1393e63b899597ac21c6 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:37:05 +0100 Subject: [PATCH 286/509] Add BTX --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2145a28f..11c351b2 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,13 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) -![Ether-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) -**Ethereum (ETH) address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** +![ETH-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) +**Ethereum (ETH) address: +0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** + +![BTX-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT) +**Bitcoin (BTX) address: +3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT** ### Request a New Feature From acef9017c8185b9ec6717417cc29b498e3425bb9 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:38:31 +0100 Subject: [PATCH 287/509] Clarification --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11c351b2..924d1e4c 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.or ### Donations I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal, Bitcoin or Ether 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. +**Full disclaimer:** I will see your name and address if you use PayPal. Rest assured that I will not share this with anyone. [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) From f279efb25587e562a4143a4ab91ef9e7bee2bd9e Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:48:50 +0100 Subject: [PATCH 288/509] Less logging --- resources/lib/plexbmchelper/subscribers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 66ecd99e..95041870 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -312,8 +312,6 @@ class SubscriptionMgr(object): except IndexError: # E.g. for direct path playback for single item return False - LOG.debug('item: %s', item) - LOG.debug('playstate: %s', info) if item.plex_id != info['plex_id']: # Kodi playqueue already progressed; need to wait until # everything is loaded From 9a96f70f63db0ada572bb29e9e41bebdebff9e24 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:50:50 +0100 Subject: [PATCH 289/509] Clarify logging --- service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.py b/service.py index 1ddd791c..a0e7fbe3 100644 --- a/service.py +++ b/service.py @@ -97,7 +97,7 @@ class Service(): LOG.info("Platform: %s", v.PLATFORM) LOG.info("KODI Version: %s", v.KODILONGVERSION) LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) - LOG.info("Using plugin paths: %s", + LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') != "true") LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) LOG.info("Full sys.argv received: %s", argv) From 4cc8ffdd39a67cfc345c90861a4528b45152d20e Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:51:20 +0100 Subject: [PATCH 290/509] Revert "Clarify logging" This reverts commit 9a96f70f63db0ada572bb29e9e41bebdebff9e24. --- service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.py b/service.py index a0e7fbe3..1ddd791c 100644 --- a/service.py +++ b/service.py @@ -97,7 +97,7 @@ class Service(): LOG.info("Platform: %s", v.PLATFORM) LOG.info("KODI Version: %s", v.KODILONGVERSION) LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) - LOG.info("PKC Direct Paths: %s", + LOG.info("Using plugin paths: %s", settings('useDirectPaths') != "true") LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) LOG.info("Full sys.argv received: %s", argv) From 5250e56f7ca1ad2c63afcb85bc9f58e00dae8f95 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 12:51:41 +0100 Subject: [PATCH 291/509] Clarify logging --- service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service.py b/service.py index 1ddd791c..033e007a 100644 --- a/service.py +++ b/service.py @@ -97,8 +97,7 @@ class Service(): LOG.info("Platform: %s", v.PLATFORM) LOG.info("KODI Version: %s", v.KODILONGVERSION) LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) - LOG.info("Using plugin paths: %s", - settings('useDirectPaths') != "true") + LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == "true") LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) LOG.info("Full sys.argv received: %s", argv) From 447d233df1114ae15ea6ebd38ac31e4277ce221d Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 13:09:50 +0100 Subject: [PATCH 292/509] Better 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 0bb0c984..2571974b 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -367,7 +367,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): Returns the first PKC playlist item or raises PlaylistError """ - LOG.debug('Initializing the playlist %s on the Plex side', playlist) + LOG.debug('Initializing the playlist on the Plex side: %s', playlist) playlist.clear(kodi=False) try: if plex_id: From bdad905df3a0f7985b1e81c6f6add2c3f8daf200 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 13:32:10 +0100 Subject: [PATCH 293/509] Fix playback reporting not starting up correctly - Should fix #400 --- resources/lib/kodimonitor.py | 23 +++++++++++------------ resources/lib/playback.py | 3 --- resources/lib/playlist_func.py | 2 -- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 6f05d127..232c1ab9 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -255,8 +255,8 @@ class KodiMonitor(Monitor): kodi_type = json_item.get('type') path = json_item.get('file') if not path and not kodi_id: - LOG.info('Aborting playback report - no Kodi id or file for %s', - json_item) + LOG.debug('Aborting playback report - no Kodi id or file for %s', + json_item) raise RuntimeError # Plex id will NOT be set with direct paths plex_id = state.PLEX_IDS.get(path) @@ -341,17 +341,7 @@ class KodiMonitor(Monitor): status = state.PLAYER_STATES[playerid] try: item = playqueue.items[pos] - # See if playback.py already initiated playback - init_done = item.init_done except IndexError: - init_done = False - if init_done is True: - kodi_id = item.kodi_id - kodi_type = item.kodi_type - plex_id = item.plex_id - plex_type = item.plex_type - container_key = '/playQueues/%s' % playqueue.id - else: try: kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) except RuntimeError: @@ -379,6 +369,15 @@ class KodiMonitor(Monitor): elif plex_id is not None: container_key = '/library/metadata/%s' % plex_id LOG.debug('Set the Plex container_key to: %s', container_key) + else: + kodi_id = item.kodi_id + kodi_type = item.kodi_type + plex_id = item.plex_id + plex_type = item.plex_type + if playqueue.id: + container_key = '/playQueues/%s' % playqueue.id + else: + container_key = '/library/metadata/%s' % plex_id status.update(info) status['container_key'] = container_key status['file'] = path diff --git a/resources/lib/playback.py b/resources/lib/playback.py index d20016bb..da9d8b9a 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -96,7 +96,6 @@ def play_resume(playqueue, xml, stack): item.playcount = stack_item['playcount'] item.offset = stack_item['offset'] item.part = stack_item['part'] - item.init_done = True api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() @@ -249,7 +248,6 @@ def _process_stack(playqueue, stack): playlist_item.part = item['part'] playlist_item.id = item['id'] playlist_item.force_transcode = state.FORCE_TRANSCODE - playlist_item.init_done = True pos += 1 @@ -339,7 +337,6 @@ def process_indirect(key, offset, resolve=True): item.offset = int(offset) item.plex_type = v.PLEX_TYPE_CLIP item.playmethod = 'DirectStream' - item.init_done = True # Need to get yet another xml to get the final playback url xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % xml[0][0][0].attrib['key']) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 2571974b..99d39557 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -137,7 +137,6 @@ class Playlist_Item(object): offset = None [int] the item's view offset UPON START in Plex time part = 0 [int] part number if Plex video consists of mult. parts force_transcode [bool] defaults to False - init_done = False Set to True only if run through playback init """ def __init__(self): self.id = None @@ -156,7 +155,6 @@ class Playlist_Item(object): # If Plex video consists of several parts; part number self.part = 0 self.force_transcode = False - self.init_done = False def __repr__(self): """ From 5ca0f7d6afbeedd130c64050d417572e923cb109 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 13:57:07 +0100 Subject: [PATCH 294/509] Fix playback cleanup if PKC causes stop --- resources/lib/playback.py | 1 + resources/lib/player.py | 6 +++++- resources/lib/state.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index da9d8b9a..68f223dd 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -68,6 +68,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): except IndexError: # Release our default.py before starting our own Kodi player instance if resolve is True: + state.PKC_CAUSED_STOP = True result = Playback_Successful() result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) diff --git a/resources/lib/player.py b/resources/lib/player.py index 29d4b660..64710f66 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -82,7 +82,11 @@ class PKC_Player(Player): Will be called when playback is stopped by the user """ LOG.debug("ONPLAYBACK_STOPPED") - playback_cleanup() + if state.PKC_CAUSED_STOP is True: + state.PKC_CAUSED_STOP = False + LOG.debug('PKC caused this playback stop - ignoring') + else: + playback_cleanup() def onPlayBackEnded(self): """ diff --git a/resources/lib/state.py b/resources/lib/state.py index 9e371232..0f7b20f4 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -88,6 +88,8 @@ WEBSOCKET_QUEUE = None # Which Kodi player is/has been active? (either int 1, 2 or 3) ACTIVE_PLAYERS = [] +# Failsafe for throwing failing ListItems() back to Kodi's setResolvedUrl +PKC_CAUSED_STOP = False # Kodi player states - here, initial values are set PLAYER_STATES = { From e744ff2b97000f9f00d09064e38326290b3092a1 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 14:09:16 +0100 Subject: [PATCH 295/509] Always detect if user resumes playback --- resources/lib/kodimonitor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 232c1ab9..61c74167 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -398,13 +398,10 @@ class SpecialMonitor(Thread): """ def run(self): LOG.info("----====# Starting Special Monitor #====----") - player = Player() # "Start from beginning", "Play from beginning" strings = (getLocalizedString(12021), getLocalizedString(12023)) while not self.thread_stopped(): - is_playing = player.isPlaying() - if (not is_playing and - getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and + if (getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and getInfoLabel('Control.GetLabel(1002)') in strings): # Remember that the item IS indeed resumable state.RESUMABLE = True @@ -417,5 +414,5 @@ class SpecialMonitor(Thread): else: # User chose something else from the context menu state.RESUME_PLAYBACK = False - sleep(100) + sleep(200) LOG.info("#====---- Special Monitor Stopped ----====#") From 5f3aa91a54b0f7e7c169edbd9134abff5346cedd Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 14:28:54 +0100 Subject: [PATCH 296/509] 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 99d39557..a233c6a0 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -315,7 +315,7 @@ def _get_playListVersion_from_xml(playlist, xml): playlist.version = int(xml.attrib['%sVersion' % playlist.kind]) except (TypeError, AttributeError, KeyError): raise PlaylistError('Could not get new playlist Version for playlist ' - '%s', playlist) + '%s' % playlist) def get_playlist_details_from_xml(playlist, xml): From 0731ae0179eff2c09a3e440d19afdf96a59cfd82 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 14:32:58 +0100 Subject: [PATCH 297/509] Enable resume within a playqueue --- resources/lib/kodidb_functions.py | 18 ++++++++++++++++++ resources/lib/playback.py | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 465c7174..592e02ae 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -952,6 +952,24 @@ class Kodidb_Functions(): return None return int(runtime) + def get_resume(self, file_id): + """ + Returns the first resume point in seconds (int) if found, else None for + the Kodi file_id provided + """ + query = ''' + SELECT timeInSeconds + FROM bookmark + WHERE idFile = ? + ''' + self.cursor.execute(query, (file_id,)) + resume = self.cursor.fetchone() + try: + resume = resume[0] + except TypeError: + resume = None + return resume + def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): # Delete existing resume point query = ' '.join(( diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 68f223dd..bc6b66d5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -11,6 +11,7 @@ from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue from downloadutils import DownloadUtils as DU import plexdb_functions as plexdb +import kodidb_functions as kodidb import playlist_func as PL import playqueue as PQ from playutils import PlayUtils @@ -287,6 +288,13 @@ def conclude_playback(playqueue, pos): playutils.audio_subtitle_prefs(listitem) if state.RESUME_PLAYBACK is True: state.RESUME_PLAYBACK = False + if (item.offset is None and + item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)): + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(item.plex_id) + file_id = plex_dbitem[1] if plex_dbitem else None + with kodidb.GetKodiDB('video') as kodi_db: + item.offset = kodi_db.get_resume(file_id) LOG.info('Resuming playback at %s', item.offset) listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('resumetime', str(item.offset)) From e393547e137cf68c79daa7da679590fc79072a6f Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 14:57:36 +0100 Subject: [PATCH 298/509] Compare playqueue items more reliably --- resources/lib/playqueue.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index cf5a85e5..fffd311a 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -3,6 +3,7 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly """ from logging import getLogger from threading import Thread +from re import compile as re_compile from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep @@ -19,6 +20,7 @@ import variables as v LOG = getLogger("PLEX." + __name__) PLUGIN = 'plugin://%s' % v.ADDON_ID +REGEX = re_compile(r'''plex_id=(\d+)''') # Our PKC playqueues (3 instances of Playqueue_Object()) PLAYQUEUES = [] @@ -159,11 +161,17 @@ class PlayqueueMonitor(Thread): except AttributeError: # were not passed a filename; ignore pass - if new_item.get('id') is None: - identical = old_item.file == new_item['file'] - else: + if 'id' in new_item: identical = (old_item.kodi_id == new_item['id'] and old_item.kodi_type == new_item['type']) + else: + try: + plex_id = REGEX.findall(new_item['file'])[0] + except IndexError: + LOG.debug('Comparing paths directly as a fallback') + identical = old_item.file == new_item['file'] + else: + identical = plex_id == old_item.plex_id if j == 0 and identical: del old[j], index[j] break From c86ba92837d0227fa306216b1146d67c6c7e4ba3 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Feb 2018 15:00:11 +0100 Subject: [PATCH 299/509] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 924d1e4c..ab3012c8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.1-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.2-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index d538a6fb..79f9f9a6 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,14 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.1 (beta only): + version 2.0.2 (beta only): +- Fix playback reporting not starting up correctly +- Fix playback cleanup if PKC causes stop +- Always detect if user resumes playback +- Enable resume within a playqueue +- Compare playqueue items more reliably + +version 2.0.1 (beta only): - Fix empty On Deck for tv shows - Fix trailers not playing diff --git a/changelog.txt b/changelog.txt index 1c006c67..721c483b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 2.0.2 (beta only): +- Fix playback reporting not starting up correctly +- Fix playback cleanup if PKC causes stop +- Always detect if user resumes playback +- Enable resume within a playqueue +- Compare playqueue items more reliably + version 2.0.1 (beta only): - Fix empty On Deck for tv shows - Fix trailers not playing From 15e97a63c279b3cf4fa9ec216c8054b9ae6efa16 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 8 Feb 2018 10:35:10 +0100 Subject: [PATCH 300/509] Remove obsolete code --- resources/lib/playback.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index bc6b66d5..1bb3c492 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -79,43 +79,6 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): conclude_playback(playqueue, pos) -def play_resume(playqueue, xml, stack): - """ - If there exists a resume point, Kodi will ask the user whether to continue - playback. We thus need to use setResolvedUrl "correctly". Mind that there - might be several parts! - """ - result = Playback_Successful() - listitem = PKC_ListItem() - # Only get the very first item of our playqueue (i.e. the very first part) - stack_item = stack.pop(0) - api = API(xml[0]) - item = PL.playlist_item_from_xml(playqueue, - xml[0], - kodi_id=stack_item['kodi_id'], - kodi_type=stack_item['kodi_type']) - api.setPartNumber(item.part) - item.playcount = stack_item['playcount'] - item.offset = stack_item['offset'] - item.part = stack_item['part'] - api.CreateListItemFromPlexItem(listitem) - playutils = PlayUtils(api, item) - playurl = playutils.getPlayUrl() - listitem.setPath(tryEncode(playurl)) - if item.playmethod in ('DirectStream', 'DirectPlay'): - listitem.setSubtitles(api.externalSubs()) - else: - playutils.audio_subtitle_prefs(listitem) - result.listitem = listitem - # Add to our playlist - playqueue.items.append(item) - # This will release default.py with setResolvedUrl - pickle_me(result) - # Add remaining parts to the playlist, if any - if stack: - _process_stack(playqueue, stack) - - def playback_init(plex_id, plex_type, playqueue): """ Playback setup if Kodi starts playing an item for the first time. From e3882acf5064481cbdc4917838f7cb8eaa3459c2 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 8 Feb 2018 10:43:38 +0100 Subject: [PATCH 301/509] Remove obsolete code --- resources/lib/PlexFunctions.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index ce8d9624..71f1e32f 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -76,32 +76,6 @@ def GetMethodFromPlexType(plexType): return methods[plexType] -def XbmcItemtypes(): - return ['photo', 'video', 'audio'] - - -def PlexItemtypes(): - return ['photo', 'video', 'audio'] - - -def PlexLibraryItemtypes(): - return ['movie', 'show'] - # later add: 'artist', 'photo' - - -def EmbyItemtypes(): - return ['Movie', 'Series', 'Season', 'Episode'] - - -def SelectStreams(url, args): - """ - Does a PUT request to tell the PMS what audio and subtitle streams we have - chosen. - """ - downloadutils.DownloadUtils().downloadUrl( - url + '?' + urlencode(args), action_type='PUT') - - def GetPlexMetadata(key): """ Returns raw API metadata for key as an etree XML. From bbb35856e0995438a26262a10d209040a1a90e24 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 8 Feb 2018 11:16:39 +0100 Subject: [PATCH 302/509] Fix Alexa playback --- resources/lib/PlexCompanion.py | 37 +++++++++++++++++++++++----------- resources/lib/playback.py | 23 +++++++++++++++------ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index f8437d5f..c79e99c3 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -9,12 +9,14 @@ from urllib import urlencode from xbmc import sleep, executebuiltin -from utils import settings, thread_methods +from utils import settings, thread_methods, language as lang, dialog from plexbmchelper import listener, plexgdm, subscribers, httppersist from plexbmchelper.subscribers import LOCKER -from PlexFunctions import ParseContainerKey, GetPlexMetadata +from PlexFunctions import ParseContainerKey, GetPlexMetadata, DownloadChunks from PlexAPI import API -from playlist_func import get_pms_playqueue, get_plextype_from_xml +from playlist_func import get_pms_playqueue, get_plextype_from_xml, \ + get_playlist_details_from_xml +from playback import playback_triage, play_xml import json_rpc as js import player import variables as v @@ -60,17 +62,28 @@ class PlexCompanion(Thread): PQ.init_playqueue_from_plex_children( api.getRatingKey(), transient_token=data.get('token')) + elif data['containerKey'].startswith('/playQueues/'): + _, container_key, _ = ParseContainerKey(data['containerKey']) + xml = DownloadChunks('{server}/playQueues/%s?' % container_key) + if xml is None: + # "Play error" + dialog('notification', lang(29999), lang(30128), icon='{error}') + return + playqueue = PQ.get_playqueue_from_type( + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + playqueue.clear() + get_playlist_details_from_xml(playqueue, xml) + if data.get('offset') != '0': + offset = float(data['offset']) / 1000.0 + else: + offset = None + play_xml(playqueue, xml, offset) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') - params = { - 'mode': 'plex_node', - 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), - 'play_directly': 'true', - 'node': 'false' - } - executebuiltin('RunPlugin(plugin://%s?%s)' - % (v.ADDON_ID, urlencode(params))) + if data.get('offset') != '0': + state.RESUMABLE = True + state.RESUME_PLAYBACK = True + playback_triage(api.getRatingKey(), api.getType(), resolve=False) @staticmethod def _process_node(data): diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1bb3c492..ed8747de 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -336,19 +336,30 @@ def process_indirect(key, offset, resolve=True): thread.start() -def play_xml(playqueue, xml, offset=None): +def play_xml(playqueue, xml, offset=None, start_plex_id=None): """ Play all items contained in the xml passed in. Called by Plex Companion. + + Either supply the ratingKey of the starting Plex element. Or set + playqueue.selectedItemID """ - LOG.info("play_xml called") + LOG.info("play_xml called with offset %s, start_plex_id %s", + offset, start_plex_id) stack = _prep_playlist_stack(xml) _process_stack(playqueue, stack) LOG.debug('Playqueue after play_xml update: %s', playqueue) - for startpos, item in enumerate(playqueue.items): - if item.id == playqueue.selectedItemID: - break + if start_plex_id is not None: + for startpos, item in enumerate(playqueue.items): + if item.plex_id == start_plex_id: + break + else: + startpos = 0 else: - startpos = 0 + for startpos, item in enumerate(playqueue.items): + if item.id == playqueue.selectedItemID: + break + else: + startpos = 0 thread = Thread(target=threaded_playback, args=(playqueue.kodi_pl, startpos, offset)) LOG.info('Done play_xml, starting Kodi player at position %s', startpos) From 8c10c66bdcf9c0e1a083b5c33f80e756330024c8 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 8 Feb 2018 11:22:26 +0100 Subject: [PATCH 303/509] Mind Alexa transient token --- resources/lib/PlexCompanion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index c79e99c3..ffd31e07 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -73,6 +73,7 @@ class PlexCompanion(Thread): v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) playqueue.clear() get_playlist_details_from_xml(playqueue, xml) + playqueue.plex_transient_token = data.get('token') if data.get('offset') != '0': offset = float(data['offset']) / 1000.0 else: From 31d42d0b04736c793674f068294436d76bac33bb Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 8 Feb 2018 16:02:29 +0100 Subject: [PATCH 304/509] Change bookmarks from Videoplayer to DVDPlayer --- resources/lib/kodidb_functions.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 592e02ae..4b8b1d87 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -992,16 +992,15 @@ class Kodidb_Functions(): if resume_seconds: self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") bookmarkId = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO bookmark( - idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, - "VideoPlayer", 1)) + query = ''' + INSERT INTO bookmark( + idBookmark, idFile, timeInSeconds, totalTimeInSeconds, + thumbNailImage, player, playerState, type) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, + total_seconds, None, "DVDPlayer", + None, 1)) def addTags(self, kodiid, tags, mediatype): # First, delete any existing tags associated to the id From 96be262f7897b8d2cbb6c79ee20b4f91d500961d Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 9 Feb 2018 13:57:58 +0100 Subject: [PATCH 305/509] Move minimal Kodi DB version to variables.py --- resources/lib/librarysync.py | 6 ++---- resources/lib/variables.py | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 7ccec2f3..55983170 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1527,11 +1527,9 @@ class LibrarySync(Thread): self.force_dialog = False # Verify the validity of the database currentVersion = settings('dbCreatedWithVersion') - minVersion = window('plex_minDBVersion') - - if not compare_version(currentVersion, minVersion): + if not compare_version(currentVersion, v.MIN_DB_VERSION): log.warn("Db version out of date: %s minimum version " - "required: %s" % (currentVersion, minVersion)) + "required: %s", (currentVersion, v.MIN_DB_VERSION)) # DB out of date. Proceed to recreate? resp = dialog('yesno', heading=lang(29999), diff --git a/resources/lib/variables.py b/resources/lib/variables.py index a83d9c40..4a7f6d33 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -74,6 +74,9 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort')) # Unique ID for this Plex client; also see clientinfo.py PKC_MACHINE_IDENTIFIER = None +# Minimal PKC version needed for the Kodi database - otherwise need to recreate +MIN_DB_VERSION = '2.0.0' + # Database paths _DB_VIDEO_VERSION = { 13: 78, # Gotham From 90a0c4b545859600109b02f33f6cc72964203899 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 9 Feb 2018 15:10:20 +0100 Subject: [PATCH 306/509] Replace websocket shutdown with close --- resources/lib/websocket_client.py | 133 ++++++++++++++---------------- 1 file changed, 60 insertions(+), 73 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 575db958..16e14a0d 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -17,14 +17,17 @@ import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) - ws = None + + def __init__(self): + self.ws = None + super(WebSocket, self).__init__() def process(self, opcode, message): raise NotImplementedError @@ -51,7 +54,7 @@ class WebSocket(Thread): raise NotImplementedError def run(self): - log.info("----===## Starting %s ##===----" % self.__class__.__name__) + LOG.info("----===## Starting %s ##===----", self.__class__.__name__) counter = 0 handshake_counter = 0 @@ -62,15 +65,12 @@ class WebSocket(Thread): while thread_suspended(): # Set in service.py if self.ws is not None: - try: - self.ws.shutdown() - except: - pass + self.ws.close() self.ws = None if thread_stopped(): # Abort was requested while waiting. We should exit - log.info("##===---- %s Stopped ----===##" - % self.__class__.__name__) + LOG.info("##===---- %s Stopped ----===##", + self.__class__.__name__) return sleep(1000) try: @@ -79,8 +79,8 @@ class WebSocket(Thread): # No worries if read timed out pass except websocket.WebSocketConnectionClosedException: - log.info("%s: connection closed, (re)connecting" - % self.__class__.__name__) + LOG.info("%s: connection closed, (re)connecting", + self.__class__.__name__) uri, sslopt = self.getUri() try: # Low timeout - let's us shut this thread down! @@ -91,7 +91,7 @@ class WebSocket(Thread): enable_multithread=True) except IOError: # Server is probably offline - log.info("%s: Error connecting" % self.__class__.__name__) + LOG.info("%s: Error connecting", self.__class__.__name__) self.ws = None counter += 1 if counter > 3: @@ -99,59 +99,46 @@ class WebSocket(Thread): self.IOError_response() sleep(1000) except websocket.WebSocketTimeoutException: - log.info("%s: Timeout while connecting, trying again" - % self.__class__.__name__) + LOG.info("%s: Timeout while connecting, trying again", + self.__class__.__name__) self.ws = None sleep(1000) except websocket.WebSocketException as e: - log.info('%s: WebSocketException: %s' - % (self.__class__.__name__, e)) + LOG.info('%s: WebSocketException: %s', + self.__class__.__name__, e) if ('Handshake Status 401' in e.args or 'Handshake Status 403' in e.args): handshake_counter += 1 if handshake_counter >= 5: - log.info('%s: Error in handshake detected. ' - 'Stopping now' - % self.__class__.__name__) + LOG.info('%s: Error in handshake detected. ' + 'Stopping now', self.__class__.__name__) break self.ws = None sleep(1000) except Exception as e: - log.error('%s: Unknown exception encountered when ' - 'connecting: %s' % (self.__class__.__name__, e)) + LOG.error('%s: Unknown exception encountered when ' + 'connecting: %s', self.__class__.__name__, e) import traceback - log.error("%s: Traceback:\n%s" - % (self.__class__.__name__, - traceback.format_exc())) + LOG.error("%s: Traceback:\n%s", + self.__class__.__name__, traceback.format_exc()) self.ws = None sleep(1000) else: counter = 0 handshake_counter = 0 except Exception as e: - log.error("%s: Unknown exception encountered: %s" - % (self.__class__.__name__, e)) + LOG.error("%s: Unknown exception encountered: %s", + self.__class__.__name__, e) import traceback - log.error("%s: Traceback:\n%s" - % (self.__class__.__name__, - traceback.format_exc())) - try: - self.ws.shutdown() - except: - pass + LOG.error("%s: Traceback:\n%s", + self.__class__.__name__, traceback.format_exc()) + if self.ws is not None: + self.ws.close() self.ws = None - log.info("##===---- %s Stopped ----===##" % self.__class__.__name__) - - def stopThread(self): - """ - Overwrite this method from thread_methods to close websockets - """ - log.info("Stopping %s thread." % self.__class__.__name__) - self.__threadStopped = True - try: - self.ws.shutdown() - except: - pass + # Close websocket connection on shutdown + if self.ws is not None: + self.ws.close() + LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) @thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) @@ -173,8 +160,8 @@ class PMS_Websocket(WebSocket): sslopt = {} if settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE - log.debug("%s: Uri: %s, sslopt: %s" - % (self.__class__.__name__, uri, sslopt)) + LOG.debug("%s: Uri: %s, sslopt: %s", + self.__class__.__name__, uri, sslopt) return uri, sslopt def process(self, opcode, message): @@ -184,38 +171,38 @@ class PMS_Websocket(WebSocket): try: message = loads(message) except ValueError: - log.error('%s: Error decoding message from websocket' - % self.__class__.__name__) - log.error(message) + LOG.error('%s: Error decoding message from websocket', + self.__class__.__name__) + LOG.error(message) return try: message = message['NotificationContainer'] except KeyError: - log.error('%s: Could not parse PMS message: %s' - % (self.__class__.__name__, message)) + LOG.error('%s: Could not parse PMS message: %s', + self.__class__.__name__, message) return # Triage typus = message.get('type') if typus is None: - log.error('%s: No message type, dropping message: %s' - % (self.__class__.__name__, message)) + LOG.error('%s: No message type, dropping message: %s', + self.__class__.__name__, message) return - log.debug('%s: Received message from PMS server: %s' - % (self.__class__.__name__, message)) + LOG.debug('%s: Received message from PMS server: %s', + self.__class__.__name__, message) # Drop everything we're not interested in if typus not in ('playing', 'timeline', 'activity'): return elif typus == 'activity' and state.DB_SCAN is True: # Only add to processing if PKC is NOT doing a lib scan (and thus # possibly causing these reprocessing messages en mass) - log.debug('%s: Dropping message as PKC is currently synching' - % self.__class__.__name__) + LOG.debug('%s: Dropping message as PKC is currently synching', + self.__class__.__name__) else: # Put PMS message on queue and let libsync take care of it state.WEBSOCKET_QUEUE.put(message) def IOError_response(self): - log.warn("Repeatedly could not connect to PMS, " + LOG.warn("Repeatedly could not connect to PMS, " "declaring the connection dead") window('plex_online', value='false') @@ -232,37 +219,37 @@ class Alexa_Websocket(WebSocket): def getUri(self): uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' % (state.PLEX_USER_ID, - v.PKC_MACHINE_IDENTIFIER, state.PLEX_TOKEN)) + v.PKC_MACHINE_IDENTIFIER, + state.PLEX_TOKEN)) sslopt = {} - log.debug("%s: Uri: %s, sslopt: %s" - % (self.__class__.__name__, uri, sslopt)) + LOG.debug("%s: Uri: %s, sslopt: %s", + self.__class__.__name__, uri, sslopt) return uri, sslopt def process(self, opcode, message): if opcode not in self.opcode_data: return - log.debug('%s: Received the following message from Alexa:' - % self.__class__.__name__) - log.debug('%s: %s' % (self.__class__.__name__, message)) + LOG.debug('%s: Received the following message from Alexa:', + self.__class__.__name__) + LOG.debug('%s: %s', self.__class__.__name__, message) try: message = etree.fromstring(message) except Exception as ex: - log.error('%s: Error decoding message from Alexa: %s' - % (self.__class__.__name__, ex)) + LOG.error('%s: Error decoding message from Alexa: %s', + self.__class__.__name__, ex) return try: if message.attrib['command'] == 'processRemoteControlCommand': message = message[0] else: - log.error('%s: Unknown Alexa message received' - % self.__class__.__name__) + LOG.error('%s: Unknown Alexa message received', + self.__class__.__name__) return except: - log.error('%s: Could not parse Alexa message' - % self.__class__.__name__) + LOG.error('%s: Could not parse Alexa message', + self.__class__.__name__) return - process_command(message.attrib['path'][1:], - message.attrib) + process_command(message.attrib['path'][1:], message.attrib) def IOError_response(self): pass From 4fca4ecf633e21cfdb5eebd8281980c2a0110a58 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 9 Feb 2018 17:48:25 +0100 Subject: [PATCH 307/509] Code refactoring --- resources/lib/initialsetup.py | 105 ++++++++++++++++++++++++++++---- resources/lib/kodimonitor.py | 16 ++--- resources/lib/state.py | 2 + resources/lib/utils.py | 15 ----- service.py | 109 +++++++--------------------------- 5 files changed, 125 insertions(+), 122 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 9cb25373..b5e023f6 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -7,16 +7,19 @@ import xml.etree.ElementTree as etree import xbmc import xbmcgui -from utils import settings, window, language as lang, tryEncode, \ +from utils import settings, window, language as lang, tryEncode, tryDecode, \ XmlKodiSetting, reboot_kodi +from migration import check_migration from downloadutils import DownloadUtils as DU from userclient import UserClient - +from clientinfo import getDeviceId from PlexAPI import PlexAPI from PlexFunctions import GetMachineIdentifier, get_PMS_settings -import state -from migration import check_migration +from json_rpc import get_setting, set_setting import playqueue as PQ +from videonodes import VideoNodes +import state +import variables as v ############################################################################### @@ -25,6 +28,93 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### +WINDOW_PROPERTIES = ( + "plex_online", "plex_serverStatus", "plex_onWake", "plex_kodiScan", + "plex_shouldStop", "plex_dbScan", "plex_initialScan", + "plex_customplayqueue", "plex_playbackProps", "pms_token", "plex_token", + "pms_server", "plex_machineIdentifier", "plex_servername", + "plex_authenticated", "PlexUserImage", "useDirectPaths", "countError", + "countUnauthorized", "plex_restricteduser", "plex_allows_mediaDeletion", + "plex_command", "plex_result", "plex_force_transcode_pix" +) + + +def reload_pkc(): + """ + Will reload state.py entirely and then initiate some values from the Kodi + settings file + """ + LOG.info('Start (re-)loading PKC settings') + # Reset state.py + reload(state) + # Reset window props + for prop in WINDOW_PROPERTIES: + window(prop, clear=True) + # Clear video nodes properties + VideoNodes().clearProperties() + + # Initializing + state.VERIFY_SSL_CERT = settings('sslverify') == 'true' + state.SSL_CERT_PATH = settings('sslcert') \ + if settings('sslcert') != 'None' else None + state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 + state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) + state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' + state.ENABLE_MUSIC = settings('enableMusic') == 'true' + state.BACKGROUND_SYNC = settings( + 'enableBackgroundSync') == 'true' + state.BACKGROUNDSYNC_SAFTYMARGIN = int( + settings('backgroundsync_saftyMargin')) + state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' + state.REMAP_PATH = settings('remapSMB') == 'true' + state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) + state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') + # Init some Queues() + state.COMMAND_PIPELINE_QUEUE = Queue() + state.COMPANION_QUEUE = Queue(maxsize=100) + state.WEBSOCKET_QUEUE = Queue() + set_replace_paths() + set_webserver() + # To detect Kodi profile switches + window('plex_kodiProfile', + value=tryDecode(xbmc.translatePath("special://profile"))) + getDeviceId() + # Initialize the PKC playqueues + PQ.init_playqueues() + LOG.info('Done (re-)loading PKC settings') + + +def set_replace_paths(): + """ + Sets our values for direct paths correctly (including using lower-case + protocols like smb:// and NOT SMB://) + """ + for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): + for arg in ('Org', 'New'): + key = 'remapSMB%s%s' % (typus, arg) + value = settings(key) + if '://' in value: + protocol = value.split('://', 1)[0] + value = value.replace(protocol, protocol.lower()) + setattr(state, key, value) + + +def set_webserver(): + """ + Set the Kodi webserver details - used to set the texture cache + """ + if get_setting('services.webserver') in (None, False): + # Enable the webserver, it is disabled + set_setting('services.webserver', True) + # Set standard port and username + # set_setting('services.webserverport', 8080) + # set_setting('services.webserverusername', 'kodi') + # Webserver already enabled + state.WEBSERVER_PORT = get_setting('services.webserverport') + state.WEBSERVER_USERNAME = get_setting('services.webserverusername') + state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword') + + class InitialSetup(): def __init__(self): @@ -461,13 +551,6 @@ class InitialSetup(): # Do we need to migrate stuff? check_migration() - # Initialize the PKC playqueues - PQ.init_playqueues() - # Init some Queues() - state.COMMAND_PIPELINE_QUEUE = Queue() - state.COMPANION_QUEUE = Queue(maxsize=100) - state.WEBSOCKET_QUEUE = Queue() - # If a Plex server IP has already been set # return only if the right machine identifier is found if self.server: diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 61c74167..18df1810 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -10,12 +10,12 @@ from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, plex_command, thread_methods, \ - set_replace_paths +from utils import window, settings, plex_command, thread_methods from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER from playback import playback_triage +from initialsetup import set_replace_paths import playqueue as PQ import json_rpc as js import playlist_func as PL @@ -29,8 +29,7 @@ LOG = getLogger("PLEX." + __name__) # settings: window-variable WINDOW_SETTINGS = { 'plex_restricteduser': 'plex_restricteduser', - 'force_transcode_pix': 'plex_force_transcode_pix', - 'fetch_pms_item_number': 'fetch_pms_item_number' + 'force_transcode_pix': 'plex_force_transcode_pix' } # settings: state-variable (state.py) @@ -47,7 +46,8 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'enableBackgroundSync': 'BACKGROUND_SYNC' + 'enableBackgroundSync': 'BACKGROUND_SYNC', + 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } ############################################################################### @@ -94,9 +94,6 @@ class KodiMonitor(Monitor): LOG.debug('PKC window settings changed: %s is now %s', settings_value, settings(settings_value)) window(window_value, value=settings(settings_value)) - if settings_value == 'fetch_pms_item_number': - LOG.info('Requesting playlist/nodes refresh') - plex_command('RUN_LIB_SCAN', 'views') # Reset the state variables in state.py for settings_value, state_name in STATE_SETTINGS.iteritems(): new = settings(settings_value) @@ -109,6 +106,9 @@ class KodiMonitor(Monitor): LOG.debug('PKC state settings %s changed from %s to %s', settings_value, getattr(state, state_name), new) setattr(state, state_name, new) + if state_name == 'FETCH_PMS_ITEM_NUMBER': + LOG.info('Requesting playlist/nodes refresh') + plex_command('RUN_LIB_SCAN', 'views') # Special cases, overwrite all internal settings set_replace_paths() state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 diff --git a/resources/lib/state.py b/resources/lib/state.py index 0f7b20f4..f982b67c 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -28,6 +28,8 @@ DIRECT_PATHS = False INDICATE_MEDIA_VERSIONS = False # Do we need to run a special library scan? RUN_LIB_SCAN = None +# Number of items to fetch and display in widgets +FETCH_PMS_ITEM_NUMBER = None # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 641d18a3..e5988c8d 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -362,21 +362,6 @@ def create_actor_db_index(): conn.close() -def set_replace_paths(): - """ - Sets our values for direct paths correctly (including using lower-case - protocols like smb:// and NOT SMB://) - """ - for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): - for arg in ('Org', 'New'): - key = 'remapSMB%s%s' % (typus, arg) - value = settings(key) - if '://' in value: - protocol = value.split('://', 1)[0] - value = value.replace(protocol, protocol.lower()) - setattr(state, key, value) - - def reset(): # Are you sure you want to reset your local Kodi database? if not dialog('yesno', diff --git a/service.py b/service.py index 033e007a..ab293a43 100644 --- a/service.py +++ b/service.py @@ -9,35 +9,31 @@ from xbmcaddon import Addon ############################################################################### -_addon = Addon(id='plugin.video.plexkodiconnect') +_ADDON = Addon(id='plugin.video.plexkodiconnect') try: - _addon_path = _addon.getAddonInfo('path').decode('utf-8') + _ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8') except TypeError: - _addon_path = _addon.getAddonInfo('path').decode() + _ADDON_PATH = _ADDON.getAddonInfo('path').decode() try: - _base_resource = translatePath(os_path.join( - _addon_path, + _BASE_RESOURCE = translatePath(os_path.join( + _ADDON_PATH, 'resources', 'lib')).decode('utf-8') except TypeError: - _base_resource = translatePath(os_path.join( - _addon_path, + _BASE_RESOURCE = translatePath(os_path.join( + _ADDON_PATH, 'resources', 'lib')).decode() -sys_path.append(_base_resource) +sys_path.append(_BASE_RESOURCE) ############################################################################### -from utils import settings, window, language as lang, dialog, tryDecode, \ - set_replace_paths +from utils import settings, window, language as lang, dialog from userclient import UserClient import initialsetup from kodimonitor import KodiMonitor, SpecialMonitor from librarysync import LibrarySync -import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket -import downloadutils -import clientinfo import PlexAPI from PlexCompanion import PlexCompanion @@ -45,7 +41,6 @@ from command_pipeline import Monitor_Window from playback_starter import Playback_Starter from playqueue import PlayqueueMonitor from artwork import Image_Cache_Thread -from json_rpc import get_setting, set_setting import variables as v import state @@ -56,21 +51,6 @@ loghandler.config() LOG = getLogger("PLEX.service") ############################################################################### -def set_webserver(): - """ - Set the Kodi webserver details - used to set the texture cache - """ - if get_setting('services.webserver') in (None, False): - # Enable the webserver, it is disabled - set_setting('services.webserver', True) - # Set standard port and username - # set_setting('services.webserverport', 8080) - # set_setting('services.webserverusername', 'kodi') - # Webserver already enabled - state.WEBSERVER_PORT = get_setting('services.webserverport') - state.WEBSERVER_USERNAME = get_setting('services.webserverusername') - state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword') - class Service(): @@ -100,51 +80,9 @@ class Service(): LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == "true") LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) LOG.info("Full sys.argv received: %s", argv) - - # Reset window props for profile switch - properties = [ - "plex_online", "plex_serverStatus", "plex_onWake", - "plex_kodiScan", - "plex_shouldStop", "plex_dbScan", - "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", - "pms_token", "plex_token", - "pms_server", "plex_machineIdentifier", "plex_servername", - "plex_authenticated", "PlexUserImage", "useDirectPaths", - "countError", "countUnauthorized", - "plex_restricteduser", "plex_allows_mediaDeletion", - "plex_command", "plex_result", "plex_force_transcode_pix" - ] - for prop in properties: - window(prop, clear=True) - - # Clear video nodes properties - videonodes.VideoNodes().clearProperties() - - # Init some stuff - state.VERIFY_SSL_CERT = settings('sslverify') == 'true' - state.SSL_CERT_PATH = settings('sslcert') \ - if settings('sslcert') != 'None' else None - state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 - state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) - state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' - state.ENABLE_MUSIC = settings('enableMusic') == 'true' - state.BACKGROUND_SYNC = settings( - 'enableBackgroundSync') == 'true' - state.BACKGROUNDSYNC_SAFTYMARGIN = int( - settings('backgroundsync_saftyMargin')) - state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' - state.REMAP_PATH = settings('remapSMB') == 'true' - set_replace_paths() - state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) - - window('plex_minDBVersion', value="2.0.0") - set_webserver() self.monitor = Monitor() - window('plex_kodiProfile', - value=tryDecode(translatePath("special://profile"))) - window('fetch_pms_item_number', - value=settings('fetch_pms_item_number')) - clientinfo.getDeviceId() + # Load/Reset PKC entirely - important for user/Kodi profile switch + initialsetup.reload_pkc() def __stop_PKC(self): """ @@ -187,9 +125,8 @@ class Service(): if window('plex_kodiProfile') != kodiProfile: # Profile change happened, terminate this thread and others LOG.info("Kodi profile was: %s and changed to: %s. " - "Terminating old PlexKodiConnect thread." - % (kodiProfile, - window('plex_kodiProfile'))) + "Terminating old PlexKodiConnect thread.", + kodiProfile, window('plex_kodiProfile')) break # Before proceeding, need to make sure: @@ -313,7 +250,7 @@ class Service(): icon='{plex}', time=5000, sound=False) - LOG.info("Server %s is online and ready." % server) + LOG.info("Server %s is online and ready.", server) window('plex_online', value="true") if state.AUTHENTICATED: # Server got offline when we were authenticated. @@ -338,28 +275,24 @@ class Service(): # Tell all threads to terminate (e.g. several lib sync threads) state.STOP_PKC = True - try: - downloadutils.DownloadUtils().stopSession() - except: - pass window('plex_service_started', clear=True) - LOG.info("======== STOP %s ========" % v.ADDON_NAME) + LOG.info("======== STOP %s ========", v.ADDON_NAME) # Safety net - Kody starts PKC twice upon first installation! if window('plex_service_started') == 'true': - exit = True + EXIT = True else: window('plex_service_started', value='true') - exit = False + EXIT = False # Delay option -delay = int(settings('startupDelay')) +DELAY = int(settings('startupDelay')) -LOG.info("Delaying Plex startup by: %s sec..." % delay) -if exit: +LOG.info("Delaying Plex startup by: %s sec...", DELAY) +if EXIT: LOG.error('PKC service.py already started - exiting this instance') -elif delay and Monitor().waitForAbort(delay): +elif DELAY and Monitor().waitForAbort(DELAY): # Start the service LOG.info("Abort requested while waiting. PKC not started.") else: From 11510766609fa6bd29c35dcd6805f164d9dbe165 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Feb 2018 17:59:20 +0100 Subject: [PATCH 308/509] Code refactoring --- resources/lib/PlexAPI.py | 1129 +------------------------------- resources/lib/PlexFunctions.py | 531 +++++++++++++-- resources/lib/entrypoint.py | 6 +- resources/lib/initialsetup.py | 336 +++++----- resources/lib/plex_tv.py | 338 ++++++++++ resources/lib/userclient.py | 107 ++- resources/lib/utils.py | 33 +- service.py | 10 +- 8 files changed, 1086 insertions(+), 1404 deletions(-) create mode 100644 resources/lib/plex_tv.py diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 01ca1714..f8d33038 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -29,27 +29,20 @@ http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-python (and others...) """ - from logging import getLogger -from time import time -import urllib2 -import socket -from threading import Thread -import xml.etree.ElementTree as etree from re import compile as re_compile, sub -from urllib import urlencode, quote_plus, unquote +from urllib import urlencode, unquote from os.path import basename, join from os import makedirs -import xbmcgui -from xbmc import sleep, executebuiltin +from xbmcgui import ListItem from xbmcvfs import exists import clientinfo as client -from downloadutils import DownloadUtils +from downloadutils import DownloadUtils as DU from utils import window, settings, language as lang, tryDecode, tryEncode, \ - DateToKodi, exists_dir, slugify -from PlexFunctions import PMSHttpsEnabled + DateToKodi, exists_dir, slugify, dialog +import PlexFunctions as PF import plexdb_functions as plexdb import variables as v import state @@ -62,1065 +55,6 @@ REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') ############################################################################### -class PlexAPI(): - def __init__(self): - self.g_PMS = {} - self.doUtils = DownloadUtils().downloadUrl - - def GetPlexLoginFromSettings(self): - """ - Returns a dict: - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') - - Returns strings or unicode - - Returns empty strings '' for a setting if not found. - - myplexlogin is 'true' if user opted to log into plex.tv (the default) - plexhome is 'true' if plex home is used (the default) - """ - return { - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') - } - - def GetPlexLoginAndPassword(self): - """ - Signs in to plex.tv. - - plexLogin, authtoken = GetPlexLoginAndPassword() - - Input: nothing - Output: - plexLogin plex.tv username - authtoken token for plex.tv - - Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file - If not logged in, empty strings are returned for both. - """ - retrievedPlexLogin = '' - plexLogin = 'dummy' - authtoken = '' - dialog = xbmcgui.Dialog() - while retrievedPlexLogin == '' and plexLogin != '': - # Enter plex.tv username. Or nothing to cancel. - plexLogin = dialog.input(lang(29999) + lang(39300), - type=xbmcgui.INPUT_ALPHANUM) - if plexLogin != "": - # Enter password for plex.tv user - plexPassword = dialog.input( - lang(39301) + plexLogin, - type=xbmcgui.INPUT_ALPHANUM, - option=xbmcgui.ALPHANUM_HIDE_INPUT) - retrievedPlexLogin, authtoken = self.MyPlexSignIn( - plexLogin, - plexPassword, - {'X-Plex-Client-Identifier': window('plex_client_Id')}) - LOG.debug("plex.tv username and token: %s, %s", - plexLogin, authtoken) - if plexLogin == '': - # Could not sign in user - dialog.ok(lang(29999), lang(39302) + plexLogin) - # Write to Kodi settings file - settings('plexLogin', value=retrievedPlexLogin) - settings('plexToken', value=authtoken) - return (retrievedPlexLogin, authtoken) - - def PlexTvSignInWithPin(self): - """ - Prompts user to sign in by visiting https://plex.tv/pin - - Writes to Kodi settings file. Also returns: - { - 'plexhome': 'true' if Plex Home, 'false' otherwise - 'username': - 'avatar': URL to user avator - 'token': - 'plexid': Plex user ID - 'homesize': Number of Plex home users (defaults to '1') - } - Returns False if authentication did not work. - """ - code, identifier = self.GetPlexPin() - dialog = xbmcgui.Dialog() - if not code: - # Problems trying to contact plex.tv. Try again later - dialog.ok(lang(29999), lang(39303)) - return False - # Go to https://plex.tv/pin and enter the code: - # Or press No to cancel the sign in. - answer = dialog.yesno(lang(29999), - lang(39304) + "\n\n", - code + "\n\n", - lang(39311)) - if not answer: - return False - count = 0 - # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) - while count < 30: - xml = self.CheckPlexTvSignin(identifier) - if xml is not False: - break - # Wait for 1 seconds - sleep(1000) - count += 1 - if xml is False: - # Could not sign in to plex.tv Try again later - dialog.ok(lang(29999), lang(39305)) - return False - # Parse xml - userid = xml.attrib.get('id') - home = xml.get('home', '0') - if home == '1': - home = 'true' - else: - home = 'false' - username = xml.get('username', '') - avatar = xml.get('thumb', '') - token = xml.findtext('authentication-token') - homeSize = xml.get('homeSize', '1') - result = { - 'plexhome': home, - 'username': username, - 'avatar': avatar, - 'token': token, - 'plexid': userid, - 'homesize': homeSize - } - settings('plexLogin', username) - settings('plexToken', token) - settings('plexhome', home) - settings('plexid', userid) - settings('plexAvatar', avatar) - settings('plexHomeSize', homeSize) - # Let Kodi log into plex.tv on startup from now on - settings('myplexlogin', 'true') - settings('plex_status', value=lang(39227)) - return result - - def CheckPlexTvSignin(self, identifier): - """ - Checks with plex.tv whether user entered the correct PIN on plex.tv/pin - - Returns False if not yet done so, or the XML response file as etree - """ - # Try to get a temporary token - xml = self.doUtils('https://plex.tv/pins/%s.xml' % identifier, - authenticate=False) - try: - temp_token = xml.find('auth_token').text - except: - LOG.error("Could not find token in plex.tv answer") - return False - if not temp_token: - return False - # Use temp token to get the final plex credentials - xml = self.doUtils('https://plex.tv/users/account', - authenticate=False, - parameters={'X-Plex-Token': temp_token}) - return xml - - def GetPlexPin(self): - """ - For plex.tv sign-in: returns 4-digit code and identifier as 2 str - """ - code = None - identifier = None - # Download - xml = self.doUtils('https://plex.tv/pins.xml', - authenticate=False, - action_type="POST") - try: - xml.attrib - except: - LOG.error("Error, no PIN from plex.tv provided") - return None, None - code = xml.find('code').text - identifier = xml.find('id').text - LOG.info('Successfully retrieved code and id from plex.tv') - return code, identifier - - def CheckConnection(self, url, token=None, verifySSL=None): - """ - Checks connection to a Plex server, available at url. Can also be used - to check for connection with plex.tv. - - Override SSL to skip the check by setting verifySSL=False - if 'None', SSL will be checked (standard requests setting) - if 'True', SSL settings from file settings are used (False/True) - - Input: - url URL to Plex server (e.g. https://192.168.1.1:32400) - token appropriate token to access server. If None is passed, - the current token is used - Output: - False if server could not be reached or timeout occured - 200 if connection was successfull - int or other HTML status codes as received from the server - """ - # Add '/clients' to URL because then an authentication is necessary - # If a plex.tv URL was passed, this does not work. - headerOptions = None - if token is not None: - headerOptions = {'X-Plex-Token': token} - if verifySSL is True: - verifySSL = None if settings('sslverify') == 'true' \ - else False - if 'plex.tv' in url: - url = 'https://plex.tv/api/home/users' - else: - url = url + '/library/onDeck' - LOG.debug("Checking connection to server %s with verifySSL=%s", - url, verifySSL) - count = 0 - while count < 1: - answer = self.doUtils(url, - authenticate=False, - headerOptions=headerOptions, - verifySSL=verifySSL, - timeout=10) - if answer is None: - LOG.debug("Could not connect to %s", url) - count += 1 - sleep(500) - continue - try: - # xml received? - answer.attrib - except: - if answer is True: - # Maybe no xml but connection was successful nevertheless - answer = 200 - else: - # Success - we downloaded an xml! - answer = 200 - # We could connect but maybe were not authenticated. No worries - LOG.debug("Checking connection successfull. Answer: %s", answer) - return answer - LOG.debug('Failed to connect to %s too many times. PMS is dead', url) - return False - - def GetgPMSKeylist(self): - """ - Returns a list of all keys that are saved for every entry in the - g_PMS variable. - """ - keylist = [ - 'address', - 'baseURL', - 'enableGzip', - 'ip', - 'local', - 'name', - 'owned', - 'port', - 'scheme' - ] - return keylist - - def declarePMS(self, uuid, name, scheme, ip, port): - """ - Plex Media Server handling - - parameters: - uuid - PMS ID - name, scheme, ip, port, type, owned, token - """ - address = ip + ':' + port - baseURL = scheme + '://' + ip + ':' + port - self.g_PMS[uuid] = { - 'name': name, - 'scheme': scheme, - 'ip': ip, - 'port': port, - 'address': address, - 'baseURL': baseURL, - 'local': '1', - 'owned': '1', - 'accesstoken': '', - 'enableGzip': False - } - - def updatePMSProperty(self, uuid, tag, value): - # set property element of PMS by UUID - try: - self.g_PMS[uuid][tag] = value - except: - LOG.error('%s has not yet been declared', uuid) - return False - - def getPMSProperty(self, uuid, tag): - # get name of PMS by UUID - try: - answ = self.g_PMS[uuid].get(tag, '') - except: - LOG.error('%s not found in PMS catalogue', uuid) - answ = False - return answ - - def PlexGDM(self): - """ - PlexGDM - - parameters: - none - result: - PMS_list - dict() of PMSs found - """ - import struct - - IP_PlexGDM = '239.0.0.250' # multicast to PMS - Port_PlexGDM = 32414 - Msg_PlexGDM = 'M-SEARCH * HTTP/1.0' - - # setup socket for discovery -> multicast message - GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - GDM.settimeout(2.0) - - # Set the time-to-live for messages to 2 for local network - ttl = struct.pack('b', 2) - GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - - returnData = [] - try: - # Send data to the multicast group - GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM)) - - # Look for responses from all recipients - while True: - try: - data, server = GDM.recvfrom(1024) - returnData.append({'from': server, - 'data': data}) - except socket.timeout: - break - except Exception as e: - # Probably error: (101, 'Network is unreachable') - LOG.error(e) - import traceback - LOG.error("Traceback:\n%s", traceback.format_exc()) - finally: - GDM.close() - - pmsList = {} - for response in returnData: - update = {'ip': response.get('from')[0]} - # Check if we had a positive HTTP response - if "200 OK" not in response.get('data'): - continue - for each in response.get('data').split('\n'): - # decode response data - update['discovery'] = "auto" - # update['owned']='1' - # update['master']= 1 - # update['role']='master' - - if "Content-Type:" in each: - update['content-type'] = each.split(':')[1].strip() - elif "Resource-Identifier:" in each: - update['uuid'] = each.split(':')[1].strip() - elif "Name:" in each: - update['serverName'] = tryDecode( - each.split(':')[1].strip()) - elif "Port:" in each: - update['port'] = each.split(':')[1].strip() - elif "Updated-At:" in each: - update['updated'] = each.split(':')[1].strip() - elif "Version:" in each: - update['version'] = each.split(':')[1].strip() - pmsList[update['uuid']] = update - return pmsList - - def discoverPMS(self, IP_self, plexToken=None): - """ - parameters: - IP_self Own IP - optional: - plexToken token for plex.tv - result: - self.g_PMS dict set - """ - self.g_PMS = {} - - # Look first for local PMS in the LAN - pmsList = self.PlexGDM() - LOG.debug('PMS found in the local LAN via GDM: %s', pmsList) - - # Get PMS from plex.tv - if plexToken: - LOG.info('Checking with plex.tv for more PMS to connect to') - self.getPMSListFromMyPlex(plexToken) - else: - LOG.info('No plex token supplied, only checked LAN for PMS') - - for uuid in pmsList: - PMS = pmsList[uuid] - if PMS['uuid'] in self.g_PMS: - LOG.debug('We already know of PMS %s from plex.tv', - PMS['serverName']) - # Update with GDM data - potentially more reliable than plex.tv - self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip']) - self.updatePMSProperty(PMS['uuid'], 'port', PMS['port']) - self.updatePMSProperty(PMS['uuid'], 'local', '1') - self.updatePMSProperty(PMS['uuid'], 'scheme', 'http') - self.updatePMSProperty(PMS['uuid'], - 'baseURL', - 'http://%s:%s' % (PMS['ip'], - PMS['port'])) - else: - self.declarePMS(PMS['uuid'], PMS['serverName'], 'http', - PMS['ip'], PMS['port']) - # Ping to check whether we need HTTPs or HTTP - https = PMSHttpsEnabled('%s:%s' % (PMS['ip'], PMS['port'])) - if https is None: - # Error contacting url. Skip for now - continue - elif https is True: - self.updatePMSProperty(PMS['uuid'], 'scheme', 'https') - self.updatePMSProperty( - PMS['uuid'], - 'baseURL', - 'https://%s:%s' % (PMS['ip'], PMS['port'])) - else: - # Already declared with http - pass - - # install plex.tv "virtual" PMS - for myPlex, PlexHome - # self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443') - # self.updatePMSProperty('plex.tv', 'local', '-') - # self.updatePMSProperty('plex.tv', 'owned', '-') - # self.updatePMSProperty( - # 'plex.tv', 'accesstoken', plexToken) - # (remote and local) servers from plex.tv - - def getPMSListFromMyPlex(self, token): - """ - getPMSListFromMyPlex - - get Plex media Server List from plex.tv/pms/resources - """ - xml = self.doUtils('https://plex.tv/api/resources', - authenticate=False, - parameters={'includeHttps': 1}, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except AttributeError: - LOG.error('Could not get list of PMS from plex.tv') - return - - import Queue - queue = Queue.Queue() - threadQueue = [] - - maxAgeSeconds = 2*60*60*24 - for Dir in xml.findall('Device'): - if 'server' not in Dir.get('provides'): - # No PMS - skip - continue - if Dir.find('Connection') is None: - # no valid connection - skip - continue - - # check MyPlex data age - skip if >2 days - PMS = {} - PMS['name'] = Dir.get('name') - infoAge = time() - int(Dir.get('lastSeenAt')) - if infoAge > maxAgeSeconds: - LOG.debug("Server %s not seen for 2 days - skipping.", - PMS['name']) - continue - - PMS['uuid'] = Dir.get('clientIdentifier') - PMS['token'] = Dir.get('accessToken', token) - PMS['owned'] = Dir.get('owned', '1') - PMS['local'] = Dir.get('publicAddressMatches') - PMS['ownername'] = Dir.get('sourceTitle', '') - PMS['path'] = '/' - PMS['options'] = None - - # Try a local connection first - # Backup to remote connection, if that failes - PMS['connections'] = [] - for Con in Dir.findall('Connection'): - if Con.get('local') == '1': - PMS['connections'].append(Con) - # Append non-local - for Con in Dir.findall('Connection'): - if Con.get('local') != '1': - PMS['connections'].append(Con) - - t = Thread(target=self.pokePMS, - args=(PMS, queue)) - threadQueue.append(t) - - maxThreads = 5 - threads = [] - # poke PMS, own thread for each PMS - while True: - # Remove finished threads - for t in threads: - if not t.isAlive(): - threads.remove(t) - if len(threads) < maxThreads: - try: - t = threadQueue.pop() - except IndexError: - # We have done our work - break - else: - t.start() - threads.append(t) - else: - sleep(50) - - # wait for requests being answered - for t in threads: - t.join() - - # declare new PMSs - while not queue.empty(): - PMS = queue.get() - self.declarePMS(PMS['uuid'], PMS['name'], - PMS['protocol'], PMS['ip'], PMS['port']) - self.updatePMSProperty( - PMS['uuid'], 'accesstoken', PMS['token']) - self.updatePMSProperty( - PMS['uuid'], 'owned', PMS['owned']) - self.updatePMSProperty( - PMS['uuid'], 'local', PMS['local']) - # set in declarePMS, overwrite for https encryption - self.updatePMSProperty( - PMS['uuid'], 'baseURL', PMS['baseURL']) - self.updatePMSProperty( - PMS['uuid'], 'ownername', PMS['ownername']) - LOG.debug('Found PMS %s: %s', - PMS['uuid'], self.g_PMS[PMS['uuid']]) - queue.task_done() - - def pokePMS(self, PMS, queue): - data = PMS['connections'][0].attrib - if data['local'] == '1': - protocol = data['protocol'] - address = data['address'] - port = data['port'] - url = '%s://%s:%s' % (protocol, address, port) - else: - url = data['uri'] - if url.count(':') == 1: - url = '%s:%s' % (url, data['port']) - protocol, address, port = url.split(':', 2) - address = address.replace('/', '') - - xml = self.doUtils('%s/identity' % url, - authenticate=False, - headerOptions={'X-Plex-Token': PMS['token']}, - verifySSL=False, - timeout=10) - try: - xml.attrib['machineIdentifier'] - except (AttributeError, KeyError): - # No connection, delete the one we just tested - del PMS['connections'][0] - if len(PMS['connections']) > 0: - # Still got connections left, try them - return self.pokePMS(PMS, queue) - return - else: - # Connection successful - correct PMS? - if xml.get('machineIdentifier') == PMS['uuid']: - # process later - PMS['baseURL'] = url - PMS['protocol'] = protocol - PMS['ip'] = address - PMS['port'] = port - queue.put(PMS) - return - LOG.info('Found a PMS at %s, but the expected machineIdentifier of ' - '%s did not match the one we found: %s', - url, PMS['uuid'], xml.get('machineIdentifier')) - - def MyPlexSignIn(self, username, password, options): - """ - MyPlex Sign In, Sign Out - - parameters: - username - Plex forum name, MyPlex login, or email address - password - options - dict() of PlexConnect-options as received from aTV - necessary: PlexConnectUDID - result: - username - authtoken - token for subsequent communication with MyPlex - """ - # MyPlex web address - MyPlexHost = 'plex.tv' - MyPlexSignInPath = '/users/sign_in.xml' - MyPlexURL = 'https://' + MyPlexHost + MyPlexSignInPath - - # create POST request - xargs = client.getXArgsDeviceInfo(options) - request = urllib2.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' - - passmanager = urllib2.HTTPPasswordMgr() - passmanager.add_password(MyPlexHost, MyPlexURL, username, password) - authhandler = urllib2.HTTPBasicAuthHandler(passmanager) - urlopener = urllib2.build_opener(authhandler) - - # sign in, get MyPlex response - try: - response = urlopener.open(request).read() - except urllib2.HTTPError as e: - if e.code == 401: - LOG.info("Authentication failed") - return ('', '') - else: - raise - # analyse response - XMLTree = etree.ElementTree(etree.fromstring(response)) - - el_username = XMLTree.find('username') - el_authtoken = XMLTree.find('authentication-token') - if el_username is None or \ - el_authtoken is None: - username = '' - authtoken = '' - else: - username = el_username.text - authtoken = el_authtoken.text - return (username, authtoken) - - def MyPlexSignOut(self, authtoken): - """ - TO BE DONE! - """ - # MyPlex web address - MyPlexHost = 'plex.tv' - MyPlexSignOutPath = '/users/sign_out.xml' - MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath - - # create POST request - xargs = {'X-Plex-Token': authtoken} - request = urllib2.Request(MyPlexURL, None, xargs) - # turn into 'POST' - done automatically with data!=None. But we don't - # have data. - request.get_method = lambda: 'POST' - - response = urllib2.urlopen(request).read() - - def GetUserArtworkURL(self, username): - """ - Returns the URL for the user's Avatar. Or False if something went - wrong. - """ - plexToken = settings('plexToken') - users = self.MyPlexListHomeUsers(plexToken) - url = '' - # If an error is encountered, set to False - if not users: - LOG.info("Couldnt get user from plex.tv. No URL for user avatar") - return False - for user in users: - if username in user['title']: - url = user['thumb'] - LOG.debug("Avatar url for user %s is: %s", username, url) - return url - - def ChoosePlexHomeUser(self, plexToken): - """ - Let's user choose from a list of Plex home users. Will switch to that - user accordingly. - - Returns a dict: - { - 'username': Unicode - 'userid': '' Plex ID of the user - 'token': '' User's token - 'protected': True if PIN is needed, else False - } - - Will return False if something went wrong (wrong PIN, no connection) - """ - dialog = xbmcgui.Dialog() - - # Get list of Plex home users - users = self.MyPlexListHomeUsers(plexToken) - if not users: - LOG.error("User download failed.") - return False - - userlist = [] - userlistCoded = [] - for user in users: - username = user['title'] - userlist.append(username) - # To take care of non-ASCII usernames - userlistCoded.append(tryEncode(username)) - usernumber = len(userlist) - - username = '' - usertoken = '' - trials = 0 - while trials < 3: - if usernumber > 1: - # Select user - user_select = dialog.select( - lang(29999) + lang(39306), - userlistCoded) - if user_select == -1: - LOG.info("No user selected.") - settings('username', value='') - executebuiltin('Addon.OpenSettings(%s)' - % v.ADDON_ID) - return False - # Only 1 user received, choose that one - else: - user_select = 0 - selected_user = userlist[user_select] - LOG.info("Selected user: %s", selected_user) - user = users[user_select] - # Ask for PIN, if protected: - pin = None - if user['protected'] == '1': - LOG.debug('Asking for users PIN') - pin = dialog.input( - lang(39307) + selected_user, - '', - xbmcgui.INPUT_NUMERIC, - xbmcgui.ALPHANUM_HIDE_INPUT) - # User chose to cancel - # Plex bug: don't call url for protected user with empty PIN - if not pin: - trials += 1 - continue - # Switch to this Plex Home user, if applicable - result = self.PlexSwitchHomeUser( - user['id'], - pin, - plexToken, - settings('plex_machineIdentifier')) - if result: - # Successfully retrieved username: break out of while loop - username = result['username'] - usertoken = result['usertoken'] - break - # Couldn't get user auth - else: - trials += 1 - # Could not login user, please try again - if not dialog.yesno(lang(29999), - lang(39308) + selected_user, - lang(39309)): - # User chose to cancel - break - if not username: - LOG.error('Failed signing in a user to plex.tv') - executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) - return False - return { - 'username': username, - 'userid': user['id'], - 'protected': True if user['protected'] == '1' else False, - 'token': usertoken - } - - def PlexSwitchHomeUser(self, userId, pin, token, machineIdentifier): - """ - Retrieves Plex home token for a Plex home user. - Returns False if unsuccessful - - Input: - userId id of the Plex home user - pin PIN of the Plex home user, if protected - token token for plex.tv - - Output: - { - 'username' - 'usertoken' Might be empty strings if no token found - for the machineIdentifier that was chosen - } - - settings('userid') and settings('username') with new plex token - """ - LOG.info('Switching to user %s', userId) - url = 'https://plex.tv/api/home/users/' + userId + '/switch' - if pin: - url += '?pin=' + pin - answer = self.doUtils(url, - authenticate=False, - action_type="POST", - headerOptions={'X-Plex-Token': token}) - try: - answer.attrib - except: - LOG.error('Error: plex.tv switch HomeUser change failed') - return False - - username = answer.attrib.get('title', '') - token = answer.attrib.get('authenticationToken', '') - - # Write to settings file - settings('username', username) - settings('accessToken', token) - settings('userid', answer.attrib.get('id', '')) - settings('plex_restricteduser', - 'true' if answer.attrib.get('restricted', '0') == '1' - else 'false') - state.RESTRICTED_USER = True if \ - answer.attrib.get('restricted', '0') == '1' else False - - # Get final token to the PMS we've chosen - url = 'https://plex.tv/api/resources?includeHttps=1' - xml = self.doUtils(url, - authenticate=False, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except: - LOG.error('Answer from plex.tv not as excepted') - # Set to empty iterable list for loop - xml = [] - - found = 0 - LOG.debug('Our machineIdentifier is %s', machineIdentifier) - for device in xml: - identifier = device.attrib.get('clientIdentifier') - LOG.debug('Found a Plex machineIdentifier: %s', identifier) - if (identifier in machineIdentifier or - machineIdentifier in identifier): - found += 1 - token = device.attrib.get('accessToken') - - result = { - 'username': username, - } - if found == 0: - LOG.info('No tokens found for your server! Using empty string') - result['usertoken'] = '' - else: - result['usertoken'] = token - LOG.info('Plex.tv switch HomeUser change successfull for user %s', - username) - return result - - def MyPlexListHomeUsers(self, token): - """ - Returns a list for myPlex home users for the current plex.tv account. - - Input: - token for plex.tv - Output: - List of users, where one entry is of the form: - "id": userId, - "admin": '1'/'0', - "guest": '1'/'0', - "restricted": '1'/'0', - "protected": '1'/'0', - "email": email, - "title": title, - "username": username, - "thumb": thumb_url - } - If any value is missing, None is returned instead (or "" from plex.tv) - If an error is encountered, False is returned - """ - xml = self.doUtils('https://plex.tv/api/home/users/', - authenticate=False, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except: - LOG.error('Download of Plex home users failed.') - return False - users = [] - for user in xml: - users.append(user.attrib) - return users - - def getDirectVideoPath(self, key, AuthToken): - """ - Direct Video Play support - - parameters: - path - AuthToken - Indirect - media indirect specified, grab child XML to gain real path - options - result: - final path to media file - """ - if key.startswith('http://') or key.startswith('https://'): # external address - keep - path = key - else: - if AuthToken == '': - path = key - else: - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if key.find('?') == -1: - path = key + '?' + urlencode(xargs) - else: - path = key + '&' + urlencode(xargs) - - return path - - def getTranscodeImagePath(self, key, AuthToken, path, width, height): - """ - Transcode Image support - - parameters: - key - AuthToken - path - source path of current XML: path[srcXML] - width - height - result: - final path to image file - """ - if key.startswith('http://') or key.startswith('https://'): # external address - can we get a transcoding request for external images? - path = key - elif key.startswith('/'): # internal full path. - path = 'http://127.0.0.1:32400' + key - else: # internal path, add-on - path = 'http://127.0.0.1:32400' + path + '/' + key - path = tryEncode(path) - - # This is bogus (note the extra path component) but ATV is stupid when it comes to caching images, it doesn't use querystrings. - # Fortunately PMS is lenient... - transcodePath = '/photo/:/transcode/' + \ - str(width) + 'x' + str(height) + '/' + quote_plus(path) - - args = dict() - args['width'] = width - args['height'] = height - args['url'] = path - - if not AuthToken == '': - args['X-Plex-Token'] = AuthToken - - return transcodePath + '?' + urlencode(args) - - def getDirectImagePath(self, path, AuthToken): - """ - Direct Image support - - parameters: - path - AuthToken - result: - final path to image file - """ - if not AuthToken == '': - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if path.find('?') == -1: - path = path + '?' + urlencode(xargs) - else: - path = path + '&' + urlencode(xargs) - - return path - - def getTranscodeAudioPath(self, path, AuthToken, options, maxAudioBitrate): - """ - Transcode Audio support - - parameters: - path - AuthToken - options - dict() of PlexConnect-options as received from aTV - maxAudioBitrate - [kbps] - result: - final path to pull in PMS transcoder - """ - UDID = options['PlexConnectUDID'] - - transcodePath = '/music/:/transcode/universal/start.mp3?' - - args = dict() - args['path'] = path - args['session'] = UDID - args['protocol'] = 'http' - args['maxAudioBitrate'] = maxAudioBitrate - - xargs = client.getXArgsDeviceInfo(options) - if not AuthToken == '': - xargs['X-Plex-Token'] = AuthToken - - return transcodePath + urlencode(args) + '&' + urlencode(xargs) - - def getDirectAudioPath(self, path, AuthToken): - """ - Direct Audio support - - parameters: - path - AuthToken - result: - final path to audio file - """ - if not AuthToken == '': - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if path.find('?') == -1: - path = path + '?' + urlencode(xargs) - else: - path = path + '&' + urlencode(xargs) - - return path - - def returnServerList(self, data): - """ - Returns a nicer list of all servers found in data, where data is in - g_PMS format, for the client device with unique ID ATV_udid - - Input: - data e.g. self.g_PMS - - Output: List of all servers, with an entry of the form: - { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner - } - """ - serverlist = [] - for key, value in data.items(): - serverlist.append({ - 'name': value.get('name'), - 'address': value.get('address'), - 'ip': value.get('ip'), - 'port': value.get('port'), - 'scheme': value.get('scheme'), - 'local': value.get('local'), - 'owned': value.get('owned'), - 'machineIdentifier': key, - 'accesstoken': value.get('accesstoken'), - 'baseURL': value.get('baseURL'), - 'ownername': value.get('ownername') - }) - return serverlist - - class API(): """ API(item) @@ -1129,7 +63,6 @@ class API(): item: xml.etree.ElementTree element """ - def __init__(self, item): self.item = item # which media part in the XML response shall we look at? @@ -1222,7 +155,7 @@ class API(): extension not in v.KODI_SUPPORTED_IMAGES): # Let Plex transcode # max width/height supported by plex image transcoder is 1920x1080 - path = self.server + PlexAPI().getTranscodeImagePath( + path = self.server + PF.transcode_image_path( self.item[0][0].attrib.get('key'), window('pms_token'), "%s%s" % (self.server, self.item[0][0].attrib.get('key')), @@ -1953,11 +886,10 @@ class API(): 'language': v.KODILANGUAGE, 'query': tryEncode(title) } - data = DownloadUtils().downloadUrl( - url, - authenticate=False, - parameters=parameters, - timeout=7) + data = DU().downloadUrl(url, + authenticate=False, + parameters=parameters, + timeout=7) try: data.get('test') except: @@ -2044,11 +976,10 @@ class API(): elif media_type == "tv": url = 'https://api.themoviedb.org/3/tv/%s' % tmdbId parameters['append_to_response'] = 'external_ids,videos' - data = DownloadUtils().downloadUrl( - url, - authenticate=False, - parameters=parameters, - timeout=7) + data = DU().downloadUrl(url, + authenticate=False, + parameters=parameters, + timeout=7) try: data.get('test') except: @@ -2069,11 +1000,10 @@ class API(): LOG.debug('Retrieved collections tmdb id %s for %s', mediaId, title) url = 'https://api.themoviedb.org/3/collection/%s' % mediaId - data = DownloadUtils().downloadUrl( - url, - authenticate=False, - parameters=parameters, - timeout=7) + data = DU().downloadUrl(url, + authenticate=False, + parameters=parameters, + timeout=7) try: data.get('poster_path') except AttributeError: @@ -2108,10 +1038,9 @@ class API(): else: # Not supported artwork return allartworks - data = DownloadUtils().downloadUrl( - url, - authenticate=False, - timeout=15) + data = DU().downloadUrl(url, + authenticate=False, + timeout=15) try: data.get('test') except: @@ -2287,7 +1216,7 @@ class API(): option = '%s%s ' % (option, entry.attrib.get('audioCodec')) dialoglist.append(option) - media = xbmcgui.Dialog().select('Select stream', dialoglist) + media = dialog('select', 'Select stream', dialoglist) else: media = 0 self.mediastream = media @@ -2413,7 +1342,7 @@ class API(): if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH): makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH) path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename) - r = DownloadUtils().downloadUrl(url, return_response=True) + r = DU().downloadUrl(url, return_response=True) try: r.status_code except AttributeError: @@ -2468,7 +1397,7 @@ class API(): """ title, _ = self.getTitle() if listItem is None: - listItem = xbmcgui.ListItem(title) + listItem = ListItem(title) else: listItem.setLabel(title) metadata = { @@ -2501,7 +1430,7 @@ class API(): typus = self.getType() if listItem is None: - listItem = xbmcgui.ListItem(title) + listItem = ListItem(title) else: listItem.setLabel(title) # Necessary; Kodi won't start video otherwise! @@ -2654,10 +1583,10 @@ class API(): Returns True if sync should stop, else False """ LOG.warn('Cannot access file: %s', url) - resp = xbmcgui.Dialog().yesno( - heading=lang(29999), - line1=lang(39031) + url, - line2=lang(39032)) + resp = dialog('yesno', + heading=lang(29999), + line1=lang(39031) + url, + line2=lang(39032)) return resp def set_listitem_artwork(self, listitem): diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 71f1e32f..3bf7d197 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -1,21 +1,31 @@ # -*- coding: utf-8 -*- from logging import getLogger -from urllib import urlencode +from urllib import urlencode, quote_plus from ast import literal_eval from urlparse import urlparse, parse_qsl -import re +from re import compile as re_compile from copy import deepcopy +from time import time +from threading import Thread -import downloadutils -from utils import settings +from xbmc import sleep + +from downloadutils import DownloadUtils as DU +from utils import settings, tryEncode, tryDecode from variables import PLEX_TO_KODI_TIMEFACTOR +import plex_tv ############################################################################### - -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) CONTAINERSIZE = int(settings('limitindex')) -REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''') +REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''') + +# For discovery of PMS in the local LAN +PLEX_GDM_IP = '239.0.0.250' # multicast to PMS +PLEX_GDM_PORT = 32414 +PLEX_GDM_MSG = 'M-SEARCH * HTTP/1.0' + ############################################################################### @@ -76,6 +86,374 @@ def GetMethodFromPlexType(plexType): return methods[plexType] +def GetPlexLoginFromSettings(): + """ + Returns a dict: + 'plexLogin': settings('plexLogin'), + 'plexToken': settings('plexToken'), + 'plexhome': settings('plexhome'), + 'plexid': settings('plexid'), + 'myplexlogin': settings('myplexlogin'), + 'plexAvatar': settings('plexAvatar'), + 'plexHomeSize': settings('plexHomeSize') + + Returns strings or unicode + + Returns empty strings '' for a setting if not found. + + myplexlogin is 'true' if user opted to log into plex.tv (the default) + plexhome is 'true' if plex home is used (the default) + """ + return { + 'plexLogin': settings('plexLogin'), + 'plexToken': settings('plexToken'), + 'plexhome': settings('plexhome'), + 'plexid': settings('plexid'), + 'myplexlogin': settings('myplexlogin'), + 'plexAvatar': settings('plexAvatar'), + 'plexHomeSize': settings('plexHomeSize') + } + + +def check_connection(url, token=None, verifySSL=None): + """ + Checks connection to a Plex server, available at url. Can also be used + to check for connection with plex.tv. + + Override SSL to skip the check by setting verifySSL=False + if 'None', SSL will be checked (standard requests setting) + if 'True', SSL settings from file settings are used (False/True) + + Input: + url URL to Plex server (e.g. https://192.168.1.1:32400) + token appropriate token to access server. If None is passed, + the current token is used + Output: + False if server could not be reached or timeout occured + 200 if connection was successfull + int or other HTML status codes as received from the server + """ + # Add '/clients' to URL because then an authentication is necessary + # If a plex.tv URL was passed, this does not work. + header_options = None + if token is not None: + header_options = {'X-Plex-Token': token} + if verifySSL is True: + verifySSL = None if settings('sslverify') == 'true' else False + if 'plex.tv' in url: + url = 'https://plex.tv/api/home/users' + else: + url = url + '/library/onDeck' + LOG.debug("Checking connection to server %s with verifySSL=%s", + url, verifySSL) + answer = DU().downloadUrl(url, + authenticate=False, + headerOptions=header_options, + verifySSL=verifySSL, + timeout=10) + if answer is None: + LOG.debug("Could not connect to %s", url) + return False + try: + # xml received? + answer.attrib + except AttributeError: + if answer is True: + # Maybe no xml but connection was successful nevertheless + answer = 200 + else: + # Success - we downloaded an xml! + answer = 200 + # We could connect but maybe were not authenticated. No worries + LOG.debug("Checking connection successfull. Answer: %s", answer) + return answer + + +def discover_pms(token=None): + """ + Optional parameter: + token token for plex.tv + + Returns a list of available PMS to connect to, one entry is the dict: + { + 'machineIdentifier' [str] unique identifier of the PMS + 'name' [str] name of the PMS + 'token' [str] token needed to access that PMS + 'ownername' [str] name of the owner of this PMS or None if + the owner itself supplied tries to connect + 'product' e.g. 'Plex Media Server' or None + 'version' e.g. '1.11.2.4772-3e...' or None + 'device': e.g. 'PC' or 'Windows' or None + 'platform': e.g. 'Windows', 'Android' or None + 'local' [bool] True if plex.tv supplied + 'publicAddressMatches'='1' + or if found using Plex GDM in the local LAN + 'owned' [bool] True if it's the owner's PMS + 'relay' [bool] True if plex.tv supplied 'relay'='1' + 'presence' [bool] True if plex.tv supplied 'presence'='1' + 'httpsRequired' [bool] True if plex.tv supplied + 'httpsRequired'='1' + 'scheme' [str] either 'http' or 'https' + 'ip': [str] IP of the PMS, e.g. '192.168.1.1' + 'port': [str] Port of the PMS, e.g. '32400' + 'baseURL': [str] ://: of the PMS + } + """ + LOG.info('Start discovery of Plex Media Servers') + # Look first for local PMS in the LAN + local_pms_list = _plex_gdm() + LOG.debug('PMS found in the local LAN using Plex GDM: %s', local_pms_list) + # Get PMS from plex.tv + if token: + LOG.info('Checking with plex.tv for more PMS to connect to') + plex_pms_list = _pms_list_from_plex_tv(token) + LOG.debug('PMS found on plex.tv: %s', plex_pms_list) + else: + LOG.info('No plex token supplied, only checked LAN for available PMS') + plex_pms_list = [] + + # See if we found a PMS both locally and using plex.tv. If so, use local + # connection data + all_pms = [] + for pms in local_pms_list: + for i, plex_pms in enumerate(plex_pms_list): + if pms['machineIdentifier'] == plex_pms['machineIdentifier']: + # Update with GDM data - potentially more reliable than plex.tv + LOG.debug('Found this PMS also in the LAN: %s', plex_pms) + plex_pms['ip'] = pms['ip'] + plex_pms['port'] = pms['port'] + plex_pms['local'] = True + # Use all the other data we know from plex.tv + pms = plex_pms + # Remove this particular pms since we already know it + plex_pms_list.pop(i) + break + https = _pms_https_enabled('%s:%s' % (pms['ip'], pms['port'])) + if https is None: + # Error contacting url. Skip and ignore this PMS for now + continue + elif https is True: + pms['scheme'] = 'https' + pms['baseURL'] = 'https://%s:%s' % (pms['ip'], pms['port']) + else: + pms['scheme'] = 'http' + pms['baseURL'] = 'http://%s:%s' % (pms['ip'], pms['port']) + all_pms.append(pms) + # Now add the remaining PMS from plex.tv (where we already checked connect.) + for plex_pms in plex_pms_list: + all_pms.append(plex_pms) + LOG.debug('Found the following PMS in total: %s', all_pms) + return all_pms + + +def _plex_gdm(): + """ + PlexGDM - looks for PMS in the local LAN and returns a list of the PMS found + """ + # Import here because we might not need to do gdm because we already + # connected to a PMS successfully in the past + import struct + import socket + + # setup socket for discovery -> multicast message + gdm = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + gdm.settimeout(2.0) + # Set the time-to-live for messages to 2 for local network + ttl = struct.pack('b', 2) + gdm.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + + return_data = [] + try: + # Send data to the multicast group + gdm.sendto(PLEX_GDM_MSG, (PLEX_GDM_IP, PLEX_GDM_PORT)) + + # Look for responses from all recipients + while True: + try: + data, server = gdm.recvfrom(1024) + return_data.append({'from': server, 'data': data}) + except socket.timeout: + break + except Exception as e: + # Probably error: (101, 'Network is unreachable') + LOG.error(e) + import traceback + LOG.error("Traceback:\n%s", traceback.format_exc()) + finally: + gdm.close() + LOG.debug('Plex GDM returned the data: %s', return_data) + pms_list = [] + for response in return_data: + # Check if we had a positive HTTP response + if '200 OK' not in response['data']: + continue + pms = { + 'ip': response['from'][0], + 'scheme': None, + 'local': True, # Since we found it using GDM + 'product': None, + 'baseURL': None, + 'name': None, + 'version': None, + 'token': None, + 'ownername': None, + 'device': None, + 'platform': None, + 'owned': None, + 'relay': None, + 'presence': True, # Since we're talking to the PMS + 'httpsRequired': None, + } + for line in response['data'].split('\n'): + if 'Content-Type:' in line: + pms['product'] = tryDecode(line.split(':')[1].strip()) + elif 'Host:' in line: + pms['baseURL'] = line.split(':')[1].strip() + elif 'Name:' in line: + pms['name'] = tryDecode(line.split(':')[1].strip()) + elif 'Port:' in line: + pms['port'] = line.split(':')[1].strip() + elif 'Resource-Identifier:' in line: + pms['machineIdentifier'] = line.split(':')[1].strip() + elif 'Version:' in line: + pms['version'] = line.split(':')[1].strip() + pms_list.append(pms) + return pms_list + + +def _pms_list_from_plex_tv(token): + """ + get Plex media Server List from plex.tv/pms/resources + """ + xml = DU().downloadUrl('https://plex.tv/api/resources', + authenticate=False, + parameters={'includeHttps': 1}, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except AttributeError: + LOG.error('Could not get list of PMS from plex.tv') + return + + from Queue import Queue + queue = Queue() + thread_queue = [] + + max_age_in_seconds = 2*60*60*24 + for device in xml.findall('Device'): + if 'server' not in device.get('provides'): + # No PMS - skip + continue + if device.find('Connection') is None: + # no valid connection - skip + continue + # check MyPlex data age - skip if >2 days + info_age = time() - int(device.get('lastSeenAt')) + if info_age > max_age_in_seconds: + LOG.debug("Skip server %s not seen for 2 days", device.get('name')) + continue + pms = { + 'machineIdentifier': device.get('clientIdentifier'), + 'name': device.get('name'), + 'token': device.get('accessToken'), + 'ownername': device.get('sourceTitle'), + 'product': device.get('product'), # e.g. 'Plex Media Server' + 'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e...' + 'device': device.get('device'), # e.g. 'PC' or 'Windows' + 'platform': device.get('platform'), # e.g. 'Windows', 'Android' + 'local': device.get('publicAddressMatches') == '1', + 'owned': device.get('owned') == '1', + 'relay': device.get('relay') == '1', + 'presence': device.get('presence') == '1', + 'httpsRequired': device.get('httpsRequired') == '1', + 'connections': [] + } + # Try a local connection first, no matter what plex.tv tells us + for connection in device.findall('Connection'): + if connection.get('local') == '1': + pms['connections'].append(connection) + # Then try non-local + for connection in device.findall('Connection'): + if connection.get('local') != '1': + pms['connections'].append(connection) + # Spawn threads to ping each PMS simultaneously + thread = Thread(target=_poke_pms, args=(pms, queue)) + thread_queue.append(thread) + + max_threads = 5 + threads = [] + # poke PMS, own thread for each PMS + while True: + # Remove finished threads + for thread in threads: + if not thread.isAlive(): + threads.remove(thread) + if len(threads) < max_threads: + try: + thread = thread_queue.pop() + except IndexError: + # We have done our work + break + else: + thread.start() + threads.append(thread) + else: + sleep(50) + # wait for requests being answered + for thread in threads: + thread.join() + # declare new PMSs + pms_list = [] + while not queue.empty(): + pms = queue.get() + del pms['connections'] + pms_list.append(pms) + queue.task_done() + return pms_list + + +def _poke_pms(pms, queue): + data = pms['connections'][0].attrib + if data['local'] == '1': + protocol = data['protocol'] + address = data['address'] + port = data['port'] + url = '%s://%s:%s' % (protocol, address, port) + else: + url = data['uri'] + if url.count(':') == 1: + url = '%s:%s' % (url, data['port']) + protocol, address, port = url.split(':', 2) + address = address.replace('/', '') + xml = DU().downloadUrl('%s/identity' % url, + authenticate=False, + headerOptions={'X-Plex-Token': pms['token']}, + verifySSL=False, + timeout=10) + try: + xml.attrib['machineIdentifier'] + except (AttributeError, KeyError): + # No connection, delete the one we just tested + del pms['connections'][0] + if pms['connections']: + # Still got connections left, try them + return _poke_pms(pms, queue) + return + else: + # Connection successful - correct pms? + if xml.get('machineIdentifier') == pms['machineIdentifier']: + # process later + pms['baseURL'] = url + pms['protocol'] = protocol + pms['ip'] = address + pms['port'] = port + queue.put(pms) + return + LOG.info('Found a pms at %s, but the expected machineIdentifier of ' + '%s did not match the one we found: %s', + url, pms['uuid'], xml.get('machineIdentifier')) + + def GetPlexMetadata(key): """ Returns raw API metadata for key as an etree XML. @@ -102,7 +480,7 @@ def GetPlexMetadata(key): # 'includeConcerts': 1 } url = url + '?' + urlencode(arguments) - xml = downloadutils.DownloadUtils().downloadUrl(url) + xml = DU().downloadUrl(url) if xml == 401: # Either unauthorized (taken care of by doUtils) or PMS under strain return 401 @@ -111,7 +489,7 @@ def GetPlexMetadata(key): xml.attrib # Nope we did not receive a valid XML except AttributeError: - log.error("Error retrieving metadata for %s" % url) + LOG.error("Error retrieving metadata for %s", url) xml = None return xml @@ -153,22 +531,21 @@ def DownloadChunks(url): """ xml = None pos = 0 - errorCounter = 0 - while errorCounter < 10: + error_counter = 0 + while error_counter < 10: args = { 'X-Plex-Container-Size': CONTAINERSIZE, 'X-Plex-Container-Start': pos } - xmlpart = downloadutils.DownloadUtils().downloadUrl( - url + urlencode(args)) + xmlpart = DU().downloadUrl(url + urlencode(args)) # If something went wrong - skip in the hope that it works next time try: xmlpart.attrib except AttributeError: - log.error('Error while downloading chunks: %s' - % (url + urlencode(args))) + LOG.error('Error while downloading chunks: %s', + url + urlencode(args)) pos += CONTAINERSIZE - errorCounter += 1 + error_counter += 1 continue # Very first run: starting xml (to retain data in xml's root!) @@ -186,8 +563,8 @@ def DownloadChunks(url): if len(xmlpart) < CONTAINERSIZE: break pos += CONTAINERSIZE - if errorCounter == 10: - log.error('Fatal error while downloading chunks for %s' % url) + if error_counter == 10: + LOG.error('Fatal error while downloading chunks for %s', url) return None return xml @@ -235,8 +612,7 @@ def get_plex_sections(): """ Returns all Plex sections (libraries) of the PMS as an etree xml """ - return downloadutils.DownloadUtils().downloadUrl( - '{server}/library/sections') + return DU().downloadUrl('{server}/library/sections') def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie', @@ -255,17 +631,16 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie', } if trailers is True: args['extrasPrefixCount'] = settings('trailerNumber') - xml = downloadutils.DownloadUtils().downloadUrl( - url + '?' + urlencode(args), action_type="POST") + xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST") try: xml[0].tag except (IndexError, TypeError, AttributeError): - log.error("Error retrieving metadata for %s" % url) + LOG.error("Error retrieving metadata for %s", url) return None return xml -def PMSHttpsEnabled(url): +def _pms_https_enabled(url): """ Returns True if the PMS can talk https, False otherwise. None if error occured, e.g. the connection timed out @@ -277,21 +652,20 @@ def PMSHttpsEnabled(url): Prefers HTTPS over HTTP """ - doUtils = downloadutils.DownloadUtils().downloadUrl - res = doUtils('https://%s/identity' % url, - authenticate=False, - verifySSL=False) + res = DU().downloadUrl('https://%s/identity' % url, + authenticate=False, + verifySSL=False) try: res.attrib except AttributeError: # Might have SSL deactivated. Try with http - res = doUtils('http://%s/identity' % url, - authenticate=False, - verifySSL=False) + res = DU().downloadUrl('http://%s/identity' % url, + authenticate=False, + verifySSL=False) try: res.attrib except AttributeError: - log.error("Could not contact PMS %s" % url) + LOG.error("Could not contact PMS %s", url) return None else: # Received a valid XML. Server wants to talk HTTP @@ -307,17 +681,17 @@ def GetMachineIdentifier(url): Returns None if something went wrong """ - xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url, - authenticate=False, - verifySSL=False, - timeout=10) + xml = DU().downloadUrl('%s/identity' % url, + authenticate=False, + verifySSL=False, + timeout=10) try: machineIdentifier = xml.attrib['machineIdentifier'] except (AttributeError, KeyError): - log.error('Could not get the PMS machineIdentifier for %s' % url) + LOG.error('Could not get the PMS machineIdentifier for %s', url) return None - log.debug('Found machineIdentifier %s for the PMS %s' - % (machineIdentifier, url)) + LOG.debug('Found machineIdentifier %s for the PMS %s', + machineIdentifier, url) return machineIdentifier @@ -337,9 +711,8 @@ def GetPMSStatus(token): or an empty dict. """ answer = {} - xml = downloadutils.DownloadUtils().downloadUrl( - '{server}/status/sessions', - headerOptions={'X-Plex-Token': token}) + xml = DU().downloadUrl('{server}/status/sessions', + headerOptions={'X-Plex-Token': token}) try: xml.attrib except AttributeError: @@ -377,8 +750,8 @@ def scrobble(ratingKey, state): url = "{server}/:/unscrobble?" + urlencode(args) else: return - downloadutils.DownloadUtils().downloadUrl(url) - log.info("Toggled watched state for Plex item %s" % ratingKey) + DU().downloadUrl(url) + LOG.info("Toggled watched state for Plex item %s", ratingKey) def delete_item_from_pms(plexid): @@ -388,24 +761,76 @@ def delete_item_from_pms(plexid): Returns True if successful, False otherwise """ - if downloadutils.DownloadUtils().downloadUrl( - '{server}/library/metadata/%s' % plexid, - action_type="DELETE") is True: - log.info('Successfully deleted Plex id %s from the PMS' % plexid) + if DU().downloadUrl('{server}/library/metadata/%s' % plexid, + action_type="DELETE") is True: + LOG.info('Successfully deleted Plex id %s from the PMS', plexid) return True - else: - log.error('Could not delete Plex id %s from the PMS' % plexid) - return False + LOG.error('Could not delete Plex id %s from the PMS', plexid) + return False def get_PMS_settings(url, token): """ - Retrieve the PMS' settings via /:/ + Retrieve the PMS' settings via /:/prefs Call with url: scheme://ip:port """ - return downloadutils.DownloadUtils().downloadUrl( + return DU().downloadUrl( '%s/:/prefs' % url, authenticate=False, verifySSL=False, headerOptions={'X-Plex-Token': token} if token else None) + + +def GetUserArtworkURL(username): + """ + Returns the URL for the user's Avatar. Or False if something went + wrong. + """ + users = plex_tv.list_home_users(settings('plexToken')) + url = '' + # If an error is encountered, set to False + if not users: + LOG.info("Couldnt get user from plex.tv. No URL for user avatar") + return False + for user in users: + if username in user['title']: + url = user['thumb'] + LOG.debug("Avatar url for user %s is: %s", username, url) + return url + + +def transcode_image_path(key, AuthToken, path, width, height): + """ + Transcode Image support + + parameters: + key + AuthToken + path - source path of current XML: path[srcXML] + width + height + result: + final path to image file + """ + # external address - can we get a transcoding request for external images? + if key.startswith('http://') or key.startswith('https://'): + path = key + elif key.startswith('/'): # internal full path. + path = 'http://127.0.0.1:32400' + key + else: # internal path, add-on + path = 'http://127.0.0.1:32400' + path + '/' + key + path = tryEncode(path) + # This is bogus (note the extra path component) but ATV is stupid when it + # comes to caching images, it doesn't use querystrings. Fortunately PMS is + # lenient... + transcode_path = ('/photo/:/transcode/%sx%s/%s' + % (width, height, quote_plus(path))) + args = { + 'width': width, + 'height': height, + 'url': path + } + if AuthToken: + args['X-Plex-Token'] = AuthToken + return transcode_path + '?' + urlencode(args) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index fe669e58..4268f622 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -40,7 +40,7 @@ def chooseServer(): import initialsetup setup = initialsetup.InitialSetup() - server = setup.PickPMS(showDialog=True) + server = setup.pick_pms(showDialog=True) if server is None: log.error('We did not connect to a new PMS, aborting') plex_command('SUSPEND_USER_CLIENT', 'False') @@ -48,7 +48,7 @@ def chooseServer(): return log.info("User chose server %s" % server['name']) - setup.WritePMStoSettings(server) + setup.write_pms_to_settings(server) if not __LogOut(): return @@ -87,7 +87,7 @@ def togglePlexTV(): else: log.info('Login to plex.tv') import initialsetup - initialsetup.InitialSetup().PlexTVSignIn() + initialsetup.InitialSetup().plex_tv_sign_in() dialog('notification', lang(29999), lang(39221), diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index b5e023f6..949af967 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -4,17 +4,16 @@ from logging import getLogger from Queue import Queue import xml.etree.ElementTree as etree -import xbmc -import xbmcgui +from xbmc import executebuiltin, translatePath from utils import settings, window, language as lang, tryEncode, tryDecode, \ - XmlKodiSetting, reboot_kodi + XmlKodiSetting, reboot_kodi, dialog from migration import check_migration from downloadutils import DownloadUtils as DU from userclient import UserClient from clientinfo import getDeviceId -from PlexAPI import PlexAPI -from PlexFunctions import GetMachineIdentifier, get_PMS_settings +import PlexFunctions as PF +import plex_tv from json_rpc import get_setting, set_setting import playqueue as PQ from videonodes import VideoNodes @@ -77,7 +76,7 @@ def reload_pkc(): set_webserver() # To detect Kodi profile switches window('plex_kodiProfile', - value=tryDecode(xbmc.translatePath("special://profile"))) + value=tryDecode(translatePath("special://profile"))) getDeviceId() # Initialize the PKC playqueues PQ.init_playqueues() @@ -115,48 +114,67 @@ def set_webserver(): state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword') -class InitialSetup(): +def _write_pms_settings(url, token): + """ + Sets certain settings for server by asking for the PMS' settings + Call with url: scheme://ip:port + """ + xml = PF.get_PMS_settings(url, token) + try: + xml.attrib + except AttributeError: + LOG.error('Could not get PMS settings for %s', url) + return + for entry in xml: + if entry.attrib.get('id', '') == 'allowMediaDeletion': + settings('plex_allows_mediaDeletion', + value=entry.attrib.get('value', 'true')) + window('plex_allows_mediaDeletion', + value=entry.attrib.get('value', 'true')) + +class InitialSetup(object): + """ + Will load Plex PMS settings (e.g. address) and token + Will ask the user initial questions on first PKC boot + """ def __init__(self): LOG.debug('Entering initialsetup class') - self.plx = PlexAPI() - self.dialog = xbmcgui.Dialog() - self.server = UserClient().getServer() self.serverid = settings('plex_machineIdentifier') # Get Plex credentials from settings file, if they exist - plexdict = self.plx.GetPlexLoginFromSettings() + plexdict = PF.GetPlexLoginFromSettings() self.myplexlogin = plexdict['myplexlogin'] == 'true' - self.plexLogin = plexdict['plexLogin'] - self.plexToken = plexdict['plexToken'] + self.plex_login = plexdict['plexLogin'] + self.plex_token = plexdict['plexToken'] self.plexid = plexdict['plexid'] # Token for the PMS, not plex.tv self.pms_token = settings('accessToken') - if self.plexToken: + if self.plex_token: LOG.debug('Found a plex.tv token in the settings') - def PlexTVSignIn(self): + def plex_tv_sign_in(self): """ Signs (freshly) in to plex.tv (will be saved to file settings) Returns True if successful, or False if not """ - result = self.plx.PlexTvSignInWithPin() + result = plex_tv.sign_in_with_pin() if result: - self.plexLogin = result['username'] - self.plexToken = result['token'] + self.plex_login = result['username'] + self.plex_token = result['token'] self.plexid = result['plexid'] return True return False - def CheckPlexTVSignIn(self): + def check_plex_tv_sign_in(self): """ Checks existing connection to plex.tv. If not, triggers sign in Returns True if signed in, False otherwise """ answer = True - chk = self.plx.CheckConnection('plex.tv', token=self.plexToken) + chk = PF.check_connection('plex.tv', token=self.plex_token) if chk in (401, 403): # HTTP Error: unauthorized. Token is no longer valid LOG.info('plex.tv connection returned HTTP %s', str(chk)) @@ -164,13 +182,13 @@ class InitialSetup(): settings('plexToken', value='') settings('plexLogin', value='') # Could not login, please try again - self.dialog.ok(lang(29999), lang(39009)) - answer = self.PlexTVSignIn() + dialog('ok', lang(29999), lang(39009)) + answer = self.plex_tv_sign_in() elif chk is False or chk >= 400: # Problems connecting to plex.tv. Network or internet issue? LOG.info('Problems connecting to plex.tv; connection returned ' 'HTTP %s', str(chk)) - self.dialog.ok(lang(29999), lang(39010)) + dialog('ok', lang(29999), lang(39010)) answer = False else: LOG.info('plex.tv connection with token successful') @@ -178,13 +196,13 @@ class InitialSetup(): # Refresh the info from Plex.tv xml = DU().downloadUrl('https://plex.tv/users/account', authenticate=False, - headerOptions={'X-Plex-Token': self.plexToken}) + headerOptions={'X-Plex-Token': self.plex_token}) try: - self.plexLogin = xml.attrib['title'] + self.plex_login = xml.attrib['title'] except (AttributeError, KeyError): LOG.error('Failed to update Plex info from plex.tv') else: - settings('plexLogin', value=self.plexLogin) + settings('plexLogin', value=self.plex_login) home = 'true' if xml.attrib.get('home') == '1' else 'false' settings('plexhome', value=home) settings('plexAvatar', value=xml.attrib.get('thumb')) @@ -192,7 +210,7 @@ class InitialSetup(): LOG.info('Updated Plex info from plex.tv') return answer - def CheckPMS(self): + def check_existing_pms(self): """ Check the PMS that was set in file settings. Will return False if we need to reconnect, because: @@ -203,80 +221,80 @@ class InitialSetup(): not set before """ answer = True - chk = self.plx.CheckConnection(self.server, verifySSL=False) + chk = PF.check_connection(self.server, verifySSL=False) if chk is False: LOG.warn('Could not reach PMS %s', self.server) answer = False if answer is True and not self.serverid: LOG.info('No PMS machineIdentifier found for %s. Trying to ' 'get the PMS unique ID', self.server) - self.serverid = GetMachineIdentifier(self.server) + self.serverid = PF.GetMachineIdentifier(self.server) if self.serverid is None: LOG.warn('Could not retrieve machineIdentifier') answer = False else: settings('plex_machineIdentifier', value=self.serverid) elif answer is True: - tempServerid = GetMachineIdentifier(self.server) - if tempServerid != self.serverid: + temp_server_id = PF.GetMachineIdentifier(self.server) + if temp_server_id != self.serverid: LOG.warn('The current PMS %s was expected to have a ' 'unique machineIdentifier of %s. But we got ' '%s. Pick a new server to be sure', - self.server, self.serverid, tempServerid) + self.server, self.serverid, temp_server_id) answer = False return answer - def _getServerList(self): + @staticmethod + def _check_pms_connectivity(server): """ - Returns a list of servers from GDM and possibly plex.tv - """ - self.plx.discoverPMS(xbmc.getIPAddress(), - plexToken=self.plexToken) - serverlist = self.plx.returnServerList(self.plx.g_PMS) - LOG.debug('PMS serverlist: %s', serverlist) - return serverlist - - def _checkServerCon(self, server): - """ - Checks for server's connectivity. Returns CheckConnection result + Checks for server's connectivity. Returns check_connection result """ # Re-direct via plex if remote - will lead to the correct SSL # certificate - if server['local'] == '1': - url = '%s://%s:%s' \ - % (server['scheme'], server['ip'], server['port']) + if server['local']: + url = ('%s://%s:%s' + % (server['scheme'], server['ip'], server['port'])) # Deactive SSL verification if the server is local! verifySSL = False else: url = server['baseURL'] verifySSL = True - chk = self.plx.CheckConnection(url, - token=server['accesstoken'], - verifySSL=verifySSL) + chk = PF.check_connection(url, + token=server['token'], + verifySSL=verifySSL) return chk - def PickPMS(self, showDialog=False): + def pick_pms(self, showDialog=False): """ - Searches for PMS in local Lan and optionally (if self.plexToken set) + Searches for PMS in local Lan and optionally (if self.plex_token set) also on plex.tv showDialog=True: let the user pick one showDialog=False: automatically pick PMS based on machineIdentifier Returns the picked PMS' detail as a dict: { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner + 'machineIdentifier' [str] unique identifier of the PMS + 'name' [str] name of the PMS + 'token' [str] token needed to access that PMS + 'ownername' [str] name of the owner of this PMS or None if + the owner itself supplied tries to connect + 'product' e.g. 'Plex Media Server' or None + 'version' e.g. '1.11.2.4772-3e...' or None + 'device': e.g. 'PC' or 'Windows' or None + 'platform': e.g. 'Windows', 'Android' or None + 'local' [bool] True if plex.tv supplied + 'publicAddressMatches'='1' + or if found using Plex GDM in the local LAN + 'owned' [bool] True if it's the owner's PMS + 'relay' [bool] True if plex.tv supplied 'relay'='1' + 'presence' [bool] True if plex.tv supplied 'presence'='1' + 'httpsRequired' [bool] True if plex.tv supplied + 'httpsRequired'='1' + 'scheme' [str] either 'http' or 'https' + 'ip': [str] IP of the PMS, e.g. '192.168.1.1' + 'port': [str] Port of the PMS, e.g. '32400' + 'baseURL': [str] ://: of the PMS } - or None if unsuccessful """ server = None @@ -284,44 +302,26 @@ class InitialSetup(): if not self.server or not self.serverid: showDialog = True if showDialog is True: - server = self._UserPickPMS() + server = self._user_pick_pms() else: - server = self._AutoPickPMS() + server = self._auto_pick_pms() if server is not None: - self._write_PMS_settings(server['baseURL'], server['accesstoken']) + _write_pms_settings(server['baseURL'], server['token']) return server - def _write_PMS_settings(self, url, token): - """ - Sets certain settings for server by asking for the PMS' settings - Call with url: scheme://ip:port - """ - xml = get_PMS_settings(url, token) - try: - xml.attrib - except AttributeError: - LOG.error('Could not get PMS settings for %s', url) - return - for entry in xml: - if entry.attrib.get('id', '') == 'allowMediaDeletion': - settings('plex_allows_mediaDeletion', - value=entry.attrib.get('value', 'true')) - window('plex_allows_mediaDeletion', - value=entry.attrib.get('value', 'true')) - - def _AutoPickPMS(self): + def _auto_pick_pms(self): """ Will try to pick PMS based on machineIdentifier saved in file settings but only once Returns server or None if unsuccessful """ - httpsUpdated = False - checkedPlexTV = False + https_updated = False + checked_plex_tv = False server = None while True: - if httpsUpdated is False: - serverlist = self._getServerList() + if https_updated is False: + serverlist = PF.discover_pms(self.plex_token) for item in serverlist: if item.get('machineIdentifier') == self.serverid: server = item @@ -331,26 +331,30 @@ class InitialSetup(): 'machineIdentifier of %s and name %s is ' 'offline', self.serverid, name) return - chk = self._checkServerCon(server) - if chk == 504 and httpsUpdated is False: - # Not able to use HTTP, try HTTPs for now - server['scheme'] = 'https' - httpsUpdated = True + chk = self._check_pms_connectivity(server) + if chk == 504 and https_updated is False: + # switch HTTPS to HTTP or vice-versa + if server['scheme'] == 'https': + server['scheme'] = 'http' + else: + server['scheme'] = 'https' + https_updated = True continue if chk == 401: LOG.warn('Not yet authorized for Plex server %s', server['name']) - if self.CheckPlexTVSignIn() is True: - if checkedPlexTV is False: + if self.check_plex_tv_sign_in() is True: + if checked_plex_tv is False: # Try again - checkedPlexTV = True - httpsUpdated = False + checked_plex_tv = True + https_updated = False continue else: LOG.warn('Not authorized even though we are signed ' ' in to plex.tv correctly') - self.dialog.ok(lang(29999), '%s %s' - % (lang(39214), + dialog('ok', + lang(29999), + '%s %s' % (lang(39214), tryEncode(server['name']))) return else: @@ -364,25 +368,25 @@ class InitialSetup(): server['name']) return server - def _UserPickPMS(self): + def _user_pick_pms(self): """ Lets user pick his/her PMS from a list Returns server or None if unsuccessful """ - httpsUpdated = False + https_updated = False while True: - if httpsUpdated is False: - serverlist = self._getServerList() + if https_updated is False: + serverlist = PF.discover_pms(self.plex_token) # Exit if no servers found - if len(serverlist) == 0: + if not serverlist: LOG.warn('No plex media servers found!') - self.dialog.ok(lang(29999), lang(39011)) + dialog('ok', lang(29999), lang(39011)) return # Get a nicer list dialoglist = [] for server in serverlist: - if server['local'] == '1': + if server['local']: # server is in the same network as client. # Add"local" msg = lang(39022) @@ -399,34 +403,34 @@ class InitialSetup(): dialoglist.append('%s (%s)' % (server['name'], msg)) # Let user pick server from a list - resp = self.dialog.select(lang(39012), dialoglist) + resp = dialog('select', lang(39012), dialoglist) if resp == -1: # User cancelled return server = serverlist[resp] - chk = self._checkServerCon(server) - if chk == 504 and httpsUpdated is False: + chk = self._check_pms_connectivity(server) + if chk == 504 and https_updated is False: # Not able to use HTTP, try HTTPs for now serverlist[resp]['scheme'] = 'https' - httpsUpdated = True + https_updated = True continue - httpsUpdated = False + https_updated = False if chk == 401: LOG.warn('Not yet authorized for Plex server %s', server['name']) # Please sign in to plex.tv - self.dialog.ok(lang(29999), - lang(39013) + server['name'], - lang(39014)) - if self.PlexTVSignIn() is False: + dialog('ok', + lang(29999), + lang(39013) + server['name'], + lang(39014)) + if self.plex_tv_sign_in() is False: # Exit while loop if user cancels return # Problems connecting elif chk >= 400 or chk is False: # Problems connecting to server. Pick another server? - answ = self.dialog.yesno(lang(29999), - lang(39015)) + answ = dialog('yesno', lang(29999), lang(39015)) # Exit while loop if user chooses No if not answ: return @@ -434,30 +438,16 @@ class InitialSetup(): else: return server - def WritePMStoSettings(self, server): + @staticmethod + def write_pms_to_settings(server): """ - Saves server to file settings. server is a dict of the form: - { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner - } + Saves server to file settings """ settings('plex_machineIdentifier', server['machineIdentifier']) settings('plex_servername', server['name']) - settings('plex_serverowned', - 'true' if server['owned'] == '1' - else 'false') + settings('plex_serverowned', 'true' if server['owned'] else 'false') # Careful to distinguish local from remote PMS - if server['local'] == '1': + if server['local']: scheme = server['scheme'] settings('ipaddress', server['ip']) settings('port', server['port']) @@ -491,7 +481,6 @@ class InitialSetup(): path. """ LOG.info("Initial setup called.") - dialog = self.dialog try: with XmlKodiSetting('advancedsettings.xml', force_create=True, @@ -537,11 +526,13 @@ class InitialSetup(): # Missing smb:// occurences, re-add. for _ in range(0, count): source = etree.SubElement(root, 'source') - etree.SubElement(source, - 'name').text = "PlexKodiConnect Masterlock Hack" - etree.SubElement(source, - 'path', - attrib={'pathversion': "1"}).text = "smb://" + etree.SubElement( + source, + 'name').text = "PlexKodiConnect Masterlock Hack" + etree.SubElement( + source, + 'path', + attrib={'pathversion': "1"}).text = "smb://" etree.SubElement(source, 'allowsharing').text = "true" if reboot is False: reboot = xml.write_xml @@ -555,23 +546,23 @@ class InitialSetup(): # return only if the right machine identifier is found if self.server: LOG.info("PMS is already set: %s. Checking now...", self.server) - if self.CheckPMS(): + if self.check_existing_pms(): LOG.info("Using PMS %s with machineIdentifier %s", self.server, self.serverid) - self._write_PMS_settings(self.server, self.pms_token) + _write_pms_settings(self.server, self.pms_token) if reboot is True: reboot_kodi() return # If not already retrieved myplex info, optionally let user sign in # to plex.tv. This DOES get called on very first install run - if not self.plexToken and self.myplexlogin: - self.PlexTVSignIn() + if not self.plex_token and self.myplexlogin: + self.plex_tv_sign_in() - server = self.PickPMS() + server = self.pick_pms() if server is not None: # Write our chosen server to Kodi settings file - self.WritePMStoSettings(server) + self.write_pms_to_settings(server) # User already answered the installation questions if settings('InstallQuestionsAnswered') == 'true': @@ -581,50 +572,52 @@ class InitialSetup(): # Additional settings where the user needs to choose # Direct paths (\\NAS\mymovie.mkv) or addon (http)? - goToSettings = False - if dialog.yesno(lang(29999), - lang(39027), - lang(39028), - nolabel="Addon (Default)", - yeslabel="Native (Direct Paths)"): + goto_settings = False + if dialog('yesno', + lang(29999), + lang(39027), + lang(39028), + nolabel="Addon (Default)", + yeslabel="Native (Direct Paths)"): LOG.debug("User opted to use direct paths.") settings('useDirectPaths', value="1") state.DIRECT_PATHS = True # Are you on a system where you would like to replace paths # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) - if dialog.yesno(heading=lang(29999), line1=lang(39033)): + if dialog('yesno', heading=lang(29999), line1=lang(39033)): LOG.debug("User chose to replace paths with smb") else: settings('replaceSMB', value="false") # complete replace all original Plex library paths with custom SMB - if dialog.yesno(heading=lang(29999), line1=lang(39043)): + if dialog('yesno', heading=lang(29999), line1=lang(39043)): LOG.debug("User chose custom smb paths") settings('remapSMB', value="true") # Please enter your custom smb paths in the settings under # "Sync Options" and then restart Kodi - dialog.ok(heading=lang(29999), line1=lang(39044)) - goToSettings = True + dialog('ok', heading=lang(29999), line1=lang(39044)) + goto_settings = True # Go to network credentials? - if dialog.yesno(heading=lang(29999), - line1=lang(39029), - line2=lang(39030)): + if dialog('yesno', + heading=lang(29999), + line1=lang(39029), + line2=lang(39030)): LOG.debug("Presenting network credentials dialog.") from utils import passwordsXML passwordsXML() # Disable Plex music? - if dialog.yesno(heading=lang(29999), line1=lang(39016)): + if dialog('yesno', heading=lang(29999), line1=lang(39016)): LOG.debug("User opted to disable Plex music library.") settings('enableMusic', value="false") # Download additional art from FanArtTV - if dialog.yesno(heading=lang(29999), line1=lang(39061)): + if dialog('yesno', heading=lang(29999), line1=lang(39061)): LOG.debug("User opted to use FanArtTV") settings('FanartTV', value="true") # Do you want to replace your custom user ratings with an indicator of # how many versions of a media item you posses? - if dialog.yesno(heading=lang(29999), line1=lang(39718)): + if dialog('yesno', heading=lang(29999), line1=lang(39718)): LOG.debug("User opted to replace user ratings with version number") settings('indicate_media_versions', value="true") @@ -637,12 +630,13 @@ class InitialSetup(): # Make sure that we only ask these questions upon first installation settings('InstallQuestionsAnswered', value='true') - if goToSettings is False: + if goto_settings is False: # Open Settings page now? You will need to restart! - goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) - if goToSettings: + goto_settings = dialog('yesno', + heading=lang(29999), + line1=lang(39017)) + if goto_settings: state.PMS_STATUS = 'Stop' - xbmc.executebuiltin( - 'Addon.OpenSettings(plugin.video.plexkodiconnect)') + executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') elif reboot is True: reboot_kodi() diff --git a/resources/lib/plex_tv.py b/resources/lib/plex_tv.py new file mode 100644 index 00000000..f805bee5 --- /dev/null +++ b/resources/lib/plex_tv.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +from logging import getLogger + +from xbmc import sleep, executebuiltin + +from downloadutils import DownloadUtils as DU +from utils import dialog, language as lang, settings, tryEncode +import variables as v +import state + +############################################################################### +LOG = getLogger("PLEX." + __name__) +############################################################################### + + +def choose_home_user(token): + """ + Let's user choose from a list of Plex home users. Will switch to that + user accordingly. + + Returns a dict: + { + 'username': Unicode + 'userid': '' Plex ID of the user + 'token': '' User's token + 'protected': True if PIN is needed, else False + } + + Will return False if something went wrong (wrong PIN, no connection) + """ + # Get list of Plex home users + users = list_home_users(token) + if not users: + LOG.error("User download failed.") + return False + userlist = [] + userlist_coded = [] + for user in users: + username = user['title'] + userlist.append(username) + # To take care of non-ASCII usernames + userlist_coded.append(tryEncode(username)) + usernumber = len(userlist) + username = '' + usertoken = '' + trials = 0 + while trials < 3: + if usernumber > 1: + # Select user + user_select = dialog('select', lang(29999) + lang(39306), + userlist_coded) + if user_select == -1: + LOG.info("No user selected.") + settings('username', value='') + executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + return False + # Only 1 user received, choose that one + else: + user_select = 0 + selected_user = userlist[user_select] + LOG.info("Selected user: %s", selected_user) + user = users[user_select] + # Ask for PIN, if protected: + pin = None + if user['protected'] == '1': + LOG.debug('Asking for users PIN') + pin = dialog('input', + lang(39307) + selected_user, + '', + type='{numeric}', + option='{hide}') + # User chose to cancel + # Plex bug: don't call url for protected user with empty PIN + if not pin: + trials += 1 + continue + # Switch to this Plex Home user, if applicable + result = switch_home_user(user['id'], + pin, + token, + settings('plex_machineIdentifier')) + if result: + # Successfully retrieved username: break out of while loop + username = result['username'] + usertoken = result['usertoken'] + break + # Couldn't get user auth + else: + trials += 1 + # Could not login user, please try again + if not dialog('yesno', + heading='{plex}', + line1=lang(39308) + selected_user, + line2=lang(39309)): + # User chose to cancel + break + if not username: + LOG.error('Failed signing in a user to plex.tv') + executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + return False + return { + 'username': username, + 'userid': user['id'], + 'protected': True if user['protected'] == '1' else False, + 'token': usertoken + } + + +def switch_home_user(userid, pin, token, machineIdentifier): + """ + Retrieves Plex home token for a Plex home user. + Returns False if unsuccessful + + Input: + userid id of the Plex home user + pin PIN of the Plex home user, if protected + token token for plex.tv + + Output: + { + 'username' + 'usertoken' Might be empty strings if no token found + for the machineIdentifier that was chosen + } + + settings('userid') and settings('username') with new plex token + """ + LOG.info('Switching to user %s', userid) + url = 'https://plex.tv/api/home/users/' + userid + '/switch' + if pin: + url += '?pin=' + pin + answer = DU().downloadUrl(url, + authenticate=False, + action_type="POST", + headerOptions={'X-Plex-Token': token}) + try: + answer.attrib + except AttributeError: + LOG.error('Error: plex.tv switch HomeUser change failed') + return False + + username = answer.attrib.get('title', '') + token = answer.attrib.get('authenticationToken', '') + + # Write to settings file + settings('username', username) + settings('accessToken', token) + settings('userid', answer.attrib.get('id', '')) + settings('plex_restricteduser', + 'true' if answer.attrib.get('restricted', '0') == '1' + else 'false') + state.RESTRICTED_USER = True if \ + answer.attrib.get('restricted', '0') == '1' else False + + # Get final token to the PMS we've chosen + url = 'https://plex.tv/api/resources?includeHttps=1' + xml = DU().downloadUrl(url, + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except AttributeError: + LOG.error('Answer from plex.tv not as excepted') + # Set to empty iterable list for loop + xml = [] + + found = 0 + LOG.debug('Our machineIdentifier is %s', machineIdentifier) + for device in xml: + identifier = device.attrib.get('clientIdentifier') + LOG.debug('Found a Plex machineIdentifier: %s', identifier) + if identifier == machineIdentifier: + found += 1 + token = device.attrib.get('accessToken') + + result = { + 'username': username, + } + if found == 0: + LOG.info('No tokens found for your server! Using empty string') + result['usertoken'] = '' + else: + result['usertoken'] = token + LOG.info('Plex.tv switch HomeUser change successfull for user %s', + username) + return result + + +def list_home_users(token): + """ + Returns a list for myPlex home users for the current plex.tv account. + + Input: + token for plex.tv + Output: + List of users, where one entry is of the form: + "id": userId, + "admin": '1'/'0', + "guest": '1'/'0', + "restricted": '1'/'0', + "protected": '1'/'0', + "email": email, + "title": title, + "username": username, + "thumb": thumb_url + } + If any value is missing, None is returned instead (or "" from plex.tv) + If an error is encountered, False is returned + """ + xml = DU().downloadUrl('https://plex.tv/api/home/users/', + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except AttributeError: + LOG.error('Download of Plex home users failed.') + return False + users = [] + for user in xml: + users.append(user.attrib) + return users + + +def sign_in_with_pin(): + """ + Prompts user to sign in by visiting https://plex.tv/pin + + Writes to Kodi settings file. Also returns: + { + 'plexhome': 'true' if Plex Home, 'false' otherwise + 'username': + 'avatar': URL to user avator + 'token': + 'plexid': Plex user ID + 'homesize': Number of Plex home users (defaults to '1') + } + Returns False if authentication did not work. + """ + code, identifier = get_pin() + if not code: + # Problems trying to contact plex.tv. Try again later + dialog('ok', heading='{plex}', line1=lang(39303)) + return False + # Go to https://plex.tv/pin and enter the code: + # Or press No to cancel the sign in. + answer = dialog('yesno', + heading='{plex}', + line1=lang(39304) + "\n\n", + line2=code + "\n\n", + line3=lang(39311)) + if not answer: + return False + count = 0 + # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) + while count < 30: + xml = check_pin(identifier) + if xml is not False: + break + # Wait for 1 seconds + sleep(1000) + count += 1 + if xml is False: + # Could not sign in to plex.tv Try again later + dialog('ok', heading='{plex}', line1=lang(39305)) + return False + # Parse xml + userid = xml.attrib.get('id') + home = xml.get('home', '0') + if home == '1': + home = 'true' + else: + home = 'false' + username = xml.get('username', '') + avatar = xml.get('thumb', '') + token = xml.findtext('authentication-token') + home_size = xml.get('homeSize', '1') + result = { + 'plexhome': home, + 'username': username, + 'avatar': avatar, + 'token': token, + 'plexid': userid, + 'homesize': home_size + } + settings('plexLogin', username) + settings('plexToken', token) + settings('plexhome', home) + settings('plexid', userid) + settings('plexAvatar', avatar) + settings('plexHomeSize', home_size) + # Let Kodi log into plex.tv on startup from now on + settings('myplexlogin', 'true') + settings('plex_status', value=lang(39227)) + return result + + +def get_pin(): + """ + For plex.tv sign-in: returns 4-digit code and identifier as 2 str + """ + code = None + identifier = None + # Download + xml = DU().downloadUrl('https://plex.tv/pins.xml', + authenticate=False, + action_type="POST") + try: + xml.attrib + except AttributeError: + LOG.error("Error, no PIN from plex.tv provided") + return None, None + code = xml.find('code').text + identifier = xml.find('id').text + LOG.info('Successfully retrieved code and id from plex.tv') + return code, identifier + + +def check_pin(identifier): + """ + Checks with plex.tv whether user entered the correct PIN on plex.tv/pin + + Returns False if not yet done so, or the XML response file as etree + """ + # Try to get a temporary token + xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier, + authenticate=False) + try: + temp_token = xml.find('auth_token').text + except AttributeError: + LOG.error("Could not find token in plex.tv answer") + return False + if not temp_token: + return False + # Use temp token to get the final plex credentials + xml = DU().downloadUrl('https://plex.tv/users/account', + authenticate=False, + parameters={'X-Plex-Token': temp_token}) + return xml diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 9e94598a..69af21f0 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -1,25 +1,21 @@ # -*- coding: utf-8 -*- - ############################################################################### from logging import getLogger from threading import Thread -import xbmc -import xbmcgui +from xbmc import sleep, executebuiltin, translatePath import xbmcaddon from xbmcvfs import exists - -from utils import window, settings, language as lang, thread_methods -import downloadutils - -import PlexAPI -from PlexFunctions import GetMachineIdentifier +from utils import window, settings, language as lang, thread_methods, dialog +from downloadutils import DownloadUtils as DU +import plex_tv +import PlexFunctions as PF import state ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -45,7 +41,7 @@ class UserClient(Thread): self.userSettings = None self.addon = xbmcaddon.Addon() - self.doUtils = downloadutils.DownloadUtils() + self.doUtils = DU() Thread.__init__(self) @@ -55,10 +51,10 @@ class UserClient(Thread): """ username = settings('username') if not username: - log.debug("No username saved, trying to get Plex username") + LOG.debug("No username saved, trying to get Plex username") username = settings('plexLogin') if not username: - log.debug("Also no Plex username found") + LOG.debug("Also no Plex username found") return "" return username @@ -73,7 +69,7 @@ class UserClient(Thread): server = host + ":" + port if not host: - log.debug("No server information saved.") + LOG.debug("No server information saved.") return False # If https is true @@ -84,11 +80,11 @@ class UserClient(Thread): server = "http://%s" % server # User entered IP; we need to get the machineIdentifier if self.machineIdentifier == '' and prefix is True: - self.machineIdentifier = GetMachineIdentifier(server) + self.machineIdentifier = PF.GetMachineIdentifier(server) if self.machineIdentifier is None: self.machineIdentifier = '' settings('plex_machineIdentifier', value=self.machineIdentifier) - log.debug('Returning active server: %s' % server) + LOG.debug('Returning active server: %s', server) return server def getSSLverify(self): @@ -101,10 +97,10 @@ class UserClient(Thread): else settings('sslcert') def setUserPref(self): - log.debug('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) + url = PF.GetUserArtworkURL(self.currUser) if url: window('PlexUserImage', value=url) # Set resume point max @@ -116,7 +112,7 @@ class UserClient(Thread): return True def loadCurrUser(self, username, userId, usertoken, authenticated=False): - log.debug('Loading current user') + LOG.debug('Loading current user') doUtils = self.doUtils self.currToken = usertoken @@ -127,18 +123,18 @@ class UserClient(Thread): if authenticated is False: if self.currServer is None: return False - log.debug('Testing validity of current token') - res = PlexAPI.PlexAPI().CheckConnection(self.currServer, - token=self.currToken, - verifySSL=self.ssl) + LOG.debug('Testing validity of current token') + res = PF.check_connection(self.currServer, + token=self.currToken, + verifySSL=self.ssl) if res is False: # PMS probably offline return False elif res == 401: - log.error('Token is no longer valid') + LOG.error('Token is no longer valid') return 401 elif res >= 400: - log.error('Answer from PMS is not as expected. Retrying') + LOG.error('Answer from PMS is not as expected. Retrying') return False # Set to windows property @@ -182,31 +178,29 @@ class UserClient(Thread): return True def authenticate(self): - log.debug('Authenticating user') - dialog = xbmcgui.Dialog() + LOG.debug('Authenticating user') # Give attempts at entering password / selecting user if self.retry >= 2: - log.error("Too many retries to login.") + LOG.error("Too many retries to login.") state.PMS_STATUS = 'Stop' - dialog.ok(lang(33001), - lang(39023)) - xbmc.executebuiltin( + dialog('ok', lang(33001), lang(39023)) + executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') return False # Get /profile/addon_data - addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')) + addondir = translatePath(self.addon.getAddonInfo('profile')) # If there's no settings.xml if not exists("%ssettings.xml" % addondir): - log.error("Error, no settings.xml found.") + LOG.error("Error, no settings.xml found.") self.auth = False return False server = self.getServer() # If there is no server we can connect to if not server: - log.info("Missing server information.") + LOG.info("Missing server information.") self.auth = False return False @@ -217,7 +211,7 @@ class UserClient(Thread): enforceLogin = settings('enforceUserLogin') # Found a user in the settings, try to authenticate if username and enforceLogin == 'false': - log.debug('Trying to authenticate with old settings') + LOG.debug('Trying to authenticate with old settings') answ = self.loadCurrUser(username, userId, usertoken, @@ -226,21 +220,19 @@ class UserClient(Thread): # SUCCESS: loaded a user from the settings return True elif answ == 401: - log.error("User token no longer valid. Sign user out") + 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") + LOG.debug("Could not yet authenticate user") return False - plx = PlexAPI.PlexAPI() - # Could not use settings - try to get Plex user list from plex.tv plextoken = settings('plexToken') if plextoken: - log.info("Trying to connect to plex.tv to get a user list") - userInfo = plx.ChoosePlexHomeUser(plextoken) + LOG.info("Trying to connect to plex.tv to get a user list") + userInfo = plex_tv.choose_home_user(plextoken) if userInfo is False: # FAILURE: Something went wrong, try again self.auth = True @@ -250,7 +242,7 @@ class UserClient(Thread): userId = userInfo['userid'] usertoken = userInfo['token'] else: - log.info("Trying to authenticate without a token") + LOG.info("Trying to authenticate without a token") username = '' userId = '' usertoken = '' @@ -258,14 +250,13 @@ class UserClient(Thread): if self.loadCurrUser(username, userId, usertoken, authenticated=False): # SUCCESS: loaded a user from the settings return True - else: - # FAILUR: Something went wrong, try again - self.auth = True - self.retry += 1 - return False + # Something went wrong, try again + self.auth = True + self.retry += 1 + return False def resetClient(self): - log.debug("Reset UserClient authentication.") + LOG.debug("Reset UserClient authentication.") self.doUtils.stopSession() window('plex_authenticated', clear=True) @@ -295,17 +286,17 @@ class UserClient(Thread): self.retry = 0 def run(self): - log.info("----===## Starting UserClient ##===----") + LOG.info("----===## Starting UserClient ##===----") thread_stopped = self.thread_stopped thread_suspended = self.thread_suspended while not thread_stopped(): while thread_suspended(): if thread_stopped(): break - xbmc.sleep(1000) + sleep(1000) if state.PMS_STATUS == "Stop": - xbmc.sleep(500) + sleep(500) continue # Verify the connection status to server @@ -318,7 +309,7 @@ class UserClient(Thread): state.PMS_STATUS = 'Auth' window('plex_serverStatus', value='Auth') self.resetClient() - xbmc.sleep(3000) + sleep(3000) if self.auth and (self.currUser is None): # Try to authenticate user @@ -328,9 +319,9 @@ class UserClient(Thread): self.auth = False if self.authenticate(): # Successfully authenticated and loaded a user - log.info("Successfully authenticated!") - log.info("Current user: %s" % self.currUser) - log.info("Current userId: %s" % state.PLEX_USER_ID) + LOG.info("Successfully authenticated!") + LOG.info("Current user: %s", self.currUser) + LOG.info("Current userId: %s", state.PLEX_USER_ID) self.retry = 0 state.SUSPEND_LIBRARY_THREAD = False window('plex_serverStatus', clear=True) @@ -344,10 +335,10 @@ class UserClient(Thread): # Or retried too many times if server and state.PMS_STATUS != "Stop": # Only if there's information found to login - log.debug("Server found: %s" % server) + LOG.debug("Server found: %s", server) self.auth = True # Minimize CPU load - xbmc.sleep(100) + sleep(100) - log.info("##===---- UserClient Stopped ----===##") + LOG.info("##===---- UserClient Stopped ----===##") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index e5988c8d..6ff5f68b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -129,22 +129,19 @@ def dialog(typus, *args, **kwargs): """ Displays xbmcgui Dialog. Pass a string as typus: 'yesno', 'ok', 'notification', 'input', 'select', 'numeric' - kwargs: heading='{plex}' title bar (here PlexKodiConnect) - message=lang(30128), Actual dialog content. Don't use with OK + message=lang(30128), Dialog content. Don't use with 'OK', 'yesno' line1=str(), For 'OK' and 'yesno' dialogs use line1...line3! time=5000, sound=True, nolabel=str(), For 'yesno' dialogs yeslabel=str(), For 'yesno' dialogs - Icons: icon='{plex}' Display Plex standard icon icon='{info}' xbmcgui.NOTIFICATION_INFO icon='{warning}' xbmcgui.NOTIFICATION_WARNING icon='{error}' xbmcgui.NOTIFICATION_ERROR - Input Types: type='{alphanum}' xbmcgui.INPUT_ALPHANUM (standard keyboard) type='{numeric}' xbmcgui.INPUT_NUMERIC (format: #) @@ -153,9 +150,12 @@ def dialog(typus, *args, **kwargs): type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#) type='{password}' xbmcgui.INPUT_PASSWORD (return md5 hash of input, input is masked) + Options: + option='{password}' xbmcgui.PASSWORD_VERIFY (verifies an existing + (default) md5 hashed password) + option='{hide}' xbmcgui.ALPHANUM_HIDE_INPUT (masks input) """ - d = xbmcgui.Dialog() - if "icon" in kwargs: + if 'icon' in kwargs: types = { '{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png', '{info}': xbmcgui.NOTIFICATION_INFO, @@ -174,16 +174,23 @@ def dialog(typus, *args, **kwargs): '{password}': xbmcgui.INPUT_PASSWORD } kwargs['type'] = types[kwargs['type']] - if "heading" in kwargs: + if 'option' in kwargs: + types = { + '{password}': xbmcgui.PASSWORD_VERIFY, + '{hide}': xbmcgui.ALPHANUM_HIDE_INPUT + } + kwargs['option'] = types[kwargs['option']] + if 'heading' in kwargs: kwargs['heading'] = kwargs['heading'].replace("{plex}", language(29999)) + dia = xbmcgui.Dialog() types = { - 'yesno': d.yesno, - 'ok': d.ok, - 'notification': d.notification, - 'input': d.input, - 'select': d.select, - 'numeric': d.numeric + 'yesno': dia.yesno, + 'ok': dia.ok, + 'notification': dia.notification, + 'input': dia.input, + 'select': dia.select, + 'numeric': dia.numeric } return types[typus](*args, **kwargs) diff --git a/service.py b/service.py index ab293a43..792a15c2 100644 --- a/service.py +++ b/service.py @@ -35,7 +35,7 @@ from kodimonitor import KodiMonitor, SpecialMonitor from librarysync import LibrarySync from websocket_client import PMS_Websocket, Alexa_Websocket -import PlexAPI +from PlexFunctions import check_connection from PlexCompanion import PlexCompanion from command_pipeline import Monitor_Window from playback_starter import Playback_Starter @@ -116,8 +116,6 @@ class Service(): if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() - plx = PlexAPI.PlexAPI() - welcome_msg = True counter = 0 while not __stop_PKC(): @@ -208,7 +206,7 @@ class Service(): if server is False: # No server info set in add-on settings pass - elif plx.CheckConnection(server, verifySSL=True) is False: + elif check_connection(server, verifySSL=True) is False: # Server is offline or cannot be reached # Alert the user and suppress future warning if self.server_online: @@ -228,9 +226,9 @@ class Service(): if counter > 20: counter = 0 setup = initialsetup.InitialSetup() - tmp = setup.PickPMS() + tmp = setup.pick_pms() if tmp is not None: - setup.WritePMStoSettings(tmp) + setup.write_pms_to_settings(tmp) else: # Server is online counter = 0 From b2d37ec9b7de8fea8d79a6988f1aa21b10f588be Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Feb 2018 18:07:10 +0100 Subject: [PATCH 309/509] Add notification when searching for PMS --- resources/language/resource.language.en_gb/strings.po | 4 ++++ resources/lib/initialsetup.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 018cfd44..4e6a0254 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -23,6 +23,10 @@ msgctxt "#30000" msgid "Server Address (IP)" msgstr "" +msgctxt "#30001" +msgid "Searching for PMS" +msgstr "" + msgctxt "#30002" msgid "Preferred playback method" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 949af967..517ec811 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -375,6 +375,12 @@ class InitialSetup(object): Returns server or None if unsuccessful """ https_updated = False + # Searching for PMS + dialog('notification', + heading='{plex}', + message=lang(30001), + icon='{plex}', + time=5000) while True: if https_updated is False: serverlist = PF.discover_pms(self.plex_token) From 055aadc0484b167c9cc5fc2f7c109ee6aef105da Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 12:06:04 +0100 Subject: [PATCH 310/509] Prettify --- resources/lib/itemtypes.py | 718 +++++++++++++++++-------------------- 1 file changed, 337 insertions(+), 381 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 3ca61e2a..b7c5307b 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1,24 +1,22 @@ # -*- coding: utf-8 -*- - ############################################################################### from logging import getLogger from urllib import urlencode from ntpath import dirname from datetime import datetime -import artwork +from artwork import Artwork from utils import window, kodiSQL, CatchExceptions import plexdb_functions as plexdb import kodidb_functions as kodidb -import PlexAPI +from PlexAPI import API from PlexFunctions import GetPlexMetadata import variables as v import state - ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -31,10 +29,15 @@ class Items(object): Input: kodiType: optional argument; e.g. 'video' or 'music' """ - def __init__(self): - self.artwork = artwork.Artwork() + self.artwork = Artwork() self.server = window('pms_server') + self.plexconn = None + self.plexcursor = None + self.kodiconn = None + self.kodicursor = None + self.plex_db = None + self.kodi_db = None def __enter__(self): """ @@ -71,8 +74,8 @@ class Items(object): kodi_id = db_item[0] kodi_type = db_item[4] except TypeError: - log.error('Could not get Kodi id for plex id %s, abort getfanart' - % plex_id) + LOG.error('Could not get Kodi id for plex id %s, abort getfanart', + plex_id) return False if refresh is True: # Leave the Plex art untouched @@ -87,40 +90,40 @@ class Items(object): needsupdate = True break if needsupdate is False: - log.debug('Already got all fanart for Plex id %s' % plex_id) + LOG.debug('Already got all fanart for Plex id %s', plex_id) return True xml = GetPlexMetadata(plex_id) if xml is None: # Did not receive a valid XML - skip that item for now - log.error("Could not get metadata for %s. Skipping that item " - "for now" % plex_id) + LOG.error("Could not get metadata for %s. Skipping that item " + "for now", plex_id) return False elif xml == 401: - log.error('HTTP 401 returned by PMS. Too much strain? ' + LOG.error('HTTP 401 returned by PMS. Too much strain? ' 'Cancelling sync for now') # Kill remaining items in queue (for main thread to cont.) return False - API = PlexAPI.API(xml[0]) + api = API(xml[0]) if allartworks is None: - allartworks = API.getAllArtwork() - self.artwork.addArtwork(API.getFanartArtwork(allartworks), + allartworks = api.getAllArtwork() + self.artwork.addArtwork(api.getFanartArtwork(allartworks), kodi_id, kodi_type, self.kodicursor) # Also get artwork for collections/movie sets if kodi_type == v.KODI_TYPE_MOVIE: - for setname in API.getCollections(): - log.debug('Getting artwork for movie set %s' % setname) + 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(), + self.artwork.addArtwork(api.getSetArtwork(), setid, v.KODI_TYPE_SET, self.kodicursor) self.kodi_db.assignBoxset(setid, kodi_id) return True - def updateUserdata(self, xml, viewtag=None, viewid=None): + def updateUserdata(self, xml): """ Updates the Kodi watched state of the item from PMS. Also retrieves Plex resume points for movies in progress. @@ -128,15 +131,15 @@ class Items(object): viewtag and viewid only serve as dummies """ for mediaitem in xml: - API = PlexAPI.API(mediaitem) + api = API(mediaitem) # Get key and db entry on the Kodi db side - db_item = self.plex_db.getItem_byId(API.getRatingKey()) + db_item = self.plex_db.getItem_byId(api.getRatingKey()) try: fileid = db_item[1] except TypeError: continue # Grab the user's viewcount, resume points etc. from PMS' answer - userdata = API.getUserData() + userdata = api.getUserData() # Write to Kodi DB self.kodi_db.addPlaystate(fileid, userdata['Resume'], @@ -156,7 +159,7 @@ class Items(object): # If the playback was stopped, check whether we need to increment the # playcount. PMS won't tell us the playcount via websockets if mark_played: - log.info('Marking as completely watched in Kodi') + LOG.info('Marking as completely watched in Kodi') try: view_count += 1 except TypeError: @@ -171,23 +174,27 @@ class Items(object): class Movies(Items): - + """ + Used for plex library-type movies + """ @CatchExceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): - # Process single movie + """ + Process single movie + """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) # If the item already exist in the local Kodi DB we'll perform a full # item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() # Cannot parse XML, abort if not itemid: - log.error("Cannot parse XML data for movie") + LOG.error("Cannot parse XML data for movie") return plex_dbitem = plex_db.getItem_byId(itemid) try: @@ -210,39 +217,39 @@ class Movies(Items): except TypeError: # item is not found, let's recreate it. update_item = False - log.info("movieid: %s missing from Kodi, repairing the entry." - % movieid) + LOG.info("movieid: %s missing from Kodi, repairing the entry.", + movieid) # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() + checksum = api.getChecksum() + dateadded = api.getDateCreated() + userdata = api.getUserData() playcount = userdata['PlayCount'] dateplayed = userdata['LastPlayedDate'] resume = userdata['Resume'] runtime = userdata['Runtime'] # item details - people = API.getPeople() - writer = API.joinList(people['Writer']) - director = API.joinList(people['Director']) - genres = API.getGenres() - genre = API.joinList(genres) - title, sorttitle = API.getTitle() - plot = API.getPlot() + people = api.getPeople() + writer = api.joinList(people['Writer']) + director = api.joinList(people['Director']) + genres = api.getGenres() + genre = api.joinList(genres) + title, sorttitle = api.getTitle() + plot = api.getPlot() shortplot = None - tagline = API.getTagline() + tagline = api.getTagline() votecount = None - collections = API.getCollections() + collections = api.getCollections() rating = userdata['Rating'] - year = API.getYear() - premieredate = API.getPremiereDate() - imdb = API.getProvider('imdb') - mpaa = API.getMpaa() - countries = API.getCountry() - country = API.joinList(countries) - studios = API.getStudios() + year = api.getYear() + premieredate = api.getPremiereDate() + imdb = api.getProvider('imdb') + mpaa = api.getMpaa() + countries = api.getCountry() + country = api.joinList(countries) + studios = api.getStudios() try: studio = studios[0] except IndexError: @@ -250,25 +257,24 @@ class Movies(Items): # Find one trailer trailer = None - extras = API.getExtras() + extras = api.getExtras() for extra in extras: # Only get 1st trailer element if extra['extraType'] == 1: - trailer = ("plugin://plugin.video.plexkodiconnect?" - "plex_id=%s&plex_type=%s&mode=play" - % (extra['ratingKey'], v.PLEX_TYPE_CLIP)) + trailer = ('plugin://%s?plex_id=%s&plex_type=%s&mode=play' + % (v.ADDON_ID, extra['ratingKey'], v.PLEX_TYPE_CLIP)) break # GET THE FILE AND PATH ##### - doIndirect = not state.DIRECT_PATHS + do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = API.getFilePath(forceFirstMediaStream=True) + playurl = api.getFilePath(forceFirstMediaStream=True) if playurl is None: # Something went wrong, trying to use non-direct paths - doIndirect = True + do_indirect = True else: - playurl = API.validatePlayurl(playurl, API.getType()) + playurl = api.validatePlayurl(playurl, api.getType()) if playurl is None: return False if "\\" in playurl: @@ -278,9 +284,9 @@ class Movies(Items): # Network share filename = playurl.rsplit("/", 1)[1] path = playurl.replace(filename, "") - if doIndirect: + if do_indirect: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect" + path = 'plugin://%s' % v.ADDON_ID params = { 'mode': 'play', 'plex_id': itemid, @@ -301,8 +307,7 @@ class Movies(Items): # UPDATE THE MOVIE ##### if update_item: - log.info("UPDATE movie itemid: %s - Title: %s" - % (itemid, title)) + LOG.info("UPDATE movie itemid: %s - Title: %s", itemid, title) # Update the movie entry if v.KODIVERSION >= 17: # update new ratings Kodi 17 @@ -355,7 +360,7 @@ class Movies(Items): # OR ADD THE MOVIE ##### else: - log.info("ADD movie itemid: %s - Title: %s" % (itemid, title)) + LOG.info("ADD movie itemid: %s - Title: %s", itemid, title) if v.KODIVERSION >= 17: # add new ratings Kodi 17 rating_id = self.kodi_db.create_entry_rating() @@ -430,17 +435,17 @@ class Movies(Items): "WHERE idFile = ?" )) kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - + # Process countries self.kodi_db.addCountries(movieid, countries, "movie") # Process cast - self.kodi_db.addPeople(movieid, API.getPeopleList(), "movie") + self.kodi_db.addPeople(movieid, api.getPeopleList(), "movie") # Process genres self.kodi_db.addGenres(movieid, genres, "movie") # Process artwork - artwork.addArtwork(API.getAllArtwork(), movieid, "movie", kodicursor) + artwork.addArtwork(api.getAllArtwork(), movieid, "movie", kodicursor) # Process stream details - self.kodi_db.addStreams(fileid, API.getMediaStreams(), runtime) + self.kodi_db.addStreams(fileid, api.getMediaStreams(), runtime) # Process studios self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags @@ -455,7 +460,9 @@ class Movies(Items): self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) def remove(self, itemid): - # Remove movieid, fileid, plex reference + """ + Remove movieid, fileid, plex reference + """ plex_db = self.plex_db kodicursor = self.kodicursor artwork = self.artwork @@ -465,8 +472,8 @@ class Movies(Items): kodi_id = plex_dbitem[0] file_id = plex_dbitem[1] kodi_type = plex_dbitem[4] - log.info("Removing %sid: %s file_id: %s" - % (kodi_type, kodi_id, file_id)) + LOG.info("Removing %sid: %s file_id: %s", + kodi_type, kodi_id, file_id) except TypeError: return @@ -495,27 +502,30 @@ class Movies(Items): # Update plex reference plex_db.updateParentId(plexid, None) kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,)) - log.info("Deleted %s %s from kodi database" % (kodi_type, itemid)) + LOG.info("Deleted %s %s from kodi database", kodi_type, itemid) class TVShows(Items): - + """ + For Plex library-type TV shows + """ @CatchExceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): - # Process single tvshow + """ + Process a single show + """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() if not itemid: - log.error("Cannot parse XML data for TV show") + LOG.error("Cannot parse XML data for TV show") return update_item = True - force_episodes = False plex_dbitem = plex_db.getItem_byId(itemid) try: showid = plex_dbitem[0] @@ -534,42 +544,40 @@ class TVShows(Items): except TypeError: # item is not found, let's recreate it. update_item = False - log.info("showid: %s missing from Kodi, repairing the entry." - % showid) - # Force re-add episodes after the show is re-created. - force_episodes = True + LOG.info("showid: %s missing from Kodi, repairing the entry.", + showid) # fileId information - checksum = API.getChecksum() + checksum = api.getChecksum() # item details - genres = API.getGenres() - title, sorttitle = API.getTitle() - plot = API.getPlot() - rating = API.getAudienceRating() + genres = api.getGenres() + title, sorttitle = api.getTitle() + plot = api.getPlot() + rating = api.getAudienceRating() votecount = None - premieredate = API.getPremiereDate() - tvdb = API.getProvider('tvdb') - mpaa = API.getMpaa() - genre = API.joinList(genres) - studios = API.getStudios() - collections = API.getCollections() + premieredate = api.getPremiereDate() + tvdb = api.getProvider('tvdb') + mpaa = api.getMpaa() + genre = api.joinList(genres) + studios = api.getStudios() + collections = api.getCollections() try: studio = studios[0] except IndexError: studio = None # GET THE FILE AND PATH ##### - doIndirect = not state.DIRECT_PATHS + do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = API.getTVShowPath() + playurl = api.getTVShowPath() if playurl is None: # Something went wrong, trying to use non-direct paths - doIndirect = True + do_indirect = True else: - playurl = API.validatePlayurl(playurl, - API.getType(), + playurl = api.validatePlayurl(playurl, + api.getType(), folder=True) if playurl is None: return False @@ -581,9 +589,9 @@ class TVShows(Items): # Network path path = "%s/" % playurl toplevelpath = "%s/" % dirname(dirname(path)) - if doIndirect: + if do_indirect: # Set plugin path - toplevelpath = "plugin://plugin.video.plexkodiconnect/tvshows/" + toplevelpath = "plugin://%s/tvshows/" % v.ADDON_ID path = "%s%s/" % (toplevelpath, itemid) # Add top path @@ -593,8 +601,7 @@ class TVShows(Items): pathid = self.kodi_db.addPath(path) # UPDATE THE TVSHOW ##### if update_item: - log.info("UPDATE tvshow itemid: %s - Title: %s" - % (itemid, title)) + LOG.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) # Add reference is idempotent; the call here updates also fileid # and pathid when item is moved or renamed plex_db.addReference(itemid, @@ -649,7 +656,7 @@ class TVShows(Items): # OR ADD THE TVSHOW ##### else: - log.info("ADD tvshow itemid: %s - Title: %s" % (itemid, title)) + LOG.info("ADD tvshow itemid: %s - Title: %s", itemid, title) query = ''' UPDATE path SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ? @@ -721,12 +728,12 @@ class TVShows(Items): kodicursor.execute(query, (path, None, None, 1, toppathid, pathid)) # Process cast - people = API.getPeopleList() + people = api.getPeopleList() self.kodi_db.addPeople(showid, people, "tvshow") # Process genres self.kodi_db.addGenres(showid, genres, "tvshow") # Process artwork - allartworks = API.getAllArtwork() + allartworks = api.getAllArtwork() artwork.addArtwork(allartworks, showid, "tvshow", kodicursor) # Process studios self.kodi_db.addStudios(showid, studios, "tvshow") @@ -737,38 +744,37 @@ class TVShows(Items): @CatchExceptions(warnuser=True) def add_updateSeason(self, item, viewtag=None, viewid=None): - API = PlexAPI.API(item) - plex_id = API.getRatingKey() + """ + Process a single season of a certain tv show + """ + api = API(item) + plex_id = api.getRatingKey() if not plex_id: - log.error('Error getting plex_id for season, skipping') + LOG.error('Error getting plex_id for season, skipping') return kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - seasonnum = API.getIndex() + seasonnum = api.getIndex() # Get parent tv show Plex id plexshowid = item.attrib.get('parentRatingKey') # Get Kodi showid plex_dbitem = plex_db.getItem_byId(plexshowid) try: showid = plex_dbitem[0] - except: - log.error('Could not find parent tv show for season %s. ' - 'Skipping season for now.' % (plex_id)) + except TypeError: + LOG.error('Could not find parent tv show for season %s. ' + 'Skipping season for now.', plex_id) return seasonid = self.kodi_db.addSeason(showid, seasonnum) - checksum = API.getChecksum() + checksum = api.getChecksum() # Check whether Season already exists - update_item = True plex_dbitem = plex_db.getItem_byId(plex_id) - try: - plexdbItemId = plex_dbitem[0] - except TypeError: - update_item = False + update_item = False if plex_dbitem is None else True # Process artwork - allartworks = API.getAllArtwork() + allartworks = api.getAllArtwork() artwork.addArtwork(allartworks, seasonid, "season", kodicursor) if update_item: @@ -787,20 +793,20 @@ class TVShows(Items): @CatchExceptions(warnuser=True) def add_updateEpisode(self, item, viewtag=None, viewid=None): """ + Process single episode """ - # Process single episode kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) # If the item already exist in the local Kodi DB we'll perform a full # item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() if not itemid: - log.error('Error getting itemid for episode, skipping') + LOG.error('Error getting itemid for episode, skipping') return plex_dbitem = plex_db.getItem_byId(itemid) try: @@ -822,30 +828,30 @@ class TVShows(Items): except TypeError: # item is not found, let's recreate it. update_item = False - log.info("episodeid: %s missing from Kodi, repairing entry." - % episodeid) + LOG.info("episodeid: %s missing from Kodi, repairing entry.", + episodeid) # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() + checksum = api.getChecksum() + dateadded = api.getDateCreated() + userdata = api.getUserData() playcount = userdata['PlayCount'] dateplayed = userdata['LastPlayedDate'] - tvdb = API.getProvider('tvdb') + tvdb = api.getProvider('tvdb') votecount = None # item details - peoples = API.getPeople() - director = API.joinList(peoples['Director']) - writer = API.joinList(peoples['Writer']) - title, sorttitle = API.getTitle() - plot = API.getPlot() + peoples = api.getPeople() + director = api.joinList(peoples['Director']) + writer = api.joinList(peoples['Writer']) + title, _ = api.getTitle() + plot = api.getPlot() rating = userdata['Rating'] - resume, runtime = API.getRuntime() - premieredate = API.getPremiereDate() + resume, runtime = api.getRuntime() + premieredate = api.getPremiereDate() # episode details - seriesId, seriesName, season, episode = API.getEpisodeDetails() + series_id, _, season, episode = api.getEpisodeDetails() if season is None: season = -1 @@ -858,40 +864,36 @@ class TVShows(Items): # else: # season = -1 - # Specials ordering within season - if item.get('AirsAfterSeasonNumber'): - airsBeforeSeason = item['AirsAfterSeasonNumber'] - # Kodi default number for afterseason ordering - airsBeforeEpisode = 4096 - else: - airsBeforeSeason = item.get('AirsBeforeSeasonNumber') - airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') - - airsBeforeSeason = "-1" - airsBeforeEpisode = "-1" - # Append multi episodes to title - # if item.get('IndexNumberEnd'): - # title = "| %02d | %s" % (item['IndexNumberEnd'], title) + # # Specials ordering within season + # if item.get('AirsAfterSeasonNumber'): + # airs_before_season = item['AirsAfterSeasonNumber'] + # # Kodi default number for afterseason ordering + # airs_before_episode = 4096 + # else: + # airs_before_season = item.get('AirsBeforeSeasonNumber') + # airs_before_episode = item.get('AirsBeforeEpisodeNumber') + airs_before_season = "-1" + airs_before_episode = "-1" # Get season id - show = plex_db.getItem_byId(seriesId) + show = plex_db.getItem_byId(series_id) try: showid = show[0] except TypeError: - log.error("Parent tvshow now found, skip item") + LOG.error("Parent tvshow now found, skip item") return False seasonid = self.kodi_db.addSeason(showid, season) # GET THE FILE AND PATH ##### - doIndirect = not state.DIRECT_PATHS - playurl = API.getFilePath(forceFirstMediaStream=True) + do_indirect = not state.DIRECT_PATHS + playurl = api.getFilePath(forceFirstMediaStream=True) if state.DIRECT_PATHS: # Direct paths is set the Kodi way if playurl is None: # Something went wrong, trying to use non-direct paths - doIndirect = True + do_indirect = True else: - playurl = API.validatePlayurl(playurl, API.getType()) + playurl = api.validatePlayurl(playurl, api.getType()) if playurl is None: return False if "\\" in playurl: @@ -901,8 +903,8 @@ class TVShows(Items): # Network share filename = playurl.rsplit("/", 1)[1] path = playurl.replace(filename, "") - parentPathId = self.kodi_db.getParentPathId(path) - if doIndirect: + parent_path_id = self.kodi_db.getParentPathId(path) + if do_indirect: # Set plugin path and media flags using real filename if playurl is not None: if '\\' in playurl: @@ -911,7 +913,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://%s/tvshows/%s/' % (v.ADDON_ID, series_id) params = { 'plex_id': itemid, 'plex_type': v.PLEX_TYPE_EPISODE, @@ -919,13 +921,8 @@ class TVShows(Items): } filename = "%s?%s" % (path, urlencode(params)) playurl = filename - parentPathId = self.kodi_db.addPath( - 'plugin://plugin.video.plexkodiconnect/tvshows/') - - # episodes table: - # c18 - playurl - # c19 - pathid - # This information is used later by file browser. + parent_path_id = self.kodi_db.addPath('plugin://%s/tvshows/' + % v.ADDON_ID) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value @@ -934,7 +931,7 @@ class TVShows(Items): # UPDATE THE EPISODE ##### if update_item: - log.info("UPDATE episode itemid: %s" % (itemid)) + LOG.info("UPDATE episode itemid: %s", itemid) # Update the movie entry if v.KODIVERSION >= 17: # update new ratings Kodi 17 @@ -964,7 +961,7 @@ class TVShows(Items): ''' kodicursor.execute(query, (title, plot, rating, writer, premieredate, runtime, director, season, episode, title, - airsBeforeSeason, airsBeforeEpisode, playurl, pathid, + airs_before_season, airs_before_episode, playurl, pathid, fileid, seasonid, userdata['UserRating'], episodeid)) elif v.KODIVERSION == 16: # Kodi Jarvis @@ -977,7 +974,7 @@ class TVShows(Items): ''' kodicursor.execute(query, (title, plot, rating, writer, premieredate, runtime, director, season, episode, title, - airsBeforeSeason, airsBeforeEpisode, playurl, pathid, + airs_before_season, airs_before_episode, playurl, pathid, fileid, seasonid, episodeid)) else: query = ''' @@ -989,14 +986,14 @@ class TVShows(Items): ''' kodicursor.execute(query, (title, plot, rating, writer, premieredate, runtime, director, season, episode, title, - airsBeforeSeason, airsBeforeEpisode, playurl, pathid, + airs_before_season, airs_before_episode, playurl, pathid, fileid, episodeid)) # Update parentid reference plex_db.updateParentId(itemid, seasonid) # OR ADD THE EPISODE ##### else: - log.info("ADD episode itemid: %s - Title: %s" % (itemid, title)) + LOG.info("ADD episode itemid: %s - Title: %s", itemid, title) # Create the episode entry if v.KODIVERSION >= 17: # add new ratings Kodi 17 @@ -1022,8 +1019,8 @@ class TVShows(Items): ''' kodicursor.execute(query, (episodeid, fileid, title, plot, rating_id, writer, premieredate, runtime, director, season, - episode, title, showid, airsBeforeSeason, - airsBeforeEpisode, playurl, pathid, seasonid, + episode, title, showid, airs_before_season, + airs_before_episode, playurl, pathid, seasonid, userdata['UserRating'])) elif v.KODIVERSION == 16: # Kodi Jarvis @@ -1036,8 +1033,8 @@ class TVShows(Items): ''' kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, premieredate, runtime, director, season, - episode, title, showid, airsBeforeSeason, - airsBeforeEpisode, playurl, pathid, seasonid)) + episode, title, showid, airs_before_season, + airs_before_episode, playurl, pathid, seasonid)) else: query = ( ''' @@ -1048,9 +1045,10 @@ class TVShows(Items): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''' ) - kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, showid, - airsBeforeSeason, airsBeforeEpisode, playurl, pathid)) + kodicursor.execute(query, (episodeid, fileid, title, plot, + rating, writer, premieredate, runtime, director, season, + episode, title, showid, airs_before_season, + airs_before_episode, playurl, pathid)) # Create or update the reference in plex table Add reference is # idempotent; the call here updates also fileid and pathid when item is @@ -1073,7 +1071,7 @@ class TVShows(Items): "idParentPath = ?" "WHERE idPath = ?" )) - kodicursor.execute(query, (path, None, None, 1, parentPathId, pathid)) + kodicursor.execute(query, (path, None, None, 1, parent_path_id, pathid)) # Update the file query = ' '.join(( @@ -1083,39 +1081,37 @@ class TVShows(Items): )) kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process cast - people = API.getPeopleList() + people = api.getPeopleList() self.kodi_db.addPeople(episodeid, people, "episode") # Process artwork # Wide "screenshot" of particular episode poster = item.attrib.get('thumb') if poster: - poster = API.addPlexCredentialsToUrl( + poster = api.addPlexCredentialsToUrl( "%s%s" % (self.server, poster)) artwork.addOrUpdateArt( poster, episodeid, "episode", "thumb", kodicursor) - # poster of TV show itself - # poster = item.attrib.get('grandparentThumb') - # if poster: - # poster = API.addPlexCredentialsToUrl( - # "%s%s" % (self.server, poster)) - # artwork.addOrUpdateArt( - # poster, episodeid, "episode", "poster", kodicursor) # Process stream details - streams = API.getMediaStreams() + streams = api.getMediaStreams() self.kodi_db.addStreams(fileid, streams, runtime) # Process playstates - self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) + self.kodi_db.addPlaystate(fileid, + resume, + runtime, + playcount, + dateplayed) if not state.DIRECT_PATHS and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/") + # Create additional entry for widgets. This is only required for + # plugin/episode. + temppathid = self.kodi_db.getPath('plugin://%s/tvshows/' + % v.ADDON_ID) tempfileid = self.kodi_db.addFile(filename, temppathid) - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) + query = ''' + UPDATE files + SET idPath = ?, strFilename = ?, dateAdded = ? + WHERE idFile = ? + ''' kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) self.kodi_db.addPlaystate(tempfileid, @@ -1125,7 +1121,9 @@ class TVShows(Items): dateplayed) def remove(self, itemid): - # Remove showid, fileid, pathid, plex reference + """ + Remove showid, fileid, pathid, plex reference + """ plex_db = self.plex_db kodicursor = self.kodicursor @@ -1135,8 +1133,8 @@ class TVShows(Items): fileid = plex_dbitem[1] parentid = plex_dbitem[3] mediatype = plex_dbitem[4] - log.info("Removing %s kodiid: %s fileid: %s" - % (mediatype, kodiid, fileid)) + LOG.info("Removing %s kodiid: %s fileid: %s", + mediatype, kodiid, fileid) except TypeError: return @@ -1217,7 +1215,7 @@ class TVShows(Items): self.removeEpisode(episode[1], episode[2]) # Remove plex episodes plex_db.removeItems_byParentId(kodiid, v.KODI_TYPE_EPISODE) - + # Remove season self.removeSeason(kodiid) @@ -1227,26 +1225,34 @@ class TVShows(Items): # There's no seasons, delete the show self.removeShow(parentid) plex_db.removeItem_byKodiId(parentid, v.KODI_TYPE_SHOW) - - log.debug("Deleted %s: %s from kodi database" % (mediatype, itemid)) + LOG.debug("Deleted %s: %s from kodi database", mediatype, itemid) def removeShow(self, kodi_id): + """ + Remove a TV show, and only the show, no seasons or episodes + """ kodicursor = self.kodicursor self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.remove_ratings(kodi_id, v.KODI_TYPE_SHOW) - log.info("Removed tvshow: %s." % kodi_id) + LOG.info("Removed tvshow: %s.", kodi_id) def removeSeason(self, kodi_id): + """ + Remove a season, and only a season, not the show or episodes + """ kodicursor = self.kodicursor self.artwork.deleteArtwork(kodi_id, "season", kodicursor) kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,)) - log.info("Removed season: %s." % kodi_id) + LOG.info("Removed season: %s.", kodi_id) def removeEpisode(self, kodi_id, fileid): + """ + Remove an episode, and episode only + """ kodicursor = self.kodicursor self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", @@ -1255,11 +1261,13 @@ class TVShows(Items): if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.remove_ratings(kodi_id, v.KODI_TYPE_EPISODE) - log.info("Removed episode: %s." % kodi_id) + LOG.info("Removed episode: %s.", kodi_id) class Music(Items): - + """ + For Plex library-type music. Also works for premium music libraries + """ def __enter__(self): """ OVERWRITE this method, because we need to open another DB. @@ -1275,15 +1283,17 @@ class Music(Items): return self @CatchExceptions(warnuser=True) - def add_updateArtist(self, item, viewtag=None, viewid=None, - artisttype="MusicArtist"): + def add_updateArtist(self, item, viewtag=None, viewid=None): + """ + Adds a single artist + """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() plex_dbitem = plex_db.getItem_byId(itemid) try: artistid = plex_dbitem[0] @@ -1292,17 +1302,17 @@ class Music(Items): # The artist details ##### lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - checksum = API.getChecksum() + dateadded = api.getDateCreated() + checksum = api.getChecksum() - name, sortname = API.getTitle() - # musicBrainzId = API.getProvider('MusicBrainzArtist') + name, _ = api.getTitle() + # musicBrainzId = api.getProvider('MusicBrainzArtist') musicBrainzId = None - genres = ' / '.join(API.getGenres()) - bio = API.getPlot() + genres = ' / '.join(api.getGenres()) + bio = api.getPlot() # Associate artwork - artworks = API.getAllArtwork(parentInfo=True) + artworks = api.getAllArtwork(parentInfo=True) thumb = artworks['Primary'] backdrops = artworks['Backdrop'] # List @@ -1315,13 +1325,13 @@ class Music(Items): # UPDATE THE ARTIST ##### if update_item: - log.info("UPDATE artist itemid: %s - Name: %s" % (itemid, name)) + LOG.info("UPDATE artist itemid: %s - Name: %s", itemid, name) # 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)) + LOG.info("ADD artist itemid: %s - Name: %s", itemid, name) # 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 @@ -1362,17 +1372,18 @@ class Music(Items): def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None, scan_children=True): """ - children: list of child xml's, so in this case songs + Adds a single music album + children: list of child xml's, so in this case songs """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() if not itemid: - log.error('Error processing Album, skipping') + LOG.error('Error processing Album, skipping') return plex_dbitem = plex_db.getItem_byId(itemid) try: @@ -1383,19 +1394,19 @@ class Music(Items): # The album details ##### lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - userdata = API.getUserData() - checksum = API.getChecksum() + dateadded = api.getDateCreated() + userdata = api.getUserData() + checksum = api.getChecksum() - name, sorttitle = API.getTitle() - # musicBrainzId = API.getProvider('MusicBrainzAlbum') + name, _ = api.getTitle() + # musicBrainzId = api.getProvider('MusicBrainzAlbum') musicBrainzId = None - year = API.getYear() - self.genres = API.getGenres() + year = api.getYear() + self.genres = api.getGenres() self.genre = ' / '.join(self.genres) - bio = API.getPlot() + bio = api.getPlot() rating = userdata['UserRating'] - studio = API.getMusicStudio() + studio = api.getMusicStudio() artistname = item.attrib.get('parentTitle') if not artistname: artistname = item.attrib.get('originalTitle') @@ -1407,20 +1418,20 @@ class Music(Items): self.compilation = 1 break # Associate artwork - artworks = API.getAllArtwork(parentInfo=True) + artworks = api.getAllArtwork(parentInfo=True) thumb = artworks['Primary'] if thumb: thumb = "%s" % thumb # UPDATE THE ALBUM ##### if update_item: - log.info("UPDATE album itemid: %s - Name: %s" % (itemid, name)) + LOG.info("UPDATE album itemid: %s - Name: %s", itemid, name) # 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)) + LOG.info("ADD album itemid: %s - Name: %s", itemid, name) # 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 @@ -1487,49 +1498,49 @@ class Music(Items): studio, albumid)) # Associate the parentid for plex reference - parentId = item.attrib.get('parentRatingKey') - if parentId is not None: - plex_dbartist = plex_db.getItem_byId(parentId) + parent_id = item.attrib.get('parentRatingKey') + if parent_id is not None: + plex_dbartist = plex_db.getItem_byId(parent_id) try: artistid = plex_dbartist[0] except TypeError: - log.info('Artist %s does not exist in plex database' - % parentId) - artist = GetPlexMetadata(parentId) + LOG.info('Artist %s does not exist in plex database', + parent_id) + artist = GetPlexMetadata(parent_id) # Item may not be an artist, verification necessary. if artist is not None and artist != 401: if artist[0].attrib.get('type') == v.PLEX_TYPE_ARTIST: - # Update with the parentId, for remove reference - plex_db.addReference(parentId, + # Update with the parent_id, for remove reference + plex_db.addReference(parent_id, v.PLEX_TYPE_ARTIST, - parentId, + parent_id, v.KODI_TYPE_ARTIST, view_id=viewid) - plex_db.updateParentId(itemid, parentId) + plex_db.updateParentId(itemid, parent_id) else: # Update plex reference with the artistid plex_db.updateParentId(itemid, artistid) # Assign main artists to album # Plex unfortunately only supports 1 artist :-( - artistId = parentId - plex_dbartist = plex_db.getItem_byId(artistId) + artist_id = parent_id + plex_dbartist = plex_db.getItem_byId(artist_id) try: artistid = plex_dbartist[0] 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) + LOG.info('Artist %s does not exist in Plex database', artist_id) + artist = GetPlexMetadata(artist_id) if artist is not None and artist != 401: - self.add_updateArtist(artist[0], artisttype="AlbumArtist") - plex_dbartist = plex_db.getItem_byId(artistId) + self.add_updateArtist(artist[0]) + plex_dbartist = plex_db.getItem_byId(artist_id) artistid = plex_dbartist[0] else: # Best take this name over anything else. query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" kodicursor.execute(query, (artistname, artistid,)) - log.info("UPDATE artist: strArtist: %s, idArtist: %s" - % (artistname, artistid)) + LOG.info("UPDATE artist: strArtist: %s, idArtist: %s", + artistname, artistid) # Add artist to album query = ''' @@ -1544,7 +1555,7 @@ class Music(Items): ''' kodicursor.execute(query, (artistid, name, year)) # Update plex reference with parentid - plex_db.updateParentId(artistId, albumid) + plex_db.updateParentId(artist_id, albumid) # Add genres self.kodi_db.addMusicGenres(albumid, self.genres, v.KODI_TYPE_ALBUM) # Update artwork @@ -1556,16 +1567,18 @@ class Music(Items): @CatchExceptions(warnuser=True) def add_updateSong(self, item, viewtag=None, viewid=None): - # Process single song + """ + Process single song + """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - API = PlexAPI.API(item) + api = API(item) update_item = True - itemid = API.getRatingKey() + itemid = api.getRatingKey() if not itemid: - log.error('Error processing Song; skipping') + LOG.error('Error processing Song; skipping') return plex_dbitem = plex_db.getItem_byId(itemid) try: @@ -1579,9 +1592,9 @@ class Music(Items): songid = kodicursor.fetchone()[0] + 1 # The song details ##### - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() + checksum = api.getChecksum() + dateadded = api.getDateCreated() + userdata = api.getUserData() playcount = userdata['PlayCount'] if playcount is None: # This is different to Video DB! @@ -1589,8 +1602,8 @@ class Music(Items): dateplayed = userdata['LastPlayedDate'] # item details - title, sorttitle = API.getTitle() - # musicBrainzId = API.getProvider('MusicBrainzTrackId') + title, _ = api.getTitle() + # musicBrainzId = api.getProvider('MusicBrainzTrackId') musicBrainzId = None try: genres = self.genres @@ -1614,8 +1627,8 @@ class Music(Items): track = tracknumber else: track = disc*2**16 + tracknumber - year = API.getYear() - resume, duration = API.getRuntime() + year = api.getYear() + _, duration = api.getRuntime() rating = userdata['UserRating'] comment = None # Moods @@ -1626,15 +1639,15 @@ class Music(Items): mood = ' / '.join(moods) # GET THE FILE AND PATH ##### - doIndirect = not state.DIRECT_PATHS + do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = API.getFilePath(forceFirstMediaStream=True) + playurl = api.getFilePath(forceFirstMediaStream=True) if playurl is None: # Something went wrong, trying to use non-direct paths - doIndirect = True + do_indirect = True else: - playurl = API.validatePlayurl(playurl, API.getType()) + playurl = api.validatePlayurl(playurl, api.getType()) if playurl is None: return False if "\\" in playurl: @@ -1644,17 +1657,17 @@ class Music(Items): # Network share filename = playurl.rsplit("/", 1)[1] path = playurl.replace(filename, "") - if doIndirect: + if do_indirect: # Plex works a bit differently path = "%s%s" % (self.server, item[0][0].attrib.get('key')) - path = API.addPlexCredentialsToUrl(path) + path = api.addPlexCredentialsToUrl(path) filename = path.rsplit('/', 1)[1] path = path.replace(filename, '') # UPDATE THE SONG ##### if update_item: - log.info("UPDATE song itemid: %s - Title: %s with path: %s" - % (itemid, title, path)) + LOG.info("UPDATE song itemid: %s - Title: %s with path: %s", + itemid, title, path) # Update path # Use dummy strHash '123' for Kodi query = "UPDATE path SET strPath = ?, strHash = ? WHERE idPath = ?" @@ -1679,7 +1692,7 @@ class Music(Items): # OR ADD THE SONG ##### else: - log.info("ADD song itemid: %s - Title: %s" % (itemid, title)) + LOG.info("ADD song itemid: %s - Title: %s", itemid, title) # Add path pathid = self.kodi_db.addPath(path, strHash="123") @@ -1693,11 +1706,11 @@ class Music(Items): # Verify if there's an album associated. album_name = item.get('parentTitle') if album_name: - log.info("Creating virtual music album for song: %s." - % itemid) + LOG.info("Creating virtual music album for song: %s.", + itemid) albumid = self.kodi_db.addAlbum( album_name, - API.getProvider('MusicBrainzAlbum')) + api.getProvider('MusicBrainzAlbum')) plex_db.addReference("%salbum%s" % (itemid, albumid), v.PLEX_TYPE_ALBUM, albumid, @@ -1705,28 +1718,28 @@ class Music(Items): view_id=viewid) else: # No album Id associated to the song. - log.error("Song itemid: %s has no albumId associated." - % itemid) + LOG.error("Song itemid: %s has no albumId associated.", + itemid) return False except TypeError: # No album found. Let's create it - log.info("Album database entry missing.") - plex_albumId = item.attrib.get('parentRatingKey') - album = GetPlexMetadata(plex_albumId) + LOG.info("Album database entry missing.") + plex_album_id = item.attrib.get('parentRatingKey') + album = GetPlexMetadata(plex_album_id) if album is None or album == 401: - log.error('Could not download album, abort') + LOG.error('Could not download album, abort') return self.add_updateAlbum(album[0], children=[item], scan_children=False) - plex_dbalbum = plex_db.getItem_byId(plex_albumId) + plex_dbalbum = plex_db.getItem_byId(plex_album_id) try: albumid = plex_dbalbum[0] - log.debug("Found albumid: %s" % albumid) + LOG.debug("Found albumid: %s", albumid) except TypeError: # No album found, create a single's album - log.info("Failed to add album. Creating singles.") + LOG.info("Failed to add album. Creating singles.") kodicursor.execute( "select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 @@ -1792,12 +1805,12 @@ class Music(Items): kodicursor.execute(query, (songid, albumid, track, title, duration)) # Link song to artists - artistLoop = [{ + artist_loop = [{ 'Name': item.attrib.get('grandparentTitle'), 'Id': item.attrib.get('grandparentRatingKey') }] # for index, artist in enumerate(item['ArtistItems']): - for index, artist in enumerate(artistLoop): + for index, artist in enumerate(artist_loop): artist_name = artist['Name'] artist_eid = artist['Id'] @@ -1806,11 +1819,11 @@ class Music(Items): artistid = artist_edb[0] except TypeError: # 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') + artist_xml = GetPlexMetadata(artist_eid) + if artist_xml is None or artist_xml == 401: + LOG.error('Error getting artist, abort') return - self.add_updateArtist(artistXml[0]) + self.add_updateArtist(artist_xml[0]) artist_edb = plex_db.getItem_byId(artist_eid) artistid = artist_edb[0] finally: @@ -1837,87 +1850,27 @@ class Music(Items): ''' kodicursor.execute(query, (artistid, songid, index, artist_name)) - - # Verify if album artist exists - album_artists = [] - # for artist in item['AlbumArtists']: - if False: - artist_name = artist['Name'] - album_artists.append(artist_name) - artist_eid = artist['Id'] - artist_edb = plex_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # 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 = plex_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - query = ''' - INSERT OR REPLACE INTO album_artist( - idArtist, idAlbum, strArtist) - VALUES (?, ?, ?) - ''' - kodicursor.execute(query, (artistid, albumid, artist_name)) - # Update discography - if item.get('Album'): - query = ''' - INSERT OR REPLACE INTO discography( - idArtist, strAlbum, strYear) - VALUES (?, ?, ?) - ''' - kodicursor.execute(query, (artistid, item['Album'], 0)) - # else: - if False: - album_artists = " / ".join(album_artists) - query = ''' - SELECT strArtists - FROM album - WHERE idAlbum = ? - ''' - kodicursor.execute(query, (albumid,)) - result = kodicursor.fetchone() - if result and result[0] != album_artists: - # Field is empty - if v.KODIVERSION >= 16: - # Kodi Jarvis, Krypton - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - elif v.KODIVERSION == 15: - # Kodi Isengard - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - else: - # Kodi Helix - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - # Add genres if genres: self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG) - # Update artwork - allart = API.getAllArtwork(parentInfo=True) + allart = api.getAllArtwork(parentInfo=True) artwork.addArtwork(allart, songid, v.KODI_TYPE_SONG, kodicursor) - if item.get('parentKey') is None: # Update album artwork artwork.addArtwork(allart, albumid, v.KODI_TYPE_ALBUM, kodicursor) def remove(self, itemid): - # Remove kodiid, fileid, pathid, plex reference + """ + Remove kodiid, fileid, pathid, plex reference + """ plex_db = self.plex_db plex_dbitem = plex_db.getItem_byId(itemid) try: kodiid = plex_dbitem[0] mediatype = plex_dbitem[4] - log.info("Removing %s kodiid: %s" % (mediatype, kodiid)) + LOG.info("Removing %s kodiid: %s", mediatype, kodiid) except TypeError: return @@ -1932,7 +1885,7 @@ class Music(Items): # Delete song self.removeSong(kodiid) # This should only address single song scenario, where server doesn't actually - # create an album for the song. + # create an album for the song. plex_db.removeWildItem(itemid) for item in plex_db.getItem_byWildId(itemid): @@ -1948,23 +1901,19 @@ class Music(Items): self.removeAlbum(item_kid) ##### IF ALBUM ##### - elif mediatype == v.KODI_TYPE_ALBUM: # Delete songs, album album_songs = plex_db.getItem_byParentId(kodiid, v.KODI_TYPE_SONG) for song in album_songs: self.removeSong(song[1]) - else: - # Remove plex songs - plex_db.removeItems_byParentId(kodiid, - v.KODI_TYPE_SONG) - + # Remove plex songs + plex_db.removeItems_byParentId(kodiid, + v.KODI_TYPE_SONG) # Remove the album self.removeAlbum(kodiid) ##### IF ARTIST ##### - elif mediatype == v.KODI_TYPE_ARTIST: # Delete songs, album, artist albums = plex_db.getItem_byParentId(kodiid, @@ -1975,36 +1924,43 @@ class Music(Items): v.KODI_TYPE_SONG) for song in album_songs: self.removeSong(song[1]) - else: - # Remove plex song - plex_db.removeItems_byParentId(albumid, - v.KODI_TYPE_SONG) - # Remove plex artist - plex_db.removeItems_byParentId(albumid, - v.KODI_TYPE_ARTIST) - # Remove kodi album - self.removeAlbum(albumid) - else: - # Remove plex albums - plex_db.removeItems_byParentId(kodiid, - v.KODI_TYPE_ALBUM) + # Remove plex song + plex_db.removeItems_byParentId(albumid, + v.KODI_TYPE_SONG) + # Remove plex artist + plex_db.removeItems_byParentId(albumid, + v.KODI_TYPE_ARTIST) + # Remove kodi album + self.removeAlbum(albumid) + # Remove plex albums + plex_db.removeItems_byParentId(kodiid, + v.KODI_TYPE_ALBUM) # Remove artist self.removeArtist(kodiid) - log.info("Deleted %s: %s from kodi database" % (mediatype, itemid)) + LOG.info("Deleted %s: %s from kodi database", mediatype, itemid) def removeSong(self, kodiid): + """ + Remove song, and only the song + """ self.artwork.deleteArtwork(kodiid, v.KODI_TYPE_SONG, self.kodicursor) self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) def removeAlbum(self, kodiid): + """ + Remove an album, and only the album + """ self.artwork.deleteArtwork(kodiid, v.KODI_TYPE_ALBUM, self.kodicursor) self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) def removeArtist(self, kodiid): + """ + Remove an artist, and only the artist + """ self.artwork.deleteArtwork(kodiid, v.KODI_TYPE_ARTIST, self.kodicursor) From 406c2b9f633ae9fab64c3f984eebf5470dd41e3f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 12:12:16 +0100 Subject: [PATCH 311/509] Prettify --- resources/lib/websocket.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py index 6d488e57..8cd3134a 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/websocket.py @@ -504,7 +504,8 @@ class WebSocket(object): self.connected = True - def _validate_header(self, headers, key): + @staticmethod + def _validate_header(headers, key): for k, v in _HEADERS_TO_CHECK.iteritems(): r = headers.get(k, None) if not r: @@ -598,7 +599,7 @@ class WebSocket(object): return value: string(byte array) value. """ - opcode, data = self.recv_data() + _, data = self.recv_data() return data def recv_data(self): @@ -620,7 +621,6 @@ class WebSocket(object): self._cont_data[1] += frame.data else: self._cont_data = [frame.opcode, frame.data] - if frame.fin: data = self._cont_data self._cont_data = None @@ -740,7 +740,7 @@ class WebSocket(object): def _recv(self, bufsize): try: - bytes = self.sock.recv(bufsize) + bytes_ = self.sock.recv(bufsize) except socket.timeout as e: raise WebSocketTimeoutException(e.args[0]) except SSLError as e: @@ -748,17 +748,17 @@ class WebSocket(object): raise WebSocketTimeoutException(e.args[0]) else: raise - if not bytes: + if not bytes_: raise WebSocketConnectionClosedException() - return bytes + return bytes_ def _recv_strict(self, bufsize): shortage = bufsize - sum(len(x) for x in self._recv_buffer) while shortage > 0: - bytes = self._recv(shortage) - self._recv_buffer.append(bytes) - shortage -= len(bytes) + bytes_ = self._recv(shortage) + self._recv_buffer.append(bytes_) + shortage -= len(bytes_) unified = "".join(self._recv_buffer) if shortage == 0: self._recv_buffer = [] @@ -783,7 +783,7 @@ class WebSocketApp(object): Higher level of APIs are provided. The interface is like JavaScript WebSocket object. """ - def __init__(self, url, header=[], + def __init__(self, url, header=None, on_open=None, on_message=None, on_error=None, on_close=None, keep_running=True, get_mask_key=None): """ @@ -807,7 +807,7 @@ class WebSocketApp(object): docstring for more information """ self.url = url - self.header = header + self.header = [] if header is None else header self.on_open = on_open self.on_message = on_message self.on_error = on_error @@ -830,12 +830,12 @@ class WebSocketApp(object): close websocket connection. """ self.keep_running = False - if(self.sock != None): - self.sock.close() + if self.sock != None: + self.sock.close() def _send_ping(self, interval): while True: - for i in range(interval): + for _ in range(interval): time.sleep(1) if not self.keep_running: return @@ -878,8 +878,7 @@ class WebSocketApp(object): if data is None or self.keep_running == False: break - self._callback(self.on_message, data) - + self._callback(self.on_message, data) except Exception, e: #print str(e.args[0]) if "timed out" not in e.args[0]: From ca8ad96a053bc233418df94166780b0f3926ce5b Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 12:59:04 +0100 Subject: [PATCH 312/509] Prettify --- default.py | 4 +- resources/lib/PlexAPI.py | 24 +- resources/lib/PlexFunctions.py | 8 +- resources/lib/artwork.py | 20 +- resources/lib/entrypoint.py | 28 +- resources/lib/initialsetup.py | 10 +- resources/lib/itemtypes.py | 10 +- resources/lib/kodidb_functions.py | 4 +- resources/lib/librarysync.py | 46 ++-- resources/lib/loghandler.py | 4 +- resources/lib/playback.py | 10 +- resources/lib/playlist_func.py | 6 +- resources/lib/playutils.py | 8 +- resources/lib/plex_tv.py | 4 +- resources/lib/plexdb_functions.py | 4 +- resources/lib/utils.py | 429 +++++++++++++----------------- resources/lib/variables.py | 16 +- resources/lib/videonodes.py | 26 +- resources/lib/websocket.py | 2 +- 19 files changed, 301 insertions(+), 362 deletions(-) diff --git a/default.py b/default.py index ec25242f..2da11307 100644 --- a/default.py +++ b/default.py @@ -32,7 +32,7 @@ sys_path.append(_base_resource) ############################################################################### import entrypoint -from utils import window, reset, passwordsXML, language as lang, dialog, \ +from utils import window, reset, passwords_xml, language as lang, dialog, \ plex_command from pickler import unpickle_me, pickl_window from PKC_listitem import convert_PKC_to_listitem @@ -115,7 +115,7 @@ class Main(): entrypoint.resetAuth() elif mode == 'passwords': - passwordsXML() + passwords_xml() elif mode == 'switchuser': entrypoint.switchPlexUser() diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f8d33038..513e9e75 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -40,8 +40,8 @@ from xbmcvfs import exists import clientinfo as client from downloadutils import DownloadUtils as DU -from utils import window, settings, language as lang, tryDecode, tryEncode, \ - DateToKodi, exists_dir, slugify, dialog +from utils import window, settings, language as lang, try_decode, try_encode, \ + unix_date_to_kodi, exists_dir, slugify, dialog import PlexFunctions as PF import plexdb_functions as plexdb import variables as v @@ -139,7 +139,7 @@ class API(): ans = None if ans is not None: try: - ans = tryDecode(unquote(ans)) + ans = try_decode(unquote(ans)) except UnicodeDecodeError: # Sometimes, Plex seems to have encoded in latin1 ans = unquote(ans).decode('latin1') @@ -167,7 +167,7 @@ class API(): self.item[0][0].attrib['key'])) # Attach Plex id to url to let it be picked up by our playqueue agent # later - return tryEncode('%s&plex_id=%s' % (path, self.getRatingKey())) + return try_encode('%s&plex_id=%s' % (path, self.getRatingKey())) def getTVShowPath(self): """ @@ -194,7 +194,7 @@ class API(): """ res = self.item.attrib.get('addedAt') if res is not None: - res = DateToKodi(res) + res = unix_date_to_kodi(res) else: res = '2000-01-01 10:00:00' return res @@ -231,7 +231,7 @@ class API(): played = True if playcount else False try: - lastPlayedDate = DateToKodi(int(item['lastViewedAt'])) + lastPlayedDate = unix_date_to_kodi(int(item['lastViewedAt'])) except (KeyError, ValueError): lastPlayedDate = None @@ -884,7 +884,7 @@ class API(): parameters = { 'api_key': apiKey, 'language': v.KODILANGUAGE, - 'query': tryEncode(title) + 'query': try_encode(title) } data = DU().downloadUrl(url, authenticate=False, @@ -1196,12 +1196,12 @@ class API(): languages.append(stream.attrib['language']) languages = ', '.join(languages) if filename: - option = tryEncode(filename) + option = try_encode(filename) if languages: if option: - option = '%s (%s): ' % (option, tryEncode(languages)) + option = '%s (%s): ' % (option, try_encode(languages)) else: - option = '%s: ' % tryEncode(languages) + option = '%s: ' % try_encode(languages) if 'videoResolution' in entry.attrib: option = '%s%sp ' % (option, entry.attrib.get('videoResolution')) @@ -1544,7 +1544,7 @@ class API(): # exist() needs a / or \ at the end to work for directories if folder is False: # files - check = exists(tryEncode(path)) + check = exists(try_encode(path)) else: # directories if "\\" in path: @@ -1640,7 +1640,7 @@ class API(): plexitem = "plex_%s" % playurl window('%s.runtime' % plexitem, value=str(userdata['Runtime'])) window('%s.type' % plexitem, value=itemtype) - state.PLEX_IDS[tryDecode(playurl)] = self.getRatingKey() + state.PLEX_IDS[try_decode(playurl)] = self.getRatingKey() # window('%s.itemid' % plexitem, value=self.getRatingKey()) window('%s.playcount' % plexitem, value=str(userdata['PlayCount'])) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 3bf7d197..1cbebb88 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -11,7 +11,7 @@ from threading import Thread from xbmc import sleep from downloadutils import DownloadUtils as DU -from utils import settings, tryEncode, tryDecode +from utils import settings, try_encode, try_decode from variables import PLEX_TO_KODI_TIMEFACTOR import plex_tv @@ -306,11 +306,11 @@ def _plex_gdm(): } for line in response['data'].split('\n'): if 'Content-Type:' in line: - pms['product'] = tryDecode(line.split(':')[1].strip()) + pms['product'] = try_decode(line.split(':')[1].strip()) elif 'Host:' in line: pms['baseURL'] = line.split(':')[1].strip() elif 'Name:' in line: - pms['name'] = tryDecode(line.split(':')[1].strip()) + pms['name'] = try_decode(line.split(':')[1].strip()) elif 'Port:' in line: pms['port'] = line.split(':')[1].strip() elif 'Resource-Identifier:' in line: @@ -820,7 +820,7 @@ def transcode_image_path(key, AuthToken, path, width, height): path = 'http://127.0.0.1:32400' + key else: # internal path, add-on path = 'http://127.0.0.1:32400' + path + '/' + key - path = tryEncode(path) + path = try_encode(path) # This is bogus (note the extra path component) but ATV is stupid when it # comes to caching images, it doesn't use querystrings. Fortunately PMS is # lenient... diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 61768b04..a7d2466d 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -12,8 +12,8 @@ import requests from xbmc import sleep, translatePath from xbmcvfs import exists -from utils import window, settings, language as lang, kodiSQL, tryEncode, \ - thread_methods, dialog, exists_dir, tryDecode +from utils import window, settings, language as lang, kodi_sql, try_encode, \ + thread_methods, dialog, exists_dir, try_decode import state # Disable annoying requests warnings @@ -134,13 +134,13 @@ class Artwork(): if dialog('yesno', "Image Texture Cache", lang(39251)): LOG.info("Resetting all cache data first") # Remove all existing textures first - path = tryDecode(translatePath("special://thumbnails/")) + path = try_decode(translatePath("special://thumbnails/")) if exists_dir(path): rmtree(path, ignore_errors=True) self.restoreCacheDirectories() # remove all existing data from texture DB - connection = kodiSQL('texture') + connection = kodi_sql('texture') cursor = connection.cursor() query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' cursor.execute(query, ('table', )) @@ -153,7 +153,7 @@ class Artwork(): connection.close() # Cache all entries in video DB - connection = kodiSQL('video') + connection = kodi_sql('video') cursor = connection.cursor() # dont include actors query = "SELECT url FROM art WHERE media_type != ?" @@ -166,7 +166,7 @@ class Artwork(): for url in result: self.cacheTexture(url[0]) # Cache all entries in music DB - connection = kodiSQL('music') + connection = kodi_sql('music') cursor = connection.cursor() cursor.execute("SELECT url FROM art") result = cursor.fetchall() @@ -179,7 +179,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(tryEncode(url))) + self.queue.put(double_urlencode(try_encode(url))) def addArtwork(self, artwork, kodiId, mediaType, cursor): # Kodi conversion table @@ -323,7 +323,7 @@ class Artwork(): def deleteCachedArtwork(self, url): # Only necessary to remove and apply a new backdrop or poster - connection = kodiSQL('texture') + connection = kodi_sql('texture') cursor = connection.cursor() try: cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", @@ -336,7 +336,7 @@ class Artwork(): path = translatePath("special://thumbnails/%s" % cachedurl) LOG.debug("Deleting cached thumbnail: %s" % path) if exists(path): - rmtree(tryDecode(path), ignore_errors=True) + rmtree(try_decode(path), ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: @@ -347,4 +347,4 @@ class Artwork(): LOG.info("Restoring cache directories...") paths = ("","0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","Video","plex") for p in paths: - makedirs(tryDecode(translatePath("special://thumbnails/%s" % p))) + makedirs(try_decode(translatePath("special://thumbnails/%s" % p))) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 4268f622..619801f6 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -11,8 +11,8 @@ import xbmcplugin from xbmc import sleep, executebuiltin, translatePath from xbmcgui import ListItem -from utils import window, settings, language as lang, dialog, tryEncode, \ - CatchExceptions, exists_dir, plex_command, tryDecode +from utils import window, settings, language as lang, dialog, try_encode, \ + CatchExceptions, exists_dir, plex_command, try_decode import downloadutils from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ @@ -53,11 +53,11 @@ def chooseServer(): if not __LogOut(): return - from utils import deletePlaylists, deleteNodes + from utils import delete_playlists, delete_nodes # First remove playlists - deletePlaylists() + delete_playlists() # Remove video nodes - deleteNodes() + delete_nodes() # Log in again __LogIn() @@ -175,10 +175,10 @@ def switchPlexUser(): return # First remove playlists of old user - from utils import deletePlaylists, deleteNodes - deletePlaylists() + from utils import delete_playlists, delete_nodes + delete_playlists() # Remove video nodes - deleteNodes() + delete_nodes() __LogIn() @@ -455,14 +455,14 @@ def getVideoFiles(plexId, params): if exists_dir(path): for root, dirs, files in walk(path): for directory in dirs: - item_path = tryEncode(join(root, directory)) + item_path = try_encode(join(root, directory)) li = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=item_path, listitem=li, isFolder=True) for file in files: - item_path = tryEncode(join(root, file)) + item_path = try_encode(join(root, file)) li = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=file, @@ -490,7 +490,7 @@ def getExtraFanArt(plexid, plexPath): # We need to store the images locally for this to work # because of the caching system in xbmc - fanartDir = tryDecode(translatePath( + fanartDir = try_decode(translatePath( "special://thumbnails/plex/%s/" % plexid)) if not exists_dir(fanartDir): # Download the images to the cache directory @@ -504,19 +504,19 @@ def getExtraFanArt(plexid, plexPath): backdrops = api.getAllArtwork()['Backdrop'] for count, backdrop in enumerate(backdrops): # Same ordering as in artwork - fanartFile = tryEncode(join(fanartDir, "fanart%.3d.jpg" % count)) + fanartFile = try_encode(join(fanartDir, "fanart%.3d.jpg" % count)) li = ListItem("%.3d" % count, path=fanartFile) xbmcplugin.addDirectoryItem( handle=HANDLE, url=fanartFile, listitem=li) - copyfile(backdrop, tryDecode(fanartFile)) + copyfile(backdrop, try_decode(fanartFile)) else: log.info("Found cached backdrop.") # Use existing cached images for root, dirs, files in walk(fanartDir): for file in files: - fanartFile = tryEncode(join(root, file)) + fanartFile = try_encode(join(root, file)) li = ListItem(file, path=fanartFile) xbmcplugin.addDirectoryItem(handle=HANDLE, url=fanartFile, diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 517ec811..bd756f6b 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -6,7 +6,7 @@ import xml.etree.ElementTree as etree from xbmc import executebuiltin, translatePath -from utils import settings, window, language as lang, tryEncode, tryDecode, \ +from utils import settings, window, language as lang, try_encode, try_decode, \ XmlKodiSetting, reboot_kodi, dialog from migration import check_migration from downloadutils import DownloadUtils as DU @@ -76,7 +76,7 @@ def reload_pkc(): set_webserver() # To detect Kodi profile switches window('plex_kodiProfile', - value=tryDecode(translatePath("special://profile"))) + value=try_decode(translatePath("special://profile"))) getDeviceId() # Initialize the PKC playqueues PQ.init_playqueues() @@ -355,7 +355,7 @@ class InitialSetup(object): dialog('ok', lang(29999), '%s %s' % (lang(39214), - tryEncode(server['name']))) + try_encode(server['name']))) return else: return @@ -610,8 +610,8 @@ class InitialSetup(object): line1=lang(39029), line2=lang(39030)): LOG.debug("Presenting network credentials dialog.") - from utils import passwordsXML - passwordsXML() + from utils import passwords_xml + passwords_xml() # Disable Plex music? if dialog('yesno', heading=lang(29999), line1=lang(39016)): LOG.debug("User opted to disable Plex music library.") diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index b7c5307b..25414cb4 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -6,7 +6,7 @@ from ntpath import dirname from datetime import datetime from artwork import Artwork -from utils import window, kodiSQL, CatchExceptions +from utils import window, kodi_sql, CatchExceptions import plexdb_functions as plexdb import kodidb_functions as kodidb @@ -43,9 +43,9 @@ class Items(object): """ Open DB connections and cursors """ - self.plexconn = kodiSQL('plex') + self.plexconn = kodi_sql('plex') self.plexcursor = self.plexconn.cursor() - self.kodiconn = kodiSQL('video') + self.kodiconn = kodi_sql('video') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) @@ -1273,10 +1273,10 @@ class Music(Items): OVERWRITE this method, because we need to open another DB. Open DB connections and cursors """ - self.plexconn = kodiSQL('plex') + self.plexconn = kodi_sql('plex') self.plexcursor = self.plexconn.cursor() # Here it is, not 'video' but 'music' - self.kodiconn = kodiSQL('music') + self.kodiconn = kodi_sql('music') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 4b8b1d87..5390c8e6 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -5,7 +5,7 @@ from logging import getLogger from ntpath import dirname import artwork -from utils import kodiSQL +from utils import kodi_sql import variables as v ############################################################################### @@ -30,7 +30,7 @@ class GetKodiDB(): self.db_type = db_type def __enter__(self): - self.kodiconn = kodiSQL(self.db_type) + self.kodiconn = kodi_sql(self.db_type) kodi_db = Kodidb_Functions(self.kodiconn.cursor()) return kodi_db diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 55983170..150fa594 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -8,10 +8,10 @@ from random import shuffle import xbmc from xbmcvfs import exists -from utils import window, settings, getUnixTimestamp, \ - thread_methods, create_actor_db_index, dialog, LogTime, playlistXSP,\ - language as lang, DateToKodi, reset, tryDecode, deletePlaylists, \ - deleteNodes, tryEncode, compare_version +from utils import window, settings, unix_timestamp, thread_methods, \ + create_actor_db_index, dialog, LogTime, playlist_xsp, language as lang, \ + unix_date_to_kodi, reset, try_decode, delete_playlists, delete_nodes, \ + try_encode, compare_version import downloadutils import itemtypes import plexdb_functions as plexdb @@ -155,7 +155,7 @@ class LibrarySync(Thread): log.debug('No timestamp; using 0') # Set the timer - koditime = getUnixTimestamp() + koditime = unix_timestamp() # Toggle watched state scrobble(plexId, 'watched') # Let the PMS process this first! @@ -329,7 +329,7 @@ class LibrarySync(Thread): # Create playlist for the video library if (foldername not in playlists and mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - playlistXSP(mediatype, foldername, folderid, viewtype) + playlist_xsp(mediatype, foldername, folderid, viewtype) playlists.append(foldername) # Create the video node if (foldername not in nodes and @@ -371,7 +371,7 @@ class LibrarySync(Thread): # The tag could be a combined view. Ensure there's # no other tags with the same name before deleting # playlist. - playlistXSP(mediatype, + playlist_xsp(mediatype, current_viewname, folderid, current_viewtype, @@ -388,7 +388,7 @@ class LibrarySync(Thread): # Added new playlist if (foldername not in playlists and mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - playlistXSP(mediatype, + playlist_xsp(mediatype, foldername, folderid, viewtype) @@ -414,7 +414,7 @@ class LibrarySync(Thread): if mediatype != v.PLEX_TYPE_ARTIST: if (foldername not in playlists and mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): - playlistXSP(mediatype, + playlist_xsp(mediatype, foldername, folderid, viewtype) @@ -1102,7 +1102,7 @@ class LibrarySync(Thread): """ self.videoLibUpdate = False self.musicLibUpdate = False - now = getUnixTimestamp() + now = unix_timestamp() deleteListe = [] for i, item in enumerate(self.itemsToProcess): if self.thread_stopped() or self.thread_suspended(): @@ -1220,7 +1220,7 @@ class LibrarySync(Thread): 'state': status, 'type': typus, 'ratingKey': str(item['itemID']), - 'timestamp': getUnixTimestamp(), + 'timestamp': unix_timestamp(), 'attempt': 0 }) elif typus in (v.PLEX_TYPE_MOVIE, @@ -1237,7 +1237,7 @@ class LibrarySync(Thread): 'state': status, 'type': typus, 'ratingKey': plex_id, - 'timestamp': getUnixTimestamp(), + 'timestamp': unix_timestamp(), 'attempt': 0 }) @@ -1276,7 +1276,7 @@ class LibrarySync(Thread): 'state': None, # Don't need a state here 'type': kodi_info[5], 'ratingKey': plex_id, - 'timestamp': getUnixTimestamp(), + 'timestamp': unix_timestamp(), 'attempt': 0 }) @@ -1386,7 +1386,7 @@ class LibrarySync(Thread): resume, session['duration'], session['file_id'], - DateToKodi(getUnixTimestamp())) + unix_date_to_kodi(unix_timestamp())) def fanartSync(self, refresh=False): """ @@ -1430,9 +1430,9 @@ class LibrarySync(Thread): window('plex_dbScan', value="true") state.DB_SCAN = True # First remove playlists - deletePlaylists() + delete_playlists() # Remove video nodes - deleteNodes() + delete_nodes() # Kick off refresh if self.maintainViews() is True: # Ran successfully @@ -1549,11 +1549,11 @@ class LibrarySync(Thread): # Also runs when first installed # Verify the video database can be found videoDb = v.DB_VIDEO_PATH - if not exists(tryEncode(videoDb)): + if not exists(try_encode(videoDb)): # Database does not exists log.error("The current Kodi version is incompatible " "to know which Kodi versions are supported.") - log.error('Current Kodi version: %s' % tryDecode( + log.error('Current Kodi version: %s' % try_decode( xbmc.getInfoLabel('System.BuildVersion'))) # "Current Kodi version is unsupported, cancel lib sync" dialog('ok', heading='{plex}', line1=lang(39403)) @@ -1562,10 +1562,10 @@ class LibrarySync(Thread): state.DB_SCAN = True window('plex_dbScan', value="true") log.info("Db version: %s" % settings('dbCreatedWithVersion')) - lastTimeSync = getUnixTimestamp() + lastTimeSync = unix_timestamp() # Initialize time offset Kodi - PMS self.syncPMStime() - lastSync = getUnixTimestamp() + lastSync = unix_timestamp() if settings('FanartTV') == 'true': # Start getting additional missing artwork with plexdb.Get_Plex_DB() as plex_db: @@ -1579,8 +1579,8 @@ class LibrarySync(Thread): 'refresh': True }) log.info('Refreshing video nodes and playlists now') - deletePlaylists() - deleteNodes() + delete_playlists() + delete_nodes() log.info("Initial start-up full sync starting") librarySync = fullSync() window('plex_dbScan', clear=True) @@ -1604,7 +1604,7 @@ class LibrarySync(Thread): self.triage_lib_scans() self.force_dialog = False continue - now = getUnixTimestamp() + now = unix_timestamp() # Standard syncs - don't force-show dialogs self.force_dialog = False if (now - lastSync > FULL_SYNC_INTERVALL and diff --git a/resources/lib/loghandler.py b/resources/lib/loghandler.py index f81c962d..5a91a214 100644 --- a/resources/lib/loghandler.py +++ b/resources/lib/loghandler.py @@ -12,7 +12,7 @@ LEVELS = { ############################################################################### -def tryEncode(uniString, encoding='utf-8'): +def try_encode(uniString, encoding='utf-8'): """ Will try to encode uniString (in unicode) to encoding. This possibly fails with e.g. Android TV's Python, which does not accept arguments for @@ -43,5 +43,5 @@ class LogHandler(logging.StreamHandler): try: xbmc.log(self.format(record), level=LEVELS[record.levelno]) except UnicodeEncodeError: - xbmc.log(tryEncode(self.format(record)), + xbmc.log(try_encode(self.format(record)), level=LEVELS[record.levelno]) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index ed8747de..35010d17 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -18,7 +18,7 @@ from playutils import PlayUtils from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful import json_rpc as js -from utils import settings, dialog, language as lang, tryEncode +from utils import settings, dialog, language as lang, try_encode from plexbmchelper.subscribers import LOCKER import variables as v import state @@ -167,7 +167,7 @@ def _prep_playlist_stack(xml): path = ('plugin://plugin.video.plexkodiconnect?%s' % urlencode(params)) listitem = api.CreateListItemFromPlexItem() - listitem.setPath(tryEncode(path)) + listitem.setPath(try_encode(path)) else: # Will add directly via the Kodi DB path = None @@ -244,7 +244,7 @@ def conclude_playback(playqueue, pos): playurl = playutils.getPlayUrl() else: playurl = item.file - listitem.setPath(tryEncode(playurl)) + listitem.setPath(try_encode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): listitem.setSubtitles(api.externalSubs()) else: @@ -322,14 +322,14 @@ def process_indirect(key, offset, resolve=True): return playurl = xml[0].attrib['key'] item.file = playurl - listitem.setPath(tryEncode(playurl)) + listitem.setPath(try_encode(playurl)) playqueue.items.append(item) if resolve is True: result.listitem = listitem pickle_me(result) else: thread = Thread(target=Player().play, - args={'item': tryEncode(playurl), + args={'item': try_encode(playurl), 'listitem': listitem}) thread.setDaemon(True) LOG.info('Done initializing PKC playback, starting Kodi player') diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a233c6a0..041414ea 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -8,7 +8,7 @@ from re import compile as re_compile import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU -from utils import tryEncode, escape_html +from utils import try_encode, escape_html from PlexAPI import API from PlexFunctions import GetPlexMetadata import json_rpc as js @@ -60,7 +60,7 @@ class PlaylistObjectBaseclase(object): continue if isinstance(getattr(self, key), (str, unicode)): answ += '\'%s\': \'%s\', ' % (key, - tryEncode(getattr(self, key))) + try_encode(getattr(self, key))) else: # e.g. int answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) @@ -168,7 +168,7 @@ class Playlist_Item(object): continue if isinstance(getattr(self, key), (str, unicode)): answ += '\'%s\': \'%s\', ' % (key, - tryEncode(getattr(self, key))) + try_encode(getattr(self, key))) else: # e.g. int answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 7fbadede..b4753532 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -4,7 +4,7 @@ from logging import getLogger from downloadutils import DownloadUtils as DU -from utils import window, settings, language as lang, dialog, tryEncode +from utils import window, settings, language as lang, dialog, try_encode import variables as v ############################################################################### @@ -274,7 +274,7 @@ class PlayUtils(): codec, channellayout) audio_streams_list.append(index) - audio_streams.append(tryEncode(track)) + audio_streams.append(try_encode(track)) audio_numb += 1 # Subtitles @@ -306,7 +306,7 @@ class PlayUtils(): "%s%s" % (window('pms_server'), stream.attrib['key'])) downloadable_streams.append(index) - download_subs.append(tryEncode(path)) + download_subs.append(try_encode(path)) else: track = "%s (%s)" % (track, lang(39710)) # burn-in if stream.attrib.get('selected') == '1' and downloadable: @@ -315,7 +315,7 @@ class PlayUtils(): default_sub = index subtitle_streams_list.append(index) - subtitle_streams.append(tryEncode(track)) + subtitle_streams.append(try_encode(track)) sub_num += 1 if audio_numb > 1: diff --git a/resources/lib/plex_tv.py b/resources/lib/plex_tv.py index f805bee5..fa28cb67 100644 --- a/resources/lib/plex_tv.py +++ b/resources/lib/plex_tv.py @@ -4,7 +4,7 @@ from logging import getLogger from xbmc import sleep, executebuiltin from downloadutils import DownloadUtils as DU -from utils import dialog, language as lang, settings, tryEncode +from utils import dialog, language as lang, settings, try_encode import variables as v import state @@ -39,7 +39,7 @@ def choose_home_user(token): username = user['title'] userlist.append(username) # To take care of non-ASCII usernames - userlist_coded.append(tryEncode(username)) + userlist_coded.append(try_encode(username)) usernumber = len(userlist) username = '' usertoken = '' diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index 239d25df..742a461d 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -3,7 +3,7 @@ ############################################################################### from logging import getLogger -from utils import kodiSQL +from utils import kodi_sql import variables as v ############################################################################### @@ -22,7 +22,7 @@ class Get_Plex_DB(): and the db gets closed """ def __enter__(self): - self.plexconn = kodiSQL('plex') + self.plexconn = kodi_sql('plex') return Plex_DB_Functions(self.plexconn.cursor()) def __exit__(self, type, value, traceback): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 6ff5f68b..3c40057c 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- - +""" +Various functions and decorators for PKC +""" ############################################################################### from logging import getLogger from cProfile import Profile @@ -7,7 +9,7 @@ from pstats import Stats from sqlite3 import connect, OperationalError from datetime import datetime, timedelta from StringIO import StringIO -from time import localtime, strftime, strptime +from time import localtime, strftime from unicodedata import normalize import xml.etree.ElementTree as etree from functools import wraps, partial @@ -21,12 +23,13 @@ import xbmc import xbmcaddon import xbmcgui from xbmcvfs import exists, delete + import variables as v import state ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') @@ -46,7 +49,8 @@ def reboot_kodi(message=None): dialog('ok', heading='{plex}', line1=message) xbmc.executebuiltin('RestartApp') -def window(property, value=None, clear=False, windowid=10000): + +def window(prop, value=None, clear=False, windowid=10000): """ Get or set window property - thread safe! @@ -60,11 +64,11 @@ def window(property, value=None, clear=False, windowid=10000): win = WINDOW if clear: - win.clearProperty(property) + win.clearProperty(prop) elif value is not None: - win.setProperty(tryEncode(property), tryEncode(value)) + win.setProperty(try_encode(prop), try_encode(value)) else: - return tryDecode(win.getProperty(property)) + return try_decode(win.getProperty(prop)) def plex_command(key, value): @@ -90,10 +94,10 @@ def settings(setting, value=None): addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') if value is not None: # Takes string or unicode by default! - addon.setSetting(tryEncode(setting), tryEncode(value)) + addon.setSetting(try_encode(setting), try_encode(value)) else: # Should return unicode by default, but just in case - return tryDecode(addon.getSetting(setting)) + return try_decode(addon.getSetting(setting)) def exists_dir(path): @@ -104,24 +108,26 @@ def exists_dir(path): Feed with encoded string or unicode """ if v.KODIVERSION >= 17: - answ = exists(tryEncode(path)) + answ = exists(try_encode(path)) else: - dummyfile = join(tryDecode(path), 'dummyfile.txt') + dummyfile = join(try_decode(path), 'dummyfile.txt') try: - with open(dummyfile, 'w') as f: - f.write('text') + with open(dummyfile, 'w') as filer: + filer.write('text') except IOError: # folder does not exist yet answ = 0 else: # Folder exists. Delete file again. - delete(tryEncode(dummyfile)) + delete(try_encode(dummyfile)) answ = 1 return answ def language(stringid): - # Central string retrieval + """ + Central string retrieval from strings.po + """ return ADDON.getLocalizedString(stringid) @@ -236,7 +242,7 @@ def kodi_time_to_millis(time): return ret -def tryEncode(uniString, encoding='utf-8'): +def try_encode(uniString, encoding='utf-8'): """ Will try to encode uniString (in unicode) to encoding. This possibly fails with e.g. Android TV's Python, which does not accept arguments for @@ -252,7 +258,7 @@ def tryEncode(uniString, encoding='utf-8'): return uniString -def tryDecode(string, encoding='utf-8'): +def try_decode(string, encoding='utf-8'): """ Will try to decode string (encoded) using encoding. This possibly fails with e.g. Android TV's Python, which does not accept arguments for @@ -295,7 +301,7 @@ def escape_html(string): return string -def DateToKodi(stamp): +def unix_date_to_kodi(stamp): """ converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a propper, human-readable time stamp used by Kodi @@ -313,49 +319,42 @@ def DateToKodi(stamp): return localdate -def IntFromStr(string): - """ - Returns an int from string or the int 0 if something happened - """ - try: - result = int(string) - except: - result = 0 - return result - - -def getUnixTimestamp(secondsIntoTheFuture=None): +def unix_timestamp(seconds_into_the_future=None): """ Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as an integer. - Optionally, pass secondsIntoTheFuture: positive int's will result in a + Optionally, pass seconds_into_the_future: positive int's will result in a future timestamp, negative the past """ - if secondsIntoTheFuture: - future = datetime.utcnow() + timedelta(seconds=secondsIntoTheFuture) + if seconds_into_the_future: + future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future) else: future = datetime.utcnow() return timegm(future.timetuple()) -def kodiSQL(media_type="video"): +def kodi_sql(media_type=None): + """ + Open a connection to the Kodi database. + media_type: 'video' (standard if not passed), 'plex', 'music', 'texture' + """ if media_type == "plex": - dbPath = v.DB_PLEX_PATH + db_path = v.DB_PLEX_PATH elif media_type == "music": - dbPath = v.DB_MUSIC_PATH + db_path = v.DB_MUSIC_PATH elif media_type == "texture": - dbPath = v.DB_TEXTURE_PATH + db_path = v.DB_TEXTURE_PATH else: - dbPath = v.DB_VIDEO_PATH - return connect(dbPath, timeout=60.0) + db_path = v.DB_VIDEO_PATH + return connect(db_path, timeout=60.0) def create_actor_db_index(): """ Index the "actors" because we got a TON - speed up SELECT and WHEN """ - conn = kodiSQL('video') + conn = kodi_sql('video') cursor = conn.cursor() try: cursor.execute(""" @@ -370,6 +369,10 @@ def create_actor_db_index(): def reset(): + """ + User navigated to the PKC settings, Advanced, and wants to reset the Kodi + database and possibly PKC entirely + """ # Are you sure you want to reset your local Kodi database? if not dialog('yesno', heading='{plex} %s ' % language(30132), @@ -380,7 +383,7 @@ def reset(): plex_command('STOP_SYNC', 'True') count = 10 while window('plex_dbScan') == "true": - log.debug("Sync is running, will retry: %s..." % count) + LOG.debug("Sync is running, will retry: %s...", count) count -= 1 if count == 0: # Could not stop the database from running. Please try again later. @@ -391,14 +394,14 @@ def reset(): xbmc.sleep(1000) # Clean up the playlists - deletePlaylists() + delete_playlists() # Clean up the video nodes - deleteNodes() + delete_nodes() # Wipe the kodi databases - log.info("Resetting the Kodi video database.") - connection = kodiSQL('video') + LOG.info("Resetting the Kodi video database.") + connection = kodi_sql('video') cursor = connection.cursor() cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor.fetchall() @@ -410,8 +413,8 @@ def reset(): cursor.close() if settings('enableMusic') == "true": - log.info("Resetting the Kodi music database.") - connection = kodiSQL('music') + LOG.info("Resetting the Kodi music database.") + connection = kodi_sql('music') cursor = connection.cursor() cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor.fetchall() @@ -423,8 +426,8 @@ def reset(): cursor.close() # Wipe the Plex database - log.info("Resetting the Plex database.") - connection = kodiSQL('plex') + LOG.info("Resetting the Plex database.") + connection = kodi_sql('plex') cursor = connection.cursor() cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor.fetchall() @@ -441,21 +444,21 @@ def reset(): if dialog('yesno', heading='{plex} %s ' % language(30132), line1=language(39602)): - log.info("Resetting all cached artwork.") + LOG.info("Resetting all cached artwork.") # Remove all existing textures first path = xbmc.translatePath("special://thumbnails/") if exists(path): - rmtree(tryDecode(path), ignore_errors=True) + rmtree(try_decode(path), ignore_errors=True) # remove all existing data from texture DB - connection = kodiSQL('texture') + connection = kodi_sql('texture') cursor = connection.cursor() query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' cursor.execute(query, ("table", )) rows = cursor.fetchall() for row in rows: - tableName = row[0] - if(tableName != "version"): - cursor.execute("DELETE FROM %s" % tableName) + table_name = row[0] + if table_name != "version": + cursor.execute("DELETE FROM %s" % table_name) connection.commit() cursor.close() @@ -469,44 +472,36 @@ def reset(): line1=language(39603)): # Delete the settings addon = xbmcaddon.Addon() - addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile'))) - dataPath = "%ssettings.xml" % addondir - log.info("Deleting: settings.xml") - remove(dataPath) + addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile'))) + LOG.info("Deleting: settings.xml") + remove("%ssettings.xml" % addondir) reboot_kodi() def profiling(sortby="cumulative"): - # Will print results to Kodi log + """ + Will print results to Kodi log. Must be enabled in the Python source code + """ def decorator(func): + """ + decorator construct + """ def wrapper(*args, **kwargs): - - pr = Profile() - - pr.enable() + """ + wrapper construct + """ + profile = Profile() + profile.enable() result = func(*args, **kwargs) - pr.disable() - - s = StringIO() - ps = Stats(pr, stream=s).sort_stats(sortby) - ps.print_stats() - log.info(s.getvalue()) - + profile.disable() + string_io = StringIO() + stats = Stats(profile, stream=string_io).sort_stats(sortby) + stats.print_stats() + LOG.info(string_io.getvalue()) return result - return wrapper return decorator -def convertdate(date): - try: - date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") - except TypeError: - # TypeError: attribute of type 'NoneType' is not callable - # Known Kodi/python error - date = datetime(*(strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) - - return date - def compare_version(current, minimum): """ @@ -515,38 +510,36 @@ def compare_version(current, minimum): Input strings: e.g. "1.2.3"; always with Major, Minor and Patch! """ - log.info("current DB: %s minimum DB: %s" % (current, minimum)) + LOG.info("current DB: %s minimum DB: %s", current, minimum) try: - currMajor, currMinor, currPatch = current.split(".") + curr_major, curr_minor, curr_patch = current.split(".") except ValueError: # there WAS no current DB, e.g. deleted. return True - minMajor, minMinor, minPatch = minimum.split(".") - currMajor = int(currMajor) - currMinor = int(currMinor) - currPatch = int(currPatch) - minMajor = int(minMajor) - minMinor = int(minMinor) - minPatch = int(minPatch) + min_major, min_minor, min_patch = minimum.split(".") + curr_major = int(curr_major) + curr_minor = int(curr_minor) + curr_patch = int(curr_patch) + min_major = int(min_major) + min_minor = int(min_minor) + min_patch = int(min_patch) - if currMajor > minMajor: + if curr_major > min_major: return True - elif currMajor < minMajor: + elif curr_major < min_major: return False - if currMinor > minMinor: + if curr_minor > min_minor: return True - elif currMinor < minMinor: - return False - - if currPatch >= minPatch: - return True - else: + elif curr_minor < min_minor: return False + return curr_patch >= min_patch def normalize_nodes(text): - # For video nodes + """ + For video nodes + """ text = text.replace(":", "") text = text.replace("/", "-") text = text.replace("\\", "-") @@ -561,13 +554,15 @@ def normalize_nodes(text): # Remove dots from the last character as windows can not have directories # with dots at the end text = text.rstrip('.') - text = tryEncode(normalize('NFKD', unicode(text, 'utf-8'))) + text = try_encode(normalize('NFKD', unicode(text, 'utf-8'))) return text + def normalize_string(text): - # For theme media, do not modify unless - # modified in TV Tunes + """ + For theme media, do not modify unless modified in TV Tunes + """ text = text.replace(":", "") text = text.replace("/", "-") text = text.replace("\\", "-") @@ -580,7 +575,7 @@ def normalize_string(text): # Remove dots from the last character as windows can not have directories # with dots at the end text = text.rstrip('.') - text = tryEncode(normalize('NFKD', unicode(text, 'utf-8'))) + text = try_encode(normalize('NFKD', unicode(text, 'utf-8'))) return text @@ -595,8 +590,8 @@ def indent(elem, level=0): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i - for elem in elem: - indent(elem, level+1) + for item in elem: + indent(item, level+1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: @@ -604,30 +599,6 @@ def indent(elem, level=0): elem.tail = i -def guisettingsXML(): - """ - Returns special://userdata/guisettings.xml as an etree xml root element - """ - path = tryDecode(xbmc.translatePath("special://profile/")) - xmlpath = "%sguisettings.xml" % path - - try: - xmlparse = etree.parse(xmlpath) - except IOError: - # Document is blank or missing - root = etree.Element('settings') - except etree.ParseError: - log.error('Error parsing %s' % xmlpath) - # "Kodi cannot parse {0}. PKC will not function correctly. Please visit - # {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( - 'guisettings.xml', 'http://kodi.wiki/view/userdata')) - return - else: - root = xmlparse.getroot() - return root - - class XmlKodiSetting(object): """ Used to load a Kodi XML settings file from special://profile as an etree @@ -670,7 +641,7 @@ class XmlKodiSetting(object): except IOError: # Document is blank or missing if self.force_create is False: - log.debug('%s does not seem to exist; not creating', self.path) + LOG.debug('%s does not seem to exist; not creating', self.path) # This will abort __enter__ self.__exit__(IOError, None, None) # Create topmost xml entry @@ -678,7 +649,7 @@ class XmlKodiSetting(object): element=etree.Element(self.top_element)) self.write_xml = True except etree.ParseError: - log.error('Error parsing %s', self.path) + LOG.error('Error parsing %s', self.path) # "Kodi cannot parse {0}. PKC will not function correctly. Please # visit {1} and correct your file!" dialog('ok', language(29999), language(39716).format( @@ -775,7 +746,7 @@ class XmlKodiSetting(object): elif old.attrib != attrib: already_set = False if already_set is True: - log.debug('Element has already been found') + LOG.debug('Element has already been found') return old # Need to set new setting, indeed self.write_xml = True @@ -790,34 +761,35 @@ class XmlKodiSetting(object): return element -def passwordsXML(): - # To add network credentials - path = tryDecode(xbmc.translatePath("special://userdata/")) +def passwords_xml(): + """ + To add network credentials to Kodi's password xml + """ + path = try_decode(xbmc.translatePath("special://userdata/")) xmlpath = "%spasswords.xml" % path - dialog = xbmcgui.Dialog() - try: xmlparse = etree.parse(xmlpath) except IOError: # Document is blank or missing root = etree.Element('passwords') - skipFind = True + skip_find = True except etree.ParseError: - log.error('Error parsing %s' % xmlpath) + LOG.error('Error parsing %s', xmlpath) # "Kodi cannot parse {0}. PKC will not function correctly. Please visit # {1} and correct your file!" - dialog.ok(language(29999), language(39716).format( + dialog('ok', language(29999), language(39716).format( 'passwords.xml', 'http://forum.kodi.tv/')) return else: root = xmlparse.getroot() - skipFind = False + skip_find = False credentials = settings('networkCreds') if credentials: # Present user with options - option = dialog.select( - "Modify/Remove network credentials", ["Modify", "Remove"]) + option = dialog('select', + "Modify/Remove network credentials", + ["Modify", "Remove"]) if option < 0: # User cancelled dialog @@ -825,76 +797,86 @@ def passwordsXML(): elif option == 1: # User selected remove + success = False for paths in root.getiterator('passwords'): for path in paths: if path.find('.//from').text == "smb://%s/" % credentials: paths.remove(path) - log.info("Successfully removed credentials for: %s" - % credentials) + LOG.info("Successfully removed credentials for: %s", + credentials) etree.ElementTree(root).write(xmlpath, encoding="UTF-8") - break - else: - log.error("Failed to find saved server: %s in passwords.xml" - % credentials) - + success = True + if not success: + LOG.error("Failed to find saved server: %s in passwords.xml", + credentials) + dialog('notification', + heading='{plex}', + message="%s not found" % credentials, + icon='{warning}', + sound=False) + return settings('networkCreds', value="") - xbmcgui.Dialog().notification( - heading='PlexKodiConnect', - message="%s removed from passwords.xml" % credentials, - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - time=1000, - sound=False) + dialog('notification', + heading='{plex}', + message="%s removed from passwords.xml" % credentials, + icon='{plex}', + sound=False) return elif option == 0: # User selected to modify - server = dialog.input("Modify the computer name or ip address", credentials) + server = dialog('input', + "Modify the computer name or ip address", + credentials) if not server: return else: # No credentials added - dialog.ok( - heading="Network credentials", - line1= ( - "Input the server name or IP address as indicated in your plex library paths. " - 'For example, the server name: \\\\SERVER-PC\\path\\ or smb://SERVER-PC/path is "SERVER-PC".')) - server = dialog.input("Enter the server name or IP address") + dialog('ok', + "Network credentials", + 'Input the server name or IP address as indicated in your plex ' + 'library paths. For example, the server name: ' + '\\\\SERVER-PC\\path\\ or smb://SERVER-PC/path is SERVER-PC') + server = dialog('input', "Enter the server name or IP address") if not server: return server = quote_plus(server) # Network username - user = dialog.input("Enter the network username") + user = dialog('input', "Enter the network username") if not user: return user = quote_plus(user) # Network password - password = dialog.input("Enter the network password", - '', # Default input - xbmcgui.INPUT_ALPHANUM, - xbmcgui.ALPHANUM_HIDE_INPUT) + password = dialog('input', + "Enter the network password", + '', # Default input + type='{alphanum}', + option='{hide}') # Need to url-encode the password password = quote_plus(password) # Add elements. Annoying etree bug where findall hangs forever - if skipFind is False: - skipFind = True + if skip_find is False: + skip_find = True for path in root.findall('.//path'): if path.find('.//from').text.lower() == "smb://%s/" % server.lower(): # Found the server, rewrite credentials - path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server) - skipFind = False + path.find('.//to').text = ("smb://%s:%s@%s/" + % (user, password, server)) + skip_find = False break - if skipFind: + if skip_find: # Server not found, add it. path = etree.SubElement(root, 'path') - etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server + etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = \ + "smb://%s/" % server topath = "smb://%s:%s@%s/" % (user, password, server) etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath # Add credentials settings('networkCreds', value="%s" % server) - log.info("Added server: %s to passwords.xml" % server) + LOG.info("Added server: %s to passwords.xml", server) # Prettify and write to file try: indent(root) @@ -902,19 +884,12 @@ def passwordsXML(): pass etree.ElementTree(root).write(xmlpath, encoding="UTF-8") - # dialog.notification( - # heading="PlexKodiConnect", - # message="Added to passwords.xml", - # icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - # time=5000, - # sound=False) - -def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): +def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False): """ Feed with tagname as unicode """ - path = tryDecode(xbmc.translatePath("special://profile/playlists/video/")) + path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) if viewtype == "mixed": plname = "%s - %s" % (tagname, mediatype) xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype) @@ -923,16 +898,16 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): xsppath = "%sPlex %s.xsp" % (path, viewid) # Create the playlist directory - if not exists(tryEncode(path)): - log.info("Creating directory: %s" % path) + if not exists(try_encode(path)): + LOG.info("Creating directory: %s", path) makedirs(path) # Only add the playlist if it doesn't already exists - if exists(tryEncode(xsppath)): - log.info('Path %s does exist' % xsppath) + if exists(try_encode(xsppath)): + LOG.info('Path %s does exist', xsppath) if delete: remove(xsppath) - log.info("Successfully removed playlist: %s." % tagname) + LOG.info("Successfully removed playlist: %s.", tagname) return # Using write process since there's no guarantee the xml declaration works @@ -942,9 +917,9 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): 'movie': 'movies', 'show': 'tvshows' } - log.info("Writing playlist file to: %s" % xsppath) - with open(xsppath, 'wb') as f: - f.write(tryEncode( + LOG.info("Writing playlist file to: %s", xsppath) + with open(xsppath, 'wb') as filer: + filer.write(try_encode( '\n' '\n\t' 'Plex %s\n\t' @@ -954,20 +929,24 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): '\n' '\n' % (itemtypes.get(mediatype, mediatype), plname, tagname))) - log.info("Successfully added playlist: %s" % tagname) + LOG.info("Successfully added playlist: %s", tagname) -def deletePlaylists(): - # Clean up the playlists - path = tryDecode(xbmc.translatePath("special://profile/playlists/video/")) +def delete_playlists(): + """ + Clean up the playlists + """ + path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) for root, _, files in walk(path): for file in files: if file.startswith('Plex'): remove(join(root, file)) -def deleteNodes(): - # Clean up video nodes - path = tryDecode(xbmc.translatePath("special://profile/library/video/")) +def delete_nodes(): + """ + Clean up video nodes + """ + path = try_decode(xbmc.translatePath("special://profile/library/video/")) for root, dirs, _ in walk(path): for directory in dirs: if directory.startswith('Plex-'): @@ -993,9 +972,9 @@ def CatchExceptions(warnuser=False): try: return func(*args, **kwargs) except Exception as e: - log.error('%s has crashed. Error: %s' % (func.__name__, e)) + LOG.error('%s has crashed. Error: %s', func.__name__, e) import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) + LOG.error("Traceback:\n%s", traceback.format_exc()) if warnuser: window('plex_scancrashed', value='true') return @@ -1012,8 +991,8 @@ def LogTime(func): starttotal = datetime.now() result = func(*args, **kwargs) elapsedtotal = datetime.now() - starttotal - log.info('It took %s to run the function %s' - % (elapsedtotal, func.__name__)) + LOG.info('It took %s to run the function %s', + elapsedtotal, func.__name__) return result return wrapper @@ -1117,43 +1096,3 @@ class Lock_Function(object): result = func(*args, **kwargs) return result return wrapper - -############################################################################### -# UNUSED METHODS - - -# def changePlayState(itemType, kodiId, playCount, lastplayed): -# """ -# YET UNUSED - -# kodiId: int or str -# playCount: int or str -# lastplayed: str or int unix timestamp -# """ -# lastplayed = DateToKodi(lastplayed) - -# kodiId = int(kodiId) -# playCount = int(playCount) -# method = { -# 'movie': ' VideoLibrary.SetMovieDetails', -# 'episode': 'VideoLibrary.SetEpisodeDetails', -# 'musicvideo': ' VideoLibrary.SetMusicVideoDetails', # TODO -# 'show': 'VideoLibrary.SetTVShowDetails', # TODO -# '': 'AudioLibrary.SetAlbumDetails', # TODO -# '': 'AudioLibrary.SetArtistDetails', # TODO -# 'track': 'AudioLibrary.SetSongDetails' -# } -# params = { -# 'movie': { -# 'movieid': kodiId, -# 'playcount': playCount, -# 'lastplayed': lastplayed -# }, -# 'episode': { -# 'episodeid': kodiId, -# 'playcount': playCount, -# 'lastplayed': lastplayed -# } -# } -# result = jsonrpc(method[itemType]).execute(params[itemType]) -# log.debug("JSON result was: %s" % result) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 4a7f6d33..fef8e4ca 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -6,7 +6,7 @@ from xbmcaddon import Addon # For any file operations with KODI function, use encoded strings! -def tryDecode(string, encoding='utf-8'): +def try_decode(string, encoding='utf-8'): """ Will try to decode string (encoded) using encoding. This possibly fails with e.g. Android TV's Python, which does not accept arguments for @@ -37,7 +37,7 @@ ADDON_VERSION = _ADDON.getAddonInfo('version') KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') -KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile")) +KODI_PROFILE = try_decode(xbmc.translatePath("special://profile")) if xbmc.getCondVisibility('system.platform.osx'): PLATFORM = "MacOSX" @@ -56,7 +56,7 @@ elif xbmc.getCondVisibility('system.platform.android'): else: PLATFORM = "Unknown" -DEVICENAME = tryDecode(_ADDON.getSetting('deviceName')) +DEVICENAME = try_decode(_ADDON.getSetting('deviceName')) DEVICENAME = DEVICENAME.replace(":", "") DEVICENAME = DEVICENAME.replace("/", "-") DEVICENAME = DEVICENAME.replace("\\", "-") @@ -86,7 +86,7 @@ _DB_VIDEO_VERSION = { 17: 107, # Krypton 18: 108 # Leia } -DB_VIDEO_PATH = tryDecode(xbmc.translatePath( +DB_VIDEO_PATH = try_decode(xbmc.translatePath( "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])) _DB_MUSIC_VERSION = { @@ -97,7 +97,7 @@ _DB_MUSIC_VERSION = { 17: 60, # Krypton 18: 62 # Leia } -DB_MUSIC_PATH = tryDecode(xbmc.translatePath( +DB_MUSIC_PATH = try_decode(xbmc.translatePath( "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])) _DB_TEXTURE_VERSION = { @@ -108,12 +108,12 @@ _DB_TEXTURE_VERSION = { 17: 13, # Krypton 18: 13 # Leia } -DB_TEXTURE_PATH = tryDecode(xbmc.translatePath( +DB_TEXTURE_PATH = try_decode(xbmc.translatePath( "special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION])) -DB_PLEX_PATH = tryDecode(xbmc.translatePath("special://database/plex.db")) +DB_PLEX_PATH = try_decode(xbmc.translatePath("special://database/plex.db")) -EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath( +EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath( "special://profile/addon_data/%s/temp/" % ADDON_ID)) diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 659894e5..5636fb3c 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -8,8 +8,8 @@ from os import makedirs import xbmc from xbmcvfs import exists -from utils import window, settings, language as lang, tryEncode, indent, \ - normalize_nodes, exists_dir, tryDecode +from utils import window, settings, language as lang, try_encode, indent, \ + normalize_nodes, exists_dir, try_decode import variables as v ############################################################################### @@ -62,9 +62,9 @@ class VideoNodes(object): dirname = viewid # Returns strings - path = tryDecode(xbmc.translatePath( + path = try_decode(xbmc.translatePath( "special://profile/library/video/")) - nodepath = tryDecode(xbmc.translatePath( + nodepath = try_decode(xbmc.translatePath( "special://profile/library/video/Plex-%s/" % dirname)) if delete: @@ -77,9 +77,9 @@ class VideoNodes(object): # Verify the video directory if not exists_dir(path): copytree( - src=tryDecode(xbmc.translatePath( + src=try_decode(xbmc.translatePath( "special://xbmc/system/library/video")), - dst=tryDecode(xbmc.translatePath( + dst=try_decode(xbmc.translatePath( "special://profile/library/video"))) # Create the node directory @@ -292,7 +292,7 @@ class VideoNodes(object): # To do: add our photos nodes to kodi picture sources somehow continue - if exists(tryEncode(nodeXML)): + if exists(try_encode(nodeXML)): # Don't recreate xml if already exists continue @@ -378,9 +378,9 @@ class VideoNodes(object): etree.ElementTree(root).write(nodeXML, encoding="UTF-8") def singleNode(self, indexnumber, tagname, mediatype, itemtype): - tagname = tryEncode(tagname) - cleantagname = tryDecode(normalize_nodes(tagname)) - nodepath = tryDecode(xbmc.translatePath( + tagname = try_encode(tagname) + cleantagname = try_decode(normalize_nodes(tagname)) + nodepath = try_decode(xbmc.translatePath( "special://profile/library/video/")) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) path = "library://video/plex_%s.xml" % cleantagname @@ -394,9 +394,9 @@ class VideoNodes(object): if not exists_dir(nodepath): # We need to copy over the default items copytree( - src=tryDecode(xbmc.translatePath( + src=try_decode(xbmc.translatePath( "special://xbmc/system/library/video")), - dst=tryDecode(xbmc.translatePath( + dst=try_decode(xbmc.translatePath( "special://profile/library/video"))) labels = { @@ -411,7 +411,7 @@ class VideoNodes(object): window('%s.content' % embynode, value=path) window('%s.type' % embynode, value=itemtype) - if exists(tryEncode(nodeXML)): + if exists(try_encode(nodeXML)): # Don't recreate xml if already exists return diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py index 8cd3134a..ce7a00f1 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/websocket.py @@ -292,7 +292,7 @@ class ABNF(object): opcode: operation code. please see OPCODE_XXX. """ if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): - data = utils.tryEncode(data) + data = utils.try_encode(data) # mask must be set if send data from client return ABNF(1, 0, 0, 0, opcode, 1, data) From ca115285932a7e367c5467c6190ea90cb4246d17 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 13:24:00 +0100 Subject: [PATCH 313/509] Prettify --- resources/lib/entrypoint.py | 4 +- resources/lib/itemtypes.py | 18 ++--- resources/lib/librarysync.py | 10 +-- resources/lib/plexbmchelper/subscribers.py | 4 +- resources/lib/utils.py | 90 ++++++++++++++-------- resources/lib/websocket_client.py | 14 ++-- 6 files changed, 82 insertions(+), 58 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 619801f6..b8f5bb26 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -12,7 +12,7 @@ from xbmc import sleep, executebuiltin, translatePath from xbmcgui import ListItem from utils import window, settings, language as lang, dialog, try_encode, \ - CatchExceptions, exists_dir, plex_command, try_decode + catch_exceptions, exists_dir, plex_command, try_decode import downloadutils from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ @@ -473,7 +473,7 @@ def getVideoFiles(plexId, params): xbmcplugin.endOfDirectory(HANDLE) -@CatchExceptions(warnuser=False) +@catch_exceptions(warnuser=False) def getExtraFanArt(plexid, plexPath): """ Get extrafanart for listitem diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 25414cb4..a8e63238 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -6,7 +6,7 @@ from ntpath import dirname from datetime import datetime from artwork import Artwork -from utils import window, kodi_sql, CatchExceptions +from utils import window, kodi_sql, catch_exceptions import plexdb_functions as plexdb import kodidb_functions as kodidb @@ -61,7 +61,7 @@ class Items(object): self.kodiconn.close() return self - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def getfanart(self, plex_id, refresh=False): """ Tries to get additional fanart for movies (+sets) and TV shows. @@ -177,7 +177,7 @@ class Movies(Items): """ Used for plex library-type movies """ - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): """ Process single movie @@ -509,7 +509,7 @@ class TVShows(Items): """ For Plex library-type TV shows """ - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): """ Process a single show @@ -742,7 +742,7 @@ class TVShows(Items): tags.extend(collections) self.kodi_db.addTags(showid, tags, "tvshow") - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_updateSeason(self, item, viewtag=None, viewid=None): """ Process a single season of a certain tv show @@ -790,7 +790,7 @@ class TVShows(Items): view_id=viewid, checksum=checksum) - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_updateEpisode(self, item, viewtag=None, viewid=None): """ Process single episode @@ -1282,7 +1282,7 @@ class Music(Items): self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) return self - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_updateArtist(self, item, viewtag=None, viewid=None): """ Adds a single artist @@ -1368,7 +1368,7 @@ class Music(Items): # Update artwork artwork.addArtwork(artworks, artistid, v.KODI_TYPE_ARTIST, kodicursor) - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None, scan_children=True): """ @@ -1565,7 +1565,7 @@ class Music(Items): for child in children: self.add_updateSong(child, viewtag, viewid) - @CatchExceptions(warnuser=True) + @catch_exceptions(warnuser=True) def add_updateSong(self, item, viewtag=None, viewid=None): """ Process single song diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 150fa594..4f6886f1 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -9,7 +9,7 @@ import xbmc from xbmcvfs import exists from utils import window, settings, unix_timestamp, thread_methods, \ - create_actor_db_index, dialog, LogTime, playlist_xsp, language as lang, \ + create_actor_db_index, dialog, log_time, playlist_xsp, language as lang, \ unix_date_to_kodi, reset, try_decode, delete_playlists, delete_nodes, \ try_encode, compare_version import downloadutils @@ -218,7 +218,7 @@ class LibrarySync(Thread): # Create an index for actors to speed up sync create_actor_db_index() - @LogTime + @log_time def fullSync(self, repair=False): """ repair=True: force sync EVERY item @@ -727,7 +727,7 @@ class LibrarySync(Thread): }) self.updatelist = [] - @LogTime + @log_time def PlexMovies(self): # Initialize self.allPlexElementsId = {} @@ -819,7 +819,7 @@ class LibrarySync(Thread): with itemMth() as method: method.updateUserdata(xml) - @LogTime + @log_time def PlexTVShows(self): # Initialize self.allPlexElementsId = {} @@ -949,7 +949,7 @@ class LibrarySync(Thread): log.info("%s sync is finished." % itemType) return True - @LogTime + @log_time def PlexMusic(self): itemType = 'Music' diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 95041870..aa3bb8ab 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -6,7 +6,7 @@ from logging import getLogger from threading import Thread, RLock from downloadutils import DownloadUtils as DU -from utils import window, kodi_time_to_millis, Lock_Function +from utils import window, kodi_time_to_millis, LockFunction import state import variables as v import json_rpc as js @@ -17,7 +17,7 @@ import playqueue as PQ LOG = getLogger("PLEX." + __name__) # Need to lock all methods and functions messing with subscribers or state LOCK = RLock() -LOCKER = Lock_Function(LOCK) +LOCKER = LockFunction(LOCK) ############################################################################### diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 3c40057c..0a6e62c5 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -242,20 +242,20 @@ def kodi_time_to_millis(time): return ret -def try_encode(uniString, encoding='utf-8'): +def try_encode(input_str, encoding='utf-8'): """ - Will try to encode uniString (in unicode) to encoding. This possibly + Will try to encode input_str (in unicode) to encoding. This possibly fails with e.g. Android TV's Python, which does not accept arguments for string.encode() """ - if isinstance(uniString, str): + if isinstance(input_str, str): # already encoded - return uniString + return input_str try: - uniString = uniString.encode(encoding, "ignore") + input_str = input_str.encode(encoding, "ignore") except TypeError: - uniString = uniString.encode() - return uniString + input_str = input_str.encode() + return input_str def try_decode(string, encoding='utf-8'): @@ -878,10 +878,7 @@ def passwords_xml(): settings('networkCreds', value="%s" % server) LOG.info("Added server: %s to passwords.xml", server) # Prettify and write to file - try: - indent(root) - except: - pass + indent(root) etree.ElementTree(root).write(xmlpath, encoding="UTF-8") @@ -957,7 +954,7 @@ def delete_nodes(): ############################################################################### # WRAPPERS -def CatchExceptions(warnuser=False): +def catch_exceptions(warnuser=False): """ Decorator for methods to catch exceptions and log them. Useful for e.g. librarysync threads using itemtypes.py, because otherwise we would not @@ -967,12 +964,18 @@ def CatchExceptions(warnuser=False): which will trigger a Kodi infobox to inform user """ def decorate(func): + """ + Decorator construct + """ @wraps(func) def wrapper(*args, **kwargs): + """ + Wrapper construct + """ try: return func(*args, **kwargs) - except Exception as e: - LOG.error('%s has crashed. Error: %s', func.__name__, e) + except Exception as err: + LOG.error('%s has crashed. Error: %s', func.__name__, err) import traceback LOG.error("Traceback:\n%s", traceback.format_exc()) if warnuser: @@ -982,7 +985,7 @@ def CatchExceptions(warnuser=False): return decorate -def LogTime(func): +def log_time(func): """ Decorator for functions and methods to log the time it took to run the code """ @@ -1010,10 +1013,10 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): ALSO returns True if PKC should exit Also adds the following class attributes: - __thread_stopped - __thread_suspended - __stops - __suspends + thread_stopped + thread_suspended + stops + suspends invoke with either @Newthread_methods @@ -1030,41 +1033,56 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): add_suspends=add_suspends) # Because we need a reference, not a copy of the immutable objects in # state, we need to look up state every time explicitly - cls.__stops = ['STOP_PKC'] + cls.stops = ['STOP_PKC'] if add_stops is not None: - cls.__stops.extend(add_stops) - cls.__suspends = add_suspends or [] + cls.stops.extend(add_stops) + cls.suspends = add_suspends or [] # Attach new attributes to class - cls.__thread_stopped = False - cls.__thread_suspended = False + cls.thread_stopped = False + cls.thread_suspended = False # Define new class methods and attach them to class def stop_thread(self): - self.__thread_stopped = True + """ + Call to stop this thread + """ + self.thread_stopped = True cls.stop_thread = stop_thread def suspend_thread(self): - self.__thread_suspended = True + """ + Call to suspend this thread + """ + self.thread_suspended = True cls.suspend_thread = suspend_thread def resume_thread(self): - self.__thread_suspended = False + """ + Call to revive a suspended thread back to life + """ + self.thread_suspended = False cls.resume_thread = resume_thread def thread_suspended(self): - if self.__thread_suspended is True: + """ + Returns True if the thread is suspended + """ + if self.thread_suspended is True: return True - for suspend in self.__suspends: + for suspend in self.suspends: if getattr(state, suspend): return True return False cls.thread_suspended = thread_suspended def thread_stopped(self): - if self.__thread_stopped is True: + """ + Returns True if the thread is stopped + """ + if self.thread_stopped is True: return True - for stop in self.__stops: + for stop in self.stops: if getattr(state, stop): return True return False @@ -1074,12 +1092,12 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): return cls -class Lock_Function(object): +class LockFunction(object): """ 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 + lockfunction = LockFunction(lock), where lock is a threading.Lock() object To then lock a function or method: @@ -1090,8 +1108,14 @@ class Lock_Function(object): self.lock = lock def lockthis(self, func): + """ + Use this method to actually lock a function or method + """ @wraps(func) def wrapper(*args, **kwargs): + """ + Wrapper construct + """ with self.lock: result = func(*args, **kwargs) return result diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 16e14a0d..a494fb7a 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -213,8 +213,8 @@ class Alexa_Websocket(WebSocket): Can't use thread_methods! """ - __thread_stopped = False - __thread_suspended = False + thread_stopped = False + thread_suspended = False def getUri(self): uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' @@ -256,16 +256,16 @@ class Alexa_Websocket(WebSocket): # Path in thread_methods def stop_thread(self): - self.__thread_stopped = True + self.thread_stopped = True def suspend_thread(self): - self.__thread_suspended = True + self.thread_suspended = True def resume_thread(self): - self.__thread_suspended = False + self.thread_suspended = False def thread_stopped(self): - if self.__thread_stopped is True: + if self.thread_stopped is True: return True if state.STOP_PKC: return True @@ -276,7 +276,7 @@ class Alexa_Websocket(WebSocket): """ Overwrite method since we need to check for plex token """ - if self.__thread_suspended is True: + if self.thread_suspended is True: return True if not state.PLEX_TOKEN: return True From 5068327408034e4a41f609ab2d7258b7cc97d6a1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 14:42:49 +0100 Subject: [PATCH 314/509] Prettify --- resources/lib/PlexAPI.py | 661 ++++++++++++++---------------- resources/lib/PlexCompanion.py | 10 +- resources/lib/context_entry.py | 2 +- resources/lib/entrypoint.py | 50 +-- resources/lib/itemtypes.py | 208 +++++----- resources/lib/kodidb_functions.py | 2 +- resources/lib/librarysync.py | 10 +- resources/lib/music.py | 6 +- resources/lib/playback.py | 30 +- resources/lib/playlist_func.py | 4 +- resources/lib/playqueue.py | 2 +- resources/lib/playutils.py | 32 +- resources/lib/plexdb_functions.py | 2 +- 13 files changed, 489 insertions(+), 530 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 513e9e75..0f72d4da 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -52,10 +52,31 @@ LOG = getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') + +# Key of library: Plex-identifier. Value represents the Kodi/emby side +PEOPLE_OF_INTEREST = { + 'Director': 'Director', + 'Writer': 'Writer', + 'Role': 'Actor', + 'Producer': 'Producer' +} +# we need to use a little mapping between fanart.tv arttypes and kodi +# artttypes +FANART_TV_TYPES = [ + ("logo", "Logo"), + ("musiclogo", "clearlogo"), + ("disc", "Disc"), + ("clearart", "Art"), + ("banner", "Banner"), + ("clearlogo", "Logo"), + ("background", "fanart"), + ("showbackground", "fanart"), + ("characterart", "characterart") +] ############################################################################### -class API(): +class API(object): """ API(item) @@ -70,47 +91,40 @@ class API(): self.mediastream = None self.server = window('pms_server') - def setPartNumber(self, number=None): + def set_part_number(self, number=None): """ Sets the part number to work with (used to deal with Movie with several parts). """ self.part = number or 0 - def getPartNumber(self): - """ - Returns the current media part number we're dealing with. - """ - return self.part - - def getType(self): + def plex_type(self): """ Returns the type of media, e.g. 'movie' or 'clip' for trailers """ return self.item.attrib.get('type') - def getChecksum(self): + def checksum(self): """ - Returns a string, not int. - + Returns a string, not int. WATCH OUT - time in Plex, not Kodi ;-) """ # Include a letter to prohibit saving as an int! - checksum = "K%s%s" % (self.getRatingKey(), + checksum = "K%s%s" % (self.plex_id(), self.item.attrib.get('updatedAt', '')) return checksum - def getRatingKey(self): + def plex_id(self): """ - Returns the Plex key such as '246922' as a string + Returns the Plex ratingKey such as '246922' as a string or None """ return self.item.attrib.get('ratingKey') - def getKey(self): + def path_and_plex_id(self): """ - Returns the Plex key such as '/library/metadata/246922' or empty string + Returns the Plex key such as '/library/metadata/246922' or None """ - return self.item.attrib.get('key', '') + return self.item.attrib.get('key') def plex_media_streams(self): """ @@ -119,23 +133,23 @@ class API(): """ return self.item[self.mediastream][self.part] - def getFilePath(self, forceFirstMediaStream=False): + def file_path(self, force_first_media=False): """ Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv' or None - forceFirstMediaStream=True: + force_first_media=True: will always use 1st media stream, e.g. when several different files are present for the same PMS item """ - if self.mediastream is None and forceFirstMediaStream is False: - self.getMediastreamNumber() + if self.mediastream is None and force_first_media is False: + self.mediastream_number() try: - if forceFirstMediaStream is False: + if force_first_media is False: ans = self.item[self.mediastream][self.part].attrib['file'] else: ans = self.item[0][self.part].attrib['file'] - except: + except (TypeError, AttributeError, IndexError, KeyError): ans = None if ans is not None: try: @@ -162,14 +176,14 @@ class API(): 1920, 1080) else: - path = self.addPlexCredentialsToUrl( + path = self.attach_plex_token_to_url( '%s%s' % (window('pms_server'), self.item[0][0].attrib['key'])) # Attach Plex id to url to let it be picked up by our playqueue agent # later - return try_encode('%s&plex_id=%s' % (path, self.getRatingKey())) + return try_encode('%s&plex_id=%s' % (path, self.plex_id())) - def getTVShowPath(self): + def tv_show_path(self): """ Returns the direct path to the TV show, e.g. '\\NAS\tv\series' or None @@ -180,13 +194,13 @@ class API(): res = child.attrib.get('path') return res - def getIndex(self): + def season_number(self): """ Returns the 'index' of an PMS XML reply. Depicts e.g. season number. """ return self.item.attrib.get('index') - def getDateCreated(self): + def date_created(self): """ Returns the date when this library item was created. @@ -199,7 +213,7 @@ class API(): res = '2000-01-01 10:00:00' return res - def getViewCount(self): + def viewcount(self): """ Returns the play count for the item as an int or the int 0 if not found """ @@ -208,7 +222,7 @@ class API(): except (KeyError, ValueError): return 0 - def getUserData(self): + def userdata(self): """ Returns a dict with None if a value is missing { @@ -231,13 +245,13 @@ class API(): played = True if playcount else False try: - lastPlayedDate = unix_date_to_kodi(int(item['lastViewedAt'])) + last_played = unix_date_to_kodi(int(item['lastViewedAt'])) except (KeyError, ValueError): - lastPlayedDate = None + last_played = None if state.INDICATE_MEDIA_VERSIONS is True: userrating = 0 - for entry in self.item.findall('./Media'): + for _ in self.item.findall('./Media'): userrating += 1 # Don't show a value of '1' userrating = 0 if userrating == 1 else userrating @@ -255,19 +269,19 @@ class API(): except (KeyError, ValueError): rating = 0.0 - resume, runtime = self.getRuntime() + resume, runtime = self.resume_runtime() return { 'Favorite': favorite, 'PlayCount': playcount, 'Played': played, - 'LastPlayedDate': lastPlayedDate, + 'LastPlayedDate': last_played, 'Resume': resume, 'Runtime': runtime, 'Rating': rating, 'UserRating': userrating } - def getCollections(self): + def collection_list(self): """ Returns a list of PMS collection tags or an empty list """ @@ -278,7 +292,7 @@ class API(): collections.append(child.attrib['tag']) return collections - def getPeople(self): + def people(self): """ Returns a dict of lists of people found. { @@ -312,7 +326,7 @@ class API(): 'Producer': producer } - def getPeopleList(self): + def people_list(self): """ Returns a list of people from item, with a list item of the form { @@ -324,36 +338,26 @@ class API(): } """ people = [] - # Key of library: Plex-identifier. Value represents the Kodi/emby side - people_of_interest = { - 'Director': 'Director', - 'Writer': 'Writer', - 'Role': 'Actor', - 'Producer': 'Producer' - } for child in self.item: - if child.tag in people_of_interest.keys(): + if child.tag in PEOPLE_OF_INTEREST.keys(): name = child.attrib['tag'] name_id = child.attrib['id'] - Type = child.tag - Type = people_of_interest[Type] - + typus = PEOPLE_OF_INTEREST[child.tag] url = child.attrib.get('thumb') - Role = child.attrib.get('role') - + role = child.attrib.get('role') people.append({ 'Name': name, - 'Type': Type, + 'Type': typus, 'Id': name_id, 'imageurl': url }) if url: people[-1].update({'imageurl': url}) - if Role: - people[-1].update({'Role': Role}) + if role: + people[-1].update({'Role': role}) return people - def getGenres(self): + def genre_list(self): """ Returns a list of genres found. (Not a string) """ @@ -363,10 +367,7 @@ class API(): genre.append(child.attrib['tag']) return genre - def getGuid(self): - return self.item.attrib.get('guid') - - def getProvider(self, providername=None): + def provider(self, providername=None): """ providername: e.g. 'imdb', 'tvdb' @@ -393,10 +394,10 @@ class API(): provider = None return provider - def getTitle(self): + def titles(self): """ Returns an item's name/title or "Missing Title Name". - Output: + Output is the tuple title, sorttitle sorttitle = title, if no sorttitle is found @@ -405,19 +406,19 @@ class API(): sorttitle = self.item.attrib.get('titleSort', title) return title, sorttitle - def getPlot(self): + def plot(self): """ Returns the plot or None. """ return self.item.attrib.get('summary', None) - def getTagline(self): + def tagline(self): """ Returns a shorter tagline or None """ return self.item.attrib.get('tagline', None) - def getAudienceRating(self): + def audience_rating(self): """ Returns the audience rating, 'rating' itself or 0.0 """ @@ -430,13 +431,13 @@ class API(): res = 0.0 return res - def getYear(self): + def year(self): """ Returns the production(?) year ("year") or None """ return self.item.attrib.get('year', None) - def getResume(self): + def resume_point(self): """ Returns the resume point of time in seconds as int. 0 if not found """ @@ -446,13 +447,13 @@ class API(): resume = 0.0 return int(resume * v.PLEX_TO_KODI_TIMEFACTOR) - def getRuntime(self): + def resume_runtime(self): """ Resume point of time and runtime/totaltime in rounded to seconds. Time from Plex server is measured in milliseconds. Kodi: seconds - Output: + Output is the tuple: resume, runtime as ints. 0 if not found """ item = self.item.attrib @@ -470,7 +471,7 @@ class API(): resume = int(resume * v.PLEX_TO_KODI_TIMEFACTOR) return resume, runtime - def getMpaa(self): + def content_rating(self): """ Get the content rating or None """ @@ -481,7 +482,7 @@ class API(): mpaa = "Rated Not Rated" return mpaa - def getCountry(self): + def country_list(self): """ Returns a list of all countries found in item. """ @@ -491,27 +492,31 @@ class API(): country.append(child.attrib['tag']) return country - def getPremiereDate(self): + def premiere_date(self): """ Returns the "originallyAvailableAt" or None """ return self.item.attrib.get('originallyAvailableAt') - def getMusicStudio(self): - return self.item.attrib.get('studio', '') + def music_studio(self): + """ + Returns the 'studio' or None + """ + return self.item.attrib.get('studio') - def getStudios(self): + def music_studio_list(self): """ Returns a list with a single entry for the studio, or an empty list """ studio = [] try: - studio.append(self.getStudio(self.item.attrib['studio'])) + studio.append(self.replace_studio(self.item.attrib['studio'])) except KeyError: pass return studio - def getStudio(self, studioName): + @staticmethod + def replace_studio(studio_name): """ Convert studio for Kodi to properly detect them """ @@ -522,21 +527,24 @@ class API(): 'showcase (ca)': "Showcase", 'wgn america': "WGN" } - return studios.get(studioName.lower(), studioName) + return studios.get(studio_name.lower(), studio_name) - def joinList(self, listobject): + @staticmethod + def list_to_string(listobject): """ - Smart-joins the listobject into a single string using a " / " - separator. + Smart-joins the listobject into a single string using a " / " separator. If the list is empty, smart_join returns an empty string. """ string = " / ".join(listobject) return string - def getParentRatingKey(self): - return self.item.attrib.get('parentRatingKey', '') + def parent_plex_id(self): + """ + Returns the 'parentRatingKey' as a string or None + """ + return self.item.attrib.get('parentRatingKey') - def getEpisodeDetails(self): + def episode_data(self): """ Call on a single episode. @@ -548,29 +556,13 @@ class API(): Episode number, Plex: 'index' ] """ - item = self.item.attrib - key = item.get('grandparentRatingKey') - title = item.get('grandparentTitle') - season = item.get('parentIndex') - episode = item.get('index') - return key, title, season, episode + return (self.item.get('grandparentRatingKey'), + self.item.get('grandparentTitle'), + self.item.get('parentIndex'), + self.item.get('index')) - def addPlexHeadersToUrl(self, url, arguments={}): - """ - Takes an URL and optional arguments (also to be URL-encoded); returns - an extended URL with e.g. the Plex token included. - - arguments overrule everything - """ - xargs = client.getXArgsDeviceInfo() - xargs.update(arguments) - if '?' not in url: - url = "%s?%s" % (url, urlencode(xargs)) - else: - url = "%s&%s" % (url, urlencode(xargs)) - return url - - def addPlexCredentialsToUrl(self, url): + @staticmethod + def attach_plex_token_to_url(url): """ Returns an extended URL with the Plex token included as 'X-Plex-Token=' @@ -584,7 +576,7 @@ class API(): url = "%s&X-Plex-Token=%s" % (url, window('pms_token')) return url - def getItemId(self): + def item_id(self): """ Returns current playQueueItemID or if unsuccessful the playListItemID If not found, None is returned @@ -598,7 +590,7 @@ class API(): answ = None return answ - def getDataFromPartOrMedia(self, key): + def _data_from_part_or_media(self, key): """ Retrieves XML data 'key' first from the active part. If unsuccessful, tries to retrieve the data from the Media response part. @@ -617,7 +609,7 @@ class API(): value = None return value - def getVideoCodec(self): + def video_codec(self): """ Returns the video codec and resolution for the child and part selected. If any data is not found on a part-level, the Media-level data is @@ -637,21 +629,21 @@ class API(): } """ answ = { - 'videocodec': self.getDataFromPartOrMedia('videoCodec'), - 'resolution': self.getDataFromPartOrMedia('videoResolution'), - 'height': self.getDataFromPartOrMedia('height'), - 'width': self.getDataFromPartOrMedia('width'), - 'aspectratio': self.getDataFromPartOrMedia('aspectratio'), - 'bitrate': self.getDataFromPartOrMedia('bitrate'), - 'container': self.getDataFromPartOrMedia('container'), + 'videocodec': self._data_from_part_or_media('videoCodec'), + 'resolution': self._data_from_part_or_media('videoResolution'), + 'height': self._data_from_part_or_media('height'), + 'width': self._data_from_part_or_media('width'), + 'aspectratio': self._data_from_part_or_media('aspectratio'), + 'bitrate': self._data_from_part_or_media('bitrate'), + 'container': self._data_from_part_or_media('container'), } try: - answ['bitDepth'] = self.item[0][self.part][self.mediastream].attrib.get('bitDepth') - except: + answ['bitDepth'] = self.item[0][self.part][self.mediastream].get('bitDepth') + except (TypeError, AttributeError, KeyError, IndexError): answ['bitDepth'] = None return answ - def getExtras(self): + def extras_list(self): """ Currently ONLY returns the very first trailer found! @@ -676,10 +668,10 @@ class API(): return elements for extra in extras: try: - extraType = int(extra.attrib['extraType']) + typus = int(extra.attrib['extraType']) except (KeyError, TypeError): - extraType = None - if extraType != 1: + typus = None + if typus != 1: continue duration = float(extra.attrib.get('duration', 0.0)) elements.append({ @@ -687,14 +679,14 @@ class API(): 'title': extra.attrib.get('title'), 'thumb': extra.attrib.get('thumb'), 'duration': int(duration * v.PLEX_TO_KODI_TIMEFACTOR), - 'extraType': extraType, + 'extraType': typus, 'originallyAvailableAt': extra.attrib.get('originallyAvailableAt'), 'year': extra.attrib.get('year') }) break return elements - def getMediaStreams(self): + def mediastreams(self): """ Returns the media streams for metadata purposes @@ -744,7 +736,7 @@ class API(): track['width'] = stream.get('width') # track['Video3DFormat'] = item.get('Video3DFormat') track['aspect'] = stream.get('aspectRatio', aspect) - track['duration'] = self.getRuntime()[1] + track['duration'] = self.resume_runtime()[1] track['video3DFormat'] = None videotracks.append(track) elif media_type == 2: # Audio streams @@ -768,21 +760,22 @@ class API(): 'subtitle': subtitlelanguages } - def __getOneArtwork(self, entry): + def _one_artwork(self, entry): if entry not in self.item.attrib: return '' artwork = self.item.attrib[entry] if artwork.startswith('http'): pass else: - artwork = self.addPlexCredentialsToUrl( - "%s/photo/:/transcode?width=4000&height=4000&minSize=1&upscale=0&url=%s" % (self.server, artwork)) + artwork = self.attach_plex_token_to_url( + '%s/photo/:/transcode?width=4000&height=4000&' + 'minSize=1&upscale=0&url=%s' % (self.server, artwork)) return artwork - def getAllArtwork(self, parentInfo=False): + def artwork(self, parent_info=False): """ Gets the URLs to the Plex artwork, or empty string if not found. - parentInfo=True will check for parent's artwork if None is found + parent_info=True will check for parent's artwork if None is found Output: { @@ -806,25 +799,25 @@ class API(): } # Process backdrops # Get background artwork URL - allartworks['Backdrop'].append(self.__getOneArtwork('art')) + allartworks['Backdrop'].append(self._one_artwork('art')) # Get primary "thumb" pictures: - allartworks['Primary'] = self.__getOneArtwork('thumb') + allartworks['Primary'] = self._one_artwork('thumb') # Banner (usually only on tv series level) - allartworks['Banner'] = self.__getOneArtwork('banner') + allartworks['Banner'] = self._one_artwork('banner') # For e.g. TV shows, get series thumb - allartworks['Thumb'] = self.__getOneArtwork('grandparentThumb') + allartworks['Thumb'] = self._one_artwork('grandparentThumb') # Process parent items if the main item is missing artwork - if parentInfo: + if parent_info: # Process parent backdrops if not allartworks['Backdrop']: allartworks['Backdrop'].append( - self.__getOneArtwork('parentArt')) + self._one_artwork('parentArt')) if not allartworks['Primary']: - allartworks['Primary'] = self.__getOneArtwork('parentThumb') + allartworks['Primary'] = self._one_artwork('parentThumb') return allartworks - def getFanartArtwork(self, allartworks, parentInfo=False): + def fanart_artwork(self, allartworks): """ Downloads additional fanart from third party sources (well, link to fanart only). @@ -839,12 +832,12 @@ class API(): 'Backdrop': [] } """ - externalId = self.getExternalItemId() - if externalId is not None: - allartworks = self.getFanartTVArt(externalId, allartworks) + external_id = self.retrieve_external_item_id() + if external_id is not None: + allartworks = self.lookup_fanart_tv(external_id, allartworks) return allartworks - def getExternalItemId(self, collection=False): + def retrieve_external_item_id(self, collection=False): """ Returns the item's IMDB id for movies or tvdb id for TV shows @@ -857,23 +850,23 @@ class API(): """ item = self.item.attrib media_type = item.get('type') - mediaId = None + media_id = None # 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 == v.PLEX_TYPE_MOVIE: - mediaId = self.getProvider('imdb') + media_id = self.provider('imdb') elif media_type == v.PLEX_TYPE_SHOW: - mediaId = self.getProvider('tvdb') - if mediaId is not None: - return mediaId + media_id = self.provider('tvdb') + if media_id is not None: + return media_id LOG.info('Plex did not provide ID for IMDB or TVDB. Start ' 'lookup process') else: LOG.info('Start movie set/collection lookup on themoviedb using %s', item.get('title', '')) - apiKey = settings('themoviedbAPIKey') + api_key = settings('themoviedbAPIKey') if media_type == v.PLEX_TYPE_SHOW: media_type = 'tv' title = item.get('title', '') @@ -882,7 +875,7 @@ class API(): title = sub(r'\s*\(\d{4}\)$', '', title, count=1) url = 'https://api.themoviedb.org/3/search/%s' % media_type parameters = { - 'api_key': apiKey, + 'api_key': api_key, 'language': v.KODILANGUAGE, 'query': try_encode(title) } @@ -892,7 +885,7 @@ class API(): timeout=7) try: data.get('test') - except: + except AttributeError: LOG.error('Could not download data from FanartTV') return if data.get('results') is None: @@ -901,18 +894,18 @@ class API(): return year = item.get('year') - matchFound = None + match_found = None # find year match if year is not None: for entry in data["results"]: if year in entry.get("first_air_date", ""): - matchFound = entry + match_found = entry break elif year in entry.get("release_date", ""): - matchFound = entry + match_found = entry break # find exact match based on title, if we haven't found a year match - if matchFound is None: + if match_found is None: LOG.info('No themoviedb match found using year %s', year) replacements = ( ' ', @@ -928,53 +921,53 @@ class API(): title_alt = title.lower() name_alt = name.lower() org_name_alt = original_name.lower() - for replaceString in replacements: - title_alt = title_alt.replace(replaceString, '') - name_alt = name_alt.replace(replaceString, '') - org_name_alt = org_name_alt.replace(replaceString, '') + for replace_string in replacements: + title_alt = title_alt.replace(replace_string, '') + name_alt = name_alt.replace(replace_string, '') + org_name_alt = org_name_alt.replace(replace_string, '') if name == title or original_name == title: # match found for exact title name - matchFound = entry + match_found = entry break elif (name.split(" (")[0] == title or title_alt == name_alt or title_alt == org_name_alt): # match found with substituting some stuff - matchFound = entry + match_found = entry break # if a match was not found, we accept the closest match from TMDB - if matchFound is None and len(data.get("results")) > 0: + if match_found is None and len(data.get("results")): LOG.info('Using very first match from themoviedb') - matchFound = entry = data.get("results")[0] + match_found = entry = data.get("results")[0] - if matchFound is None: + if match_found is None: LOG.info('Still no themoviedb match for type: %s, title: %s, ' 'year: %s', media_type, title, year) LOG.debug('themoviedb answer was %s', data['results']) return LOG.info('Found themoviedb match for %s: %s', - item.get('title'), matchFound) + item.get('title'), match_found) - tmdbId = str(entry.get("id", "")) - if tmdbId == '': + tmdb_id = str(entry.get("id", "")) + if tmdb_id == '': LOG.error('No themoviedb ID found, aborting') return if media_type == "multi" and entry.get("media_type"): media_type = entry.get("media_type") name = entry.get("name", entry.get("title")) - # lookup external tmdbId and perform artwork lookup on fanart.tv + # lookup external tmdb_id and perform artwork lookup on fanart.tv parameters = { - 'api_key': apiKey + 'api_key': api_key } for language in [v.KODILANGUAGE, "en"]: parameters['language'] = language if media_type == "movie": - url = 'https://api.themoviedb.org/3/movie/%s' % tmdbId + url = 'https://api.themoviedb.org/3/movie/%s' % tmdb_id parameters['append_to_response'] = 'videos' elif media_type == "tv": - url = 'https://api.themoviedb.org/3/tv/%s' % tmdbId + url = 'https://api.themoviedb.org/3/tv/%s' % tmdb_id parameters['append_to_response'] = 'external_ids,videos' data = DU().downloadUrl(url, authenticate=False, @@ -982,24 +975,24 @@ class API(): timeout=7) try: data.get('test') - except: + except AttributeError: LOG.error('Could not download %s with parameters %s', url, parameters) continue if collection is False: if data.get("imdb_id") is not None: - mediaId = str(data.get("imdb_id")) + media_id = str(data.get("imdb_id")) break if data.get("external_ids") is not None: - mediaId = str(data["external_ids"].get("tvdb_id")) + media_id = str(data["external_ids"].get("tvdb_id")) break else: if data.get("belongs_to_collection") is None: continue - mediaId = str(data.get("belongs_to_collection").get("id")) + media_id = str(data.get("belongs_to_collection").get("id")) LOG.debug('Retrieved collections tmdb id %s for %s', - mediaId, title) - url = 'https://api.themoviedb.org/3/collection/%s' % mediaId + media_id, title) + url = 'https://api.themoviedb.org/3/collection/%s' % media_id data = DU().downloadUrl(url, authenticate=False, parameters=parameters, @@ -1011,17 +1004,19 @@ class API(): 'the language %s', title, language) continue else: - poster = 'https://image.tmdb.org/t/p/original%s' % data.get('poster_path') - background = 'https://image.tmdb.org/t/p/original%s' % data.get('backdrop_path') - mediaId = mediaId, poster, background + poster = ('https://image.tmdb.org/t/p/original%s' % + data.get('poster_path')) + background = ('https://image.tmdb.org/t/p/original%s' % + data.get('backdrop_path')) + media_id = media_id, poster, background break - return mediaId + return media_id - def getFanartTVArt(self, mediaId, allartworks, setInfo=False): + def lookup_fanart_tv(self, media_id, allartworks, set_info=False): """ perform artwork lookup on fanart.tv - mediaId: IMDB id for movies, tvdb id for TV shows + media_id: IMDB id for movies, tvdb id for TV shows """ item = self.item.attrib api_key = settings('FanArtTVAPIKey') @@ -1031,10 +1026,10 @@ class API(): if typus == "movie": url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \ - % (mediaId, api_key) + % (media_id, api_key) elif typus == 'tv': url = 'http://webservice.fanart.tv/v3/tv/%s?api_key=%s' \ - % (mediaId, api_key) + % (media_id, api_key) else: # Not supported artwork return allartworks @@ -1043,30 +1038,18 @@ class API(): timeout=15) try: data.get('test') - except: + except AttributeError: LOG.error('Could not download data from FanartTV') return allartworks - # we need to use a little mapping between fanart.tv arttypes and kodi - # artttypes - fanartTVTypes = [ - ("logo", "Logo"), - ("musiclogo", "clearlogo"), - ("disc", "Disc"), - ("clearart", "Art"), - ("banner", "Banner"), - ("clearlogo", "Logo"), - ("background", "fanart"), - ("showbackground", "fanart"), - ("characterart", "characterart") - ] - if typus == "artist": - fanartTVTypes.append(("thumb", "folder")) - else: - fanartTVTypes.append(("thumb", "Thumb")) + fanart_tv_types = list(FANART_TV_TYPES) - if setInfo: - fanartTVTypes.append(("poster", "Primary")) + if typus == "artist": + fanart_tv_types.append(("thumb", "folder")) + else: + fanart_tv_types.append(("thumb", "Thumb")) + if set_info: + fanart_tv_types.append(("poster", "Primary")) prefixes = ( "hd" + typus, @@ -1074,7 +1057,7 @@ class API(): typus, "", ) - for fanarttype in fanartTVTypes: + for fanarttype in fanart_tv_types: # Skip the ones we already have if allartworks.get(fanarttype[1]): continue @@ -1112,7 +1095,7 @@ class API(): fanartcount += 1 return allartworks - def getSetArtwork(self, parentInfo=False): + def set_artwork(self): """ Gets the URLs to the Plex artwork, or empty string if not found. parentInfo=True will check for parent's artwork if None is found @@ -1142,30 +1125,32 @@ class API(): # Plex does not get much artwork - go ahead and get the rest from # fanart tv only for movie or tv show - externalId = self.getExternalItemId(collection=True) - if externalId is not None: + external_id = self.retrieve_external_item_id(collection=True) + if external_id is not None: try: - externalId, poster, background = externalId + external_id, poster, background = external_id except TypeError: poster, background = None, None if poster is not None: allartworks['Primary'] = poster if background is not None: allartworks['Backdrop'].append(background) - allartworks = self.getFanartTVArt(externalId, allartworks, True) + allartworks = self.lookup_fanart_tv(external_id, + allartworks, + set_info=True) else: LOG.info('Did not find a set/collection ID on TheMovieDB using %s.' - ' Artwork will be missing.', self.getTitle()[0]) + ' Artwork will be missing.', self.titles()[0]) return allartworks - def shouldStream(self): + def should_stream(self): """ Returns True if the item's 'optimizedForStreaming' is set, False other- wise """ return self.item[0].attrib.get('optimizedForStreaming') == '1' - def getMediastreamNumber(self): + def mediastream_number(self): """ Returns the Media stream as an int (mostly 0). Will let the user choose if several media streams are present for a PMS item (if settings are @@ -1176,10 +1161,10 @@ class API(): for entry in self.item.findall('./Media'): count += 1 if (count > 1 and ( - (self.getType() != 'clip' and + (self.plex_type() != 'clip' and settings('bestQuality') == 'false') or - (self.getType() == 'clip' and + (self.plex_type() == 'clip' and settings('bestTrailer') == 'false'))): # Several streams/files available. dialoglist = [] @@ -1222,7 +1207,7 @@ class API(): self.mediastream = media return media - def getTranscodeVideoPath(self, action, quality=None): + def transcode_video_path(self, action, quality=None): """ To be called on a VIDEO level of PMS xml response! @@ -1244,7 +1229,7 @@ class API(): TODO: mediaIndex """ if self.mediastream is None: - self.getMediastreamNumber() + self.mediastream_number() if quality is None: quality = {} xargs = client.getXArgsDeviceInfo() @@ -1269,7 +1254,7 @@ class API(): } # Path/key to VIDEO item of xml PMS response is needed, not part path = self.item.attrib['key'] - transcodePath = self.server + \ + transcode_path = self.server + \ '/video/:/transcode/universal/start.m3u8?' args = { 'audioBoost': settings('audioBoost'), @@ -1285,17 +1270,19 @@ class API(): 'hasMDE': 1, 'location': 'lan', 'subtitleSize': settings('subtitleSize') - # 'copyts': 1, - # 'offset': 0, # Resume point } # Look like Android to let the PMS use the transcoding profile xargs.update(headers) LOG.debug("Setting transcode quality to: %s", quality) args.update(quality) - url = transcodePath + urlencode(xargs) + '&' + urlencode(args) + url = transcode_path + urlencode(xargs) + '&' + urlencode(args) return url - def externalSubs(self): + def cache_external_subs(self): + """ + Downloads external subtitles temporarily to Kodi and returns a list + of their paths + """ externalsubs = [] try: mediastreams = self.item[0][self.part] @@ -1324,7 +1311,7 @@ class API(): fileindex += 1 # We don't know the language - no need to download else: - path = self.addPlexCredentialsToUrl( + path = self.attach_plex_token_to_url( "%s%s" % (self.server, key)) externalsubs.append(path) kodiindex += 1 @@ -1342,30 +1329,30 @@ class API(): if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH): makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH) path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename) - r = DU().downloadUrl(url, return_response=True) + response = DU().downloadUrl(url, return_response=True) try: - r.status_code + response.status_code except AttributeError: LOG.error('Could not temporarily download subtitle %s', url) return else: LOG.debug('Writing temp subtitle to %s', path) try: - with open(path, 'wb') as f: - f.write(r.content) + with open(path, 'wb') as filer: + filer.write(response.content) except UnicodeEncodeError: LOG.debug('Need to slugify the filename %s', path) path = slugify(path) - with open(path, 'wb') as f: - f.write(r.content) + with open(path, 'wb') as filer: + filer.write(response.content) return path - def GetKodiPremierDate(self): + def kodi_premiere_date(self): """ Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns - Kodi's "dd.mm.yyyy" + Kodi's "dd.mm.yyyy" or None """ - date = self.getPremiereDate() + date = self.premiere_date() if date is None: return try: @@ -1374,156 +1361,157 @@ class API(): date = None return date - def CreateListItemFromPlexItem(self, - listItem=None, - appendShowTitle=False, - appendSxxExx=False): - if self.getType() == v.PLEX_TYPE_PHOTO: - listItem = self.__createPhotoListItem(listItem) + def create_listitem(self, listitem=None, append_show_title=False, + append_sxxexx=False): + """ + Return a xbmcgui.ListItem() for this Plex item + """ + if self.plex_type() == v.PLEX_TYPE_PHOTO: + listitem = self._create_photo_listitem(listitem) # Only set the bare minimum of artwork - listItem.setArt({'icon': 'DefaultPicture.png', - 'fanart': self.__getOneArtwork('thumb')}) + listitem.setArt({'icon': 'DefaultPicture.png', + 'fanart': self._one_artwork('thumb')}) else: - listItem = self.__createVideoListItem(listItem, - appendShowTitle, - appendSxxExx) - self.add_video_streams(listItem) - self.set_listitem_artwork(listItem) - return listItem + listitem = self._create_video_listitem(listitem, + append_show_title, + append_sxxexx) + self.add_video_streams(listitem) + self.set_listitem_artwork(listitem) + return listitem - def __createPhotoListItem(self, listItem=None): + def _create_photo_listitem(self, listitem=None): """ Use for photo items only """ - title, _ = self.getTitle() - if listItem is None: - listItem = ListItem(title) + title, _ = self.titles() + if listitem is None: + listitem = ListItem(title) else: - listItem.setLabel(title) + listitem.setLabel(title) metadata = { - 'date': self.GetKodiPremierDate(), + 'date': self.kodi_premiere_date(), 'size': long(self.item[0][0].attrib.get('size', 0)), 'exif:width': self.item[0].attrib.get('width', ''), 'exif:height': self.item[0].attrib.get('height', ''), } - listItem.setInfo(type='image', infoLabels=metadata) - listItem.setProperty('plot', self.getPlot()) - listItem.setProperty('plexid', self.getRatingKey()) - return listItem + listitem.setInfo(type='image', infoLabels=metadata) + listitem.setProperty('plot', self.plot()) + listitem.setProperty('plexid', self.plex_id()) + return listitem - def __createVideoListItem(self, - listItem=None, - appendShowTitle=False, - appendSxxExx=False): + def _create_video_listitem(self, + listitem=None, + append_show_title=False, + append_sxxexx=False): """ Use for video items only Call on a child level of PMS xml response (e.g. in a for loop) - listItem : existing xbmcgui.ListItem to work with + listitem : existing xbmcgui.ListItem to work with otherwise, a new one is created - appendShowTitle : True to append TV show title to episode title - appendSxxExx : True to append SxxExx to episode title + append_show_title : True to append TV show title to episode title + append_sxxexx : True to append SxxExx to episode title Returns XBMC listitem for this PMS library item """ - title, sorttitle = self.getTitle() - typus = self.getType() + title, sorttitle = self.titles() + typus = self.plex_type() - if listItem is None: - listItem = ListItem(title) + if listitem is None: + listitem = ListItem(title) else: - listItem.setLabel(title) + listitem.setLabel(title) # Necessary; Kodi won't start video otherwise! - listItem.setProperty('IsPlayable', 'true') + listitem.setProperty('IsPlayable', 'true') # Video items, e.g. movies and episodes or clips - people = self.getPeople() - userdata = self.getUserData() + people = self.people() + userdata = self.userdata() metadata = { - 'genre': self.joinList(self.getGenres()), - 'year': self.getYear(), - 'rating': self.getAudienceRating(), + 'genre': self.list_to_string(self.genre_list()), + 'year': self.year(), + 'rating': self.audience_rating(), 'playcount': userdata['PlayCount'], 'cast': people['Cast'], - 'director': self.joinList(people.get('Director')), - 'plot': self.getPlot(), + 'director': self.list_to_string(people.get('Director')), + 'plot': self.plot(), 'sorttitle': sorttitle, 'duration': userdata['Runtime'], - 'studio': self.joinList(self.getStudios()), - 'tagline': self.getTagline(), - 'writer': self.joinList(people.get('Writer')), - 'premiered': self.getPremiereDate(), - 'dateadded': self.getDateCreated(), + 'studio': self.list_to_string(self.music_studio_list()), + 'tagline': self.tagline(), + 'writer': self.list_to_string(people.get('Writer')), + 'premiered': self.premiere_date(), + 'dateadded': self.date_created(), 'lastplayed': userdata['LastPlayedDate'], - 'mpaa': self.getMpaa(), - 'aired': self.getPremiereDate() + 'mpaa': self.content_rating(), + 'aired': self.premiere_date() } # Do NOT set resumetime - otherwise Kodi always resumes at that time # even if the user chose to start element from the beginning - # listItem.setProperty('resumetime', str(userdata['Resume'])) - listItem.setProperty('totaltime', str(userdata['Runtime'])) + # listitem.setProperty('resumetime', str(userdata['Resume'])) + listitem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: - key, show, season, episode = self.getEpisodeDetails() + _, show, season, episode = self.episode_data() season = -1 if season is None else int(season) episode = -1 if episode is None else int(episode) metadata['episode'] = episode metadata['season'] = season metadata['tvshowtitle'] = show if season and episode: - listItem.setProperty('episodeno', + listitem.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) - if appendSxxExx is True: + if append_sxxexx is True: title = "S%.2dE%.2d - %s" % (season, episode, title) - listItem.setArt({'icon': 'DefaultTVShows.png'}) - if appendShowTitle is True: + listitem.setArt({'icon': 'DefaultTVShows.png'}) + if append_show_title is True: title = "%s - %s " % (show, title) - if appendShowTitle or appendSxxExx: - listItem.setLabel(title) + if append_show_title or append_sxxexx: + listitem.setLabel(title) elif typus == v.PLEX_TYPE_MOVIE: - listItem.setArt({'icon': 'DefaultMovies.png'}) + listitem.setArt({'icon': 'DefaultMovies.png'}) else: # E.g. clips, trailers, ... - listItem.setArt({'icon': 'DefaultVideo.png'}) + listitem.setArt({'icon': 'DefaultVideo.png'}) - plexId = self.getRatingKey() - listItem.setProperty('plexid', plexId) + plex_id = self.plex_id() + listitem.setProperty('plexid', plex_id) with plexdb.Get_Plex_DB() as plex_db: try: - listItem.setProperty('dbid', - str(plex_db.getItem_byId(plexId)[0])) + listitem.setProperty('dbid', + str(plex_db.getItem_byId(plex_id)[0])) except TypeError: pass # Expensive operation metadata['title'] = title - listItem.setInfo('video', infoLabels=metadata) + listitem.setInfo('video', infoLabels=metadata) try: # Add context menu entry for information screen - listItem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) + listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) except TypeError: # Kodi fuck-up pass - return listItem + return listitem - def add_video_streams(self, listItem): + def add_video_streams(self, listitem): """ Add media stream information to xbmcgui.ListItem """ - for key, value in self.getMediaStreams().iteritems(): + for key, value in self.mediastreams().iteritems(): if value: - listItem.addStreamInfo(key, value) + listitem.addStreamInfo(key, value) - def validatePlayurl(self, path, typus, forceCheck=False, folder=False, - omitCheck=False): + def validate_playurl(self, path, typus, force_check=False, folder=False, + omit_check=False): """ Returns a valid path for Kodi, e.g. with '\' substituted to '\\' in Unicode. Returns None if this is not possible path : Unicode typus : Plex type from PMS xml - forceCheck : Will always try to check validity of path + force_check : Will always try to check validity of path Will also skip confirmation dialog if path not found folder : Set to True if path is a folder - omitCheck : Will entirely omit validity check if True + omit_check : Will entirely omit validity check if True """ if path is None: return None @@ -1537,8 +1525,8 @@ class API(): elif state.REPLACE_SMB_PATH is True: if path.startswith('\\\\'): path = 'smb:' + path.replace('\\', '/') - if ((state.PATH_VERIFIED and forceCheck is False) or - omitCheck is True): + if ((state.PATH_VERIFIED and force_check is False) or + omit_check is True): return path # exist() needs a / or \ at the end to work for directories @@ -1560,20 +1548,21 @@ class API(): check = exists_dir(path) if not check: - if forceCheck is False: + if force_check is False: # Validate the path is correct with user intervention - if self.askToValidate(path): + if self.ask_to_validate(path): state.STOP_SYNC = True path = None state.PATH_VERIFIED = True else: path = None - elif forceCheck is False: + elif force_check is False: # Only set the flag if we were not force-checking the path state.PATH_VERIFIED = True return path - def askToValidate(self, url): + @staticmethod + def ask_to_validate(url): """ Displays a YESNO dialog box: Kodi can't locate file: . Please verify the path. @@ -1593,12 +1582,10 @@ class API(): """ Set all artwork to the listitem """ - allartwork = self.getAllArtwork(parentInfo=True) + allartwork = self.artwork(parent_info=True) arttypes = { 'poster': "Primary", 'tvshow.poster': "Thumb", - 'clearart': "Art", - 'tvshow.clearart': "Art", 'clearart': "Primary", 'tvshow.clearart': "Primary", 'clearlogo': "Logo", @@ -1611,17 +1598,15 @@ class API(): 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 + # Backdrop is a list, grab the first backdrop + self._set_listitem_artprop(listitem, + arttype, + allartwork[art][0]) else: self._set_listitem_artprop(listitem, arttype, allartwork[art]) - def _set_listitem_artprop(self, listitem, arttype, path): + @staticmethod + def _set_listitem_artprop(listitem, arttype, path): if arttype in ( 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', 'medium_landscape', 'medium_poster', 'small_fanartimage', @@ -1629,29 +1614,3 @@ class API(): 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) - state.PLEX_IDS[try_decode(playurl)] = self.getRatingKey() - # window('%s.itemid' % plexitem, value=self.getRatingKey()) - window('%s.playcount' % plexitem, value=str(userdata['PlayCount'])) - - if itemtype == v.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) - # Direct play automatically appends external - # BUT: Plex may add additional subtitles NOT lying right next to video - if playmethod in ("DirectStream", "DirectPlay"): - listitem.setSubtitles(self.externalSubs(playurl)) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index ffd31e07..9b2a5048 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -57,10 +57,10 @@ class PlexCompanion(Thread): LOG.error('Could not download Plex metadata for: %s', data) return api = API(xml[0]) - if api.getType() == v.PLEX_TYPE_ALBUM: + if api.plex_type() == v.PLEX_TYPE_ALBUM: LOG.debug('Plex music album detected') PQ.init_playqueue_from_plex_children( - api.getRatingKey(), + api.plex_id(), transient_token=data.get('token')) elif data['containerKey'].startswith('/playQueues/'): _, container_key, _ = ParseContainerKey(data['containerKey']) @@ -70,7 +70,7 @@ class PlexCompanion(Thread): dialog('notification', lang(29999), lang(30128), icon='{error}') return playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() get_playlist_details_from_xml(playqueue, xml) playqueue.plex_transient_token = data.get('token') @@ -84,7 +84,7 @@ class PlexCompanion(Thread): if data.get('offset') != '0': state.RESUMABLE = True state.RESUME_PLAYBACK = True - playback_triage(api.getRatingKey(), api.getType(), resolve=False) + playback_triage(api.plex_id(), api.plex_type(), resolve=False) @staticmethod def _process_node(data): @@ -119,7 +119,7 @@ class PlexCompanion(Thread): return api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) PQ.update_playqueue_from_PMS( playqueue, playqueue_id=container_key, diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index b86fffe1..0b1a7e25 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -100,7 +100,7 @@ class ContextMenu(object): options.append(OPTIONS['PMS_Play']) if self.kodi_type in v.KODI_VIDEOTYPES: options.append(OPTIONS['Transcode']) - # userdata = self.api.getUserData() + # userdata = self.api.userdata() # if userdata['Favorite']: # # Remove from emby favourites # options.append(OPTIONS['RemoveFav']) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index b8f5bb26..dd228719 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -194,7 +194,7 @@ def GetSubFolders(nodeindex): ##### LISTITEM SETUP FOR VIDEONODES ##### -def createListItem(item, appendShowTitle=False, appendSxxExx=False): +def createListItem(item, append_show_title=False, append_sxxexx=False): title = item['title'] li = ListItem(title) li.setProperty('IsPlayable', "true") @@ -215,7 +215,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False): if season and episode: li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) - if appendSxxExx is True: + if append_sxxexx is True: title = "S%.2dE%.2d - %s" % (season, episode, title) if "firstaired" in item: @@ -223,7 +223,7 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False): if "showtitle" in item: metadata['TVshowTitle'] = item['showtitle'] - if appendShowTitle is True: + if append_show_title is True: title = item['showtitle'] + ' - ' + title if "rating" in item: @@ -375,8 +375,8 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit): # if the addon is called with recentepisodes parameter, # we return the recentepisodes list of the given tagname xbmcplugin.setContent(HANDLE, 'episodes') - appendShowTitle = settings('RecentTvAppendShow') == 'true' - appendSxxExx = settings('RecentTvAppendSeason') == 'true' + append_show_title = settings('RecentTvAppendShow') == 'true' + append_sxxexx = settings('RecentTvAppendSeason') == 'true' # First we get a list of all the TV shows - filtered by tag allshowsIds = set() params = { @@ -402,8 +402,8 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit): for episode in js.get_episodes(params): if episode['tvshowid'] in allshowsIds: listitem = createListItem(episode, - appendShowTitle=appendShowTitle, - appendSxxExx=appendSxxExx) + append_show_title=append_show_title, + append_sxxexx=append_sxxexx) xbmcplugin.addDirectoryItem( handle=HANDLE, url=episode['file'], @@ -501,7 +501,7 @@ def getExtraFanArt(plexid, plexPath): return xbmcplugin.endOfDirectory(HANDLE) api = API(xml[0]) - backdrops = api.getAllArtwork()['Backdrop'] + backdrops = api.artwork()['Backdrop'] for count, backdrop in enumerate(backdrops): # Same ordering as in artwork fanartFile = try_encode(join(fanartDir, "fanart%.3d.jpg" % count)) @@ -536,8 +536,8 @@ def getOnDeck(viewid, mediatype, tagname, limit): limit: Max. number of items to retrieve, e.g. 50 """ xbmcplugin.setContent(HANDLE, 'episodes') - appendShowTitle = settings('OnDeckTvAppendShow') == 'true' - appendSxxExx = settings('OnDeckTvAppendSeason') == 'true' + append_show_title = settings('OnDeckTvAppendShow') == 'true' + append_sxxexx = settings('OnDeckTvAppendSeason') == 'true' directpaths = settings('useDirectPaths') == 'true' if settings('OnDeckTVextended') == 'false': # Chances are that this view is used on Kodi startup @@ -558,16 +558,16 @@ def getOnDeck(viewid, mediatype, tagname, limit): limitcounter = 0 for item in xml: api = API(item) - listitem = api.CreateListItemFromPlexItem( - appendShowTitle=appendShowTitle, - appendSxxExx=appendSxxExx) + listitem = api.create_listitem( + append_show_title=append_show_title, + append_sxxexx=append_sxxexx) if directpaths: - url = api.getFilePath() + url = api.file_path() else: params = { 'mode': "play", - 'plex_id': api.getRatingKey(), - 'plex_type': api.getType() + 'plex_id': api.plex_id(), + 'plex_type': api.plex_type() } url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ % urlencode(params) @@ -646,8 +646,8 @@ def getOnDeck(viewid, mediatype, tagname, limit): for episode in episodes: # There will always be only 1 episode ('limit=1') listitem = createListItem(episode, - appendShowTitle=appendShowTitle, - appendSxxExx=appendSxxExx) + append_show_title=append_show_title, + append_sxxexx=append_sxxexx) xbmcplugin.addDirectoryItem(handle=HANDLE, url=episode['file'], listitem=listitem, @@ -823,25 +823,25 @@ def __build_folder(xml_element, plex_section_id=None): def __build_item(xml_element): api = API(xml_element) - listitem = api.CreateListItemFromPlexItem() - resume = api.getResume() + listitem = api.create_listitem() + resume = api.resume_point() if resume: listitem.setProperty('resumetime', str(resume)) - if (api.getKey().startswith('/system/services') or - api.getKey().startswith('http')): + if (api.path_and_plex_id().startswith('/system/services') or + api.path_and_plex_id().startswith('http')): params = { 'mode': 'plex_node', 'key': xml_element.attrib.get('key'), 'offset': xml_element.attrib.get('viewOffset', '0'), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) - elif api.getType() == v.PLEX_TYPE_PHOTO: + elif api.plex_type() == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: params = { 'mode': 'play', - 'plex_id': api.getRatingKey(), - 'plex_type': api.getType(), + 'plex_id': api.plex_id(), + 'plex_type': api.plex_type(), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) xbmcplugin.addDirectoryItem(handle=HANDLE, diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index a8e63238..9c59489a 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -106,17 +106,17 @@ class Items(object): return False api = API(xml[0]) if allartworks is None: - allartworks = api.getAllArtwork() - self.artwork.addArtwork(api.getFanartArtwork(allartworks), + allartworks = api.artwork() + self.artwork.addArtwork(api.fanart_artwork(allartworks), kodi_id, kodi_type, self.kodicursor) # Also get artwork for collections/movie sets if kodi_type == v.KODI_TYPE_MOVIE: - for setname in api.getCollections(): + for setname in api.collection_list(): LOG.debug('Getting artwork for movie set %s', setname) setid = self.kodi_db.createBoxset(setname) - self.artwork.addArtwork(api.getSetArtwork(), + self.artwork.addArtwork(api.set_artwork(), setid, v.KODI_TYPE_SET, self.kodicursor) @@ -133,13 +133,13 @@ class Items(object): for mediaitem in xml: api = API(mediaitem) # Get key and db entry on the Kodi db side - db_item = self.plex_db.getItem_byId(api.getRatingKey()) + db_item = self.plex_db.getItem_byId(api.plex_id()) try: fileid = db_item[1] except TypeError: continue # Grab the user's viewcount, resume points etc. from PMS' answer - userdata = api.getUserData() + userdata = api.userdata() # Write to Kodi DB self.kodi_db.addPlaystate(fileid, userdata['Resume'], @@ -191,7 +191,7 @@ class Movies(Items): # item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() # Cannot parse XML, abort if not itemid: LOG.error("Cannot parse XML data for movie") @@ -221,35 +221,35 @@ class Movies(Items): movieid) # fileId information - checksum = api.getChecksum() - dateadded = api.getDateCreated() - userdata = api.getUserData() + checksum = api.checksum() + dateadded = api.date_created() + userdata = api.userdata() playcount = userdata['PlayCount'] dateplayed = userdata['LastPlayedDate'] resume = userdata['Resume'] runtime = userdata['Runtime'] # item details - people = api.getPeople() - writer = api.joinList(people['Writer']) - director = api.joinList(people['Director']) - genres = api.getGenres() - genre = api.joinList(genres) - title, sorttitle = api.getTitle() - plot = api.getPlot() + people = api.people() + writer = api.list_to_string(people['Writer']) + director = api.list_to_string(people['Director']) + genres = api.genre_list() + genre = api.list_to_string(genres) + title, sorttitle = api.titles() + plot = api.plot() shortplot = None - tagline = api.getTagline() + tagline = api.tagline() votecount = None - collections = api.getCollections() + collections = api.collection_list() rating = userdata['Rating'] - year = api.getYear() - premieredate = api.getPremiereDate() - imdb = api.getProvider('imdb') - mpaa = api.getMpaa() - countries = api.getCountry() - country = api.joinList(countries) - studios = api.getStudios() + year = api.year() + premieredate = api.premiere_date() + imdb = api.provider('imdb') + mpaa = api.content_rating() + countries = api.country_list() + country = api.list_to_string(countries) + studios = api.music_studio_list() try: studio = studios[0] except IndexError: @@ -257,7 +257,7 @@ class Movies(Items): # Find one trailer trailer = None - extras = api.getExtras() + extras = api.extras_list() for extra in extras: # Only get 1st trailer element if extra['extraType'] == 1: @@ -269,12 +269,12 @@ class Movies(Items): do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = api.getFilePath(forceFirstMediaStream=True) + playurl = api.file_path(force_first_media=True) if playurl is None: # Something went wrong, trying to use non-direct paths do_indirect = True else: - playurl = api.validatePlayurl(playurl, api.getType()) + playurl = api.validate_playurl(playurl, api.plex_type()) if playurl is None: return False if "\\" in playurl: @@ -439,13 +439,13 @@ class Movies(Items): # Process countries self.kodi_db.addCountries(movieid, countries, "movie") # Process cast - self.kodi_db.addPeople(movieid, api.getPeopleList(), "movie") + self.kodi_db.addPeople(movieid, api.people_list(), "movie") # Process genres self.kodi_db.addGenres(movieid, genres, "movie") # Process artwork - artwork.addArtwork(api.getAllArtwork(), movieid, "movie", kodicursor) + artwork.addArtwork(api.artwork(), movieid, "movie", kodicursor) # Process stream details - self.kodi_db.addStreams(fileid, api.getMediaStreams(), runtime) + self.kodi_db.addStreams(fileid, api.mediastreams(), runtime) # Process studios self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags @@ -520,7 +520,7 @@ class TVShows(Items): api = API(item) update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() if not itemid: LOG.error("Cannot parse XML data for TV show") @@ -548,20 +548,20 @@ class TVShows(Items): showid) # fileId information - checksum = api.getChecksum() + checksum = api.checksum() # item details - genres = api.getGenres() - title, sorttitle = api.getTitle() - plot = api.getPlot() - rating = api.getAudienceRating() + genres = api.genre_list() + title, sorttitle = api.titles() + plot = api.plot() + rating = api.audience_rating() votecount = None - premieredate = api.getPremiereDate() - tvdb = api.getProvider('tvdb') - mpaa = api.getMpaa() - genre = api.joinList(genres) - studios = api.getStudios() - collections = api.getCollections() + premieredate = api.premiere_date() + tvdb = api.provider('tvdb') + mpaa = api.content_rating() + genre = api.list_to_string(genres) + studios = api.music_studio_list() + collections = api.collection_list() try: studio = studios[0] except IndexError: @@ -571,14 +571,14 @@ class TVShows(Items): do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = api.getTVShowPath() + playurl = api.tv_show_path() if playurl is None: # Something went wrong, trying to use non-direct paths do_indirect = True else: - playurl = api.validatePlayurl(playurl, - api.getType(), - folder=True) + playurl = api.validate_playurl(playurl, + api.plex_type(), + folder=True) if playurl is None: return False if "\\" in playurl: @@ -728,12 +728,12 @@ class TVShows(Items): kodicursor.execute(query, (path, None, None, 1, toppathid, pathid)) # Process cast - people = api.getPeopleList() + people = api.people_list() self.kodi_db.addPeople(showid, people, "tvshow") # Process genres self.kodi_db.addGenres(showid, genres, "tvshow") # Process artwork - allartworks = api.getAllArtwork() + allartworks = api.artwork() artwork.addArtwork(allartworks, showid, "tvshow", kodicursor) # Process studios self.kodi_db.addStudios(showid, studios, "tvshow") @@ -748,14 +748,14 @@ class TVShows(Items): Process a single season of a certain tv show """ api = API(item) - plex_id = api.getRatingKey() + plex_id = api.plex_id() if not plex_id: LOG.error('Error getting plex_id for season, skipping') return kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork - seasonnum = api.getIndex() + seasonnum = api.season_number() # Get parent tv show Plex id plexshowid = item.attrib.get('parentRatingKey') # Get Kodi showid @@ -768,13 +768,13 @@ class TVShows(Items): return seasonid = self.kodi_db.addSeason(showid, seasonnum) - checksum = api.getChecksum() + checksum = api.checksum() # Check whether Season already exists plex_dbitem = plex_db.getItem_byId(plex_id) update_item = False if plex_dbitem is None else True # Process artwork - allartworks = api.getAllArtwork() + allartworks = api.artwork() artwork.addArtwork(allartworks, seasonid, "season", kodicursor) if update_item: @@ -804,7 +804,7 @@ class TVShows(Items): # item update # If the item doesn't exist, we'll add it to the database update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() if not itemid: LOG.error('Error getting itemid for episode, skipping') return @@ -832,26 +832,26 @@ class TVShows(Items): episodeid) # fileId information - checksum = api.getChecksum() - dateadded = api.getDateCreated() - userdata = api.getUserData() + checksum = api.checksum() + dateadded = api.date_created() + userdata = api.userdata() playcount = userdata['PlayCount'] dateplayed = userdata['LastPlayedDate'] - tvdb = api.getProvider('tvdb') + tvdb = api.provider('tvdb') votecount = None # item details - peoples = api.getPeople() - director = api.joinList(peoples['Director']) - writer = api.joinList(peoples['Writer']) - title, _ = api.getTitle() - plot = api.getPlot() + peoples = api.people() + director = api.list_to_string(peoples['Director']) + writer = api.list_to_string(peoples['Writer']) + title, _ = api.titles() + plot = api.plot() rating = userdata['Rating'] - resume, runtime = api.getRuntime() - premieredate = api.getPremiereDate() + resume, runtime = api.resume_runtime() + premieredate = api.premiere_date() # episode details - series_id, _, season, episode = api.getEpisodeDetails() + series_id, _, season, episode = api.episode_data() if season is None: season = -1 @@ -886,14 +886,14 @@ class TVShows(Items): # GET THE FILE AND PATH ##### do_indirect = not state.DIRECT_PATHS - playurl = api.getFilePath(forceFirstMediaStream=True) + playurl = api.file_path(force_first_media=True) if state.DIRECT_PATHS: # Direct paths is set the Kodi way if playurl is None: # Something went wrong, trying to use non-direct paths do_indirect = True else: - playurl = api.validatePlayurl(playurl, api.getType()) + playurl = api.validate_playurl(playurl, api.plex_type()) if playurl is None: return False if "\\" in playurl: @@ -1081,19 +1081,19 @@ class TVShows(Items): )) kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process cast - people = api.getPeopleList() + people = api.people_list() self.kodi_db.addPeople(episodeid, people, "episode") # Process artwork # Wide "screenshot" of particular episode poster = item.attrib.get('thumb') if poster: - poster = api.addPlexCredentialsToUrl( + poster = api.attach_plex_token_to_url( "%s%s" % (self.server, poster)) artwork.addOrUpdateArt( poster, episodeid, "episode", "thumb", kodicursor) # Process stream details - streams = api.getMediaStreams() + streams = api.mediastreams() self.kodi_db.addStreams(fileid, streams, runtime) # Process playstates self.kodi_db.addPlaystate(fileid, @@ -1293,7 +1293,7 @@ class Music(Items): api = API(item) update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() plex_dbitem = plex_db.getItem_byId(itemid) try: artistid = plex_dbitem[0] @@ -1302,17 +1302,17 @@ class Music(Items): # The artist details ##### lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = api.getDateCreated() - checksum = api.getChecksum() + dateadded = api.date_created() + checksum = api.checksum() - name, _ = api.getTitle() - # musicBrainzId = api.getProvider('MusicBrainzArtist') + name, _ = api.titles() + # musicBrainzId = api.provider('MusicBrainzArtist') musicBrainzId = None - genres = ' / '.join(api.getGenres()) - bio = api.getPlot() + genres = ' / '.join(api.genre_list()) + bio = api.plot() # Associate artwork - artworks = api.getAllArtwork(parentInfo=True) + artworks = api.artwork(parent_info=True) thumb = artworks['Primary'] backdrops = artworks['Backdrop'] # List @@ -1381,7 +1381,7 @@ class Music(Items): api = API(item) update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() if not itemid: LOG.error('Error processing Album, skipping') return @@ -1394,19 +1394,19 @@ class Music(Items): # The album details ##### lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = api.getDateCreated() - userdata = api.getUserData() - checksum = api.getChecksum() + dateadded = api.date_created() + userdata = api.userdata() + checksum = api.checksum() - name, _ = api.getTitle() - # musicBrainzId = api.getProvider('MusicBrainzAlbum') + name, _ = api.titles() + # musicBrainzId = api.provider('MusicBrainzAlbum') musicBrainzId = None - year = api.getYear() - self.genres = api.getGenres() + year = api.year() + self.genres = api.genre_list() self.genre = ' / '.join(self.genres) - bio = api.getPlot() + bio = api.plot() rating = userdata['UserRating'] - studio = api.getMusicStudio() + studio = api.music_studio() artistname = item.attrib.get('parentTitle') if not artistname: artistname = item.attrib.get('originalTitle') @@ -1418,7 +1418,7 @@ class Music(Items): self.compilation = 1 break # Associate artwork - artworks = api.getAllArtwork(parentInfo=True) + artworks = api.artwork(parent_info=True) thumb = artworks['Primary'] if thumb: thumb = "%s" % thumb @@ -1576,7 +1576,7 @@ class Music(Items): api = API(item) update_item = True - itemid = api.getRatingKey() + itemid = api.plex_id() if not itemid: LOG.error('Error processing Song; skipping') return @@ -1592,9 +1592,9 @@ class Music(Items): songid = kodicursor.fetchone()[0] + 1 # The song details ##### - checksum = api.getChecksum() - dateadded = api.getDateCreated() - userdata = api.getUserData() + checksum = api.checksum() + dateadded = api.date_created() + userdata = api.userdata() playcount = userdata['PlayCount'] if playcount is None: # This is different to Video DB! @@ -1602,8 +1602,8 @@ class Music(Items): dateplayed = userdata['LastPlayedDate'] # item details - title, _ = api.getTitle() - # musicBrainzId = api.getProvider('MusicBrainzTrackId') + title, _ = api.titles() + # musicBrainzId = api.provider('MusicBrainzTrackId') musicBrainzId = None try: genres = self.genres @@ -1627,8 +1627,8 @@ class Music(Items): track = tracknumber else: track = disc*2**16 + tracknumber - year = api.getYear() - _, duration = api.getRuntime() + year = api.year() + _, duration = api.resume_runtime() rating = userdata['UserRating'] comment = None # Moods @@ -1642,12 +1642,12 @@ class Music(Items): do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = api.getFilePath(forceFirstMediaStream=True) + playurl = api.file_path(force_first_media=True) if playurl is None: # Something went wrong, trying to use non-direct paths do_indirect = True else: - playurl = api.validatePlayurl(playurl, api.getType()) + playurl = api.validate_playurl(playurl, api.plex_type()) if playurl is None: return False if "\\" in playurl: @@ -1660,7 +1660,7 @@ class Music(Items): if do_indirect: # Plex works a bit differently path = "%s%s" % (self.server, item[0][0].attrib.get('key')) - path = api.addPlexCredentialsToUrl(path) + path = api.attach_plex_token_to_url(path) filename = path.rsplit('/', 1)[1] path = path.replace(filename, '') @@ -1710,7 +1710,7 @@ class Music(Items): itemid) albumid = self.kodi_db.addAlbum( album_name, - api.getProvider('MusicBrainzAlbum')) + api.provider('MusicBrainzAlbum')) plex_db.addReference("%salbum%s" % (itemid, albumid), v.PLEX_TYPE_ALBUM, albumid, @@ -1854,7 +1854,7 @@ class Music(Items): if genres: self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG) # Update artwork - allart = api.getAllArtwork(parentInfo=True) + allart = api.artwork(parent_info=True) artwork.addArtwork(allart, songid, v.KODI_TYPE_SONG, kodicursor) if item.get('parentKey') is None: # Update album artwork diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 5390c8e6..5c263a30 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -766,7 +766,7 @@ class Kodidb_Functions(): ) self.cursor.execute(query, (fileid, 2, subtitletrack)) - def getResumes(self): + def resume_points(self): """ VIDEOS diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 4f6886f1..06e08f80 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -568,7 +568,7 @@ class LibrarySync(Thread): Output: self.updatelist, self.allPlexElementsId self.updatelist APPENDED(!!) list itemids (Plex Keys as - as received from API.getRatingKey()) + as received from API.plex_id()) One item in this list is of the form: 'itemId': xxx, 'itemType': 'Movies','TVShows', ... @@ -744,7 +744,7 @@ class LibrarySync(Thread): # Pull the list of movies and boxsets in Kodi try: self.allKodiElementsId = dict( - plex_db.getChecksum(v.PLEX_TYPE_MOVIE)) + plex_db.checksum(v.PLEX_TYPE_MOVIE)) except ValueError: self.allKodiElementsId = {} @@ -836,7 +836,7 @@ class LibrarySync(Thread): v.PLEX_TYPE_SEASON, v.PLEX_TYPE_EPISODE): try: - elements = dict(plex.getChecksum(kind)) + elements = dict(plex.checksum(kind)) self.allKodiElementsId.update(elements) # Yet empty/not yet synched except ValueError: @@ -1009,7 +1009,7 @@ class LibrarySync(Thread): with plexdb.Get_Plex_DB() as plex_db: # Pull the list of items already in Kodi try: - elements = dict(plex_db.getChecksum(kind)) + elements = dict(plex_db.checksum(kind)) self.allKodiElementsId.update(elements) # Yet empty/nothing yet synched except ValueError: @@ -1349,7 +1349,7 @@ class LibrarySync(Thread): plex_id) continue api = PlexAPI.API(xml[0]) - userdata = api.getUserData() + userdata = api.userdata() session['duration'] = userdata['Runtime'] session['viewCount'] = userdata['PlayCount'] # Sometimes, Plex tells us resume points in milliseconds and diff --git a/resources/lib/music.py b/resources/lib/music.py index 136c0a7a..726ff0b1 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -38,9 +38,9 @@ def excludefromscan_music_folders(): continue for location in library: if location.tag == 'Location': - path = api.validatePlayurl(location.attrib['path'], - typus=v.PLEX_TYPE_ARTIST, - omitCheck=True) + path = api.validate_playurl(location.attrib['path'], + typus=v.PLEX_TYPE_ARTIST, + omit_check=True) paths.append(__turn_to_regex(path)) try: with XmlKodiSetting('advancedsettings.xml', diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 35010d17..4619bcf5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -94,7 +94,7 @@ def playback_init(plex_id, plex_type, playqueue): return trailers = False api = API(xml[0]) - if (plex_type == v.PLEX_TYPE_MOVIE and not api.getResume() and + if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" @@ -144,11 +144,11 @@ def _prep_playlist_stack(xml): for item in xml: api = API(item) if (state.CONTEXT_MENU_PLAY is False and - api.getType() != v.PLEX_TYPE_CLIP): + api.plex_type() != v.PLEX_TYPE_CLIP): # If user chose to play via PMS or force transcode, do not # use the item path stored in the Kodi DB with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) + plex_dbitem = plex_db.getItem_byId(api.plex_id()) kodi_id = plex_dbitem[0] if plex_dbitem else None kodi_type = plex_dbitem[4] if plex_dbitem else None else: @@ -156,17 +156,17 @@ def _prep_playlist_stack(xml): kodi_id = None kodi_type = None for part, _ in enumerate(item[0]): - api.setPartNumber(part) + api.set_part_number(part) if kodi_id is None: # Need to redirect again to PKC to conclude playback params = { 'mode': 'play', - 'plex_id': api.getRatingKey(), - 'plex_type': api.getType() + 'plex_id': api.plex_id(), + 'plex_type': api.plex_type() } path = ('plugin://plugin.video.plexkodiconnect?%s' % urlencode(params)) - listitem = api.CreateListItemFromPlexItem() + listitem = api.create_listitem() listitem.setPath(try_encode(path)) else: # Will add directly via the Kodi DB @@ -179,9 +179,9 @@ def _prep_playlist_stack(xml): 'xml_video_element': item, 'listitem': listitem, 'part': part, - 'playcount': api.getViewCount(), - 'offset': api.getResume(), - 'id': api.getItemId() + 'playcount': api.viewcount(), + 'offset': api.resume_point(), + 'id': api.item_id() }) return stack @@ -238,15 +238,15 @@ def conclude_playback(playqueue, pos): if item.xml is not None: # Got a Plex element api = API(item.xml) - api.setPartNumber(item.part) - api.CreateListItemFromPlexItem(listitem) + api.set_part_number(item.part) + api.create_listitem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() else: playurl = item.file listitem.setPath(try_encode(playurl)) if item.playmethod in ('DirectStream', 'DirectPlay'): - listitem.setSubtitles(api.externalSubs()) + listitem.setSubtitles(api.cache_external_subs()) else: playutils.audio_subtitle_prefs(listitem) if state.RESUME_PLAYBACK is True: @@ -300,9 +300,9 @@ def process_indirect(key, offset, resolve=True): # Todo: implement offset api = API(xml[0]) listitem = PKC_ListItem() - api.CreateListItemFromPlexItem(listitem) + api.create_listitem(listitem) playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) + v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() item = PL.Playlist_Item() item.xml = xml[0] diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 041414ea..de480620 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -280,8 +280,8 @@ def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, """ item = Playlist_Item() api = API(xml_video_element) - item.plex_id = api.getRatingKey() - item.plex_type = api.getType() + item.plex_id = api.plex_id() + item.plex_type = api.plex_type() try: item.id = xml_video_element.attrib['%sItemID' % playlist.kind] except KeyError: diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index fffd311a..5cf61332 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -91,7 +91,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None): playqueue.clear() for i, child in enumerate(xml): api = API(child) - PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) + PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id()) playqueue.plex_transient_token = transient_token LOG.debug('Firing up Kodi player') Player().play(playqueue.kodi_pl, None, False, 0) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index b4753532..cad9b47e 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -29,18 +29,18 @@ class PlayUtils(): playurl is in unicode! """ - self.api.getMediastreamNumber() + self.api.mediastream_number() playurl = self.isDirectPlay() if playurl is not None: LOG.info("File is direct playing.") self.item.playmethod = 'DirectPlay' elif self.isDirectStream(): LOG.info("File is direct streaming.") - playurl = self.api.getTranscodeVideoPath('DirectStream') + playurl = self.api.transcode_video_path('DirectStream') self.item.playmethod = 'DirectStream' else: LOG.info("File is transcoding.") - playurl = self.api.getTranscodeVideoPath( + playurl = self.api.transcode_video_path( 'Transcode', quality={ 'maxVideoBitrate': self.get_bitrate(), @@ -58,16 +58,16 @@ class PlayUtils(): Returns the path/playurl if we can direct play, None otherwise """ # True for e.g. plex.tv watch later - if self.api.shouldStream() is True: + if self.api.should_stream() is True: LOG.info("Plex item optimized for direct streaming") return # Check whether we have a strm file that we need to throw at Kodi 1:1 - path = self.api.getFilePath() + path = self.api.file_path() if path is not None and path.endswith('.strm'): LOG.info('.strm file detected') - playurl = self.api.validatePlayurl(path, - self.api.getType(), - forceCheck=True) + playurl = self.api.validate_playurl(path, + self.api.plex_type(), + force_check=True) return playurl # set to either 'Direct Stream=1' or 'Transcode=2' # and NOT to 'Direct Play=0' @@ -77,9 +77,9 @@ class PlayUtils(): return if self.mustTranscode(): return - return self.api.validatePlayurl(path, - self.api.getType(), - forceCheck=True) + return self.api.validate_playurl(path, + self.api.plex_type(), + force_check=True) def mustTranscode(self): """ @@ -93,10 +93,10 @@ class PlayUtils(): - video bitrate above specified settings bitrate if the corresponding file settings are set to 'true' """ - if self.api.getType() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG): + if self.api.plex_type() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG): LOG.info('Plex clip or music track, not transcoding') return False - videoCodec = self.api.getVideoCodec() + videoCodec = self.api.video_codec() LOG.info("videoCodec: %s" % videoCodec) if self.item.force_transcode is True: LOG.info('User chose to force-transcode') @@ -136,7 +136,7 @@ class PlayUtils(): def isDirectStream(self): # Never transcode Music - if self.api.getType() == 'track': + if self.api.plex_type() == 'track': return True # set to 'Transcode=2' if settings('playType') == "2": @@ -232,7 +232,7 @@ class PlayUtils(): """ # Set media and part where we're at if self.api.mediastream is None: - self.api.getMediastreamNumber() + self.api.mediastream_number() try: mediastreams = self.api.plex_media_streams() except (TypeError, IndexError): @@ -302,7 +302,7 @@ class PlayUtils(): stream.attrib['codec'])) # We don't know the language - no need to download else: - path = self.api.addPlexCredentialsToUrl( + path = self.api.attach_plex_token_to_url( "%s%s" % (window('pms_server'), stream.attrib['key'])) downloadable_streams.append(index) diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index 742a461d..ca7949cf 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -296,7 +296,7 @@ class Plex_DB_Functions(): self.plexcursor.execute(query, (parent_id, kodi_type,)) return self.plexcursor.fetchall() - def getChecksum(self, plex_type): + def checksum(self, plex_type): """ Returns a list of tuples (plex_id, checksum) for plex_type """ From e02e9bcd1f283fc30264fce62d515ebb87421474 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Feb 2018 14:57:39 +0100 Subject: [PATCH 315/509] Rename thread methods --- resources/lib/PlexCompanion.py | 10 ++--- resources/lib/artwork.py | 12 +++--- resources/lib/command_pipeline.py | 4 +- resources/lib/kodimonitor.py | 2 +- resources/lib/library_sync/fanart.py | 10 ++--- resources/lib/library_sync/get_metadata.py | 6 +-- .../lib/library_sync/process_metadata.py | 4 +- resources/lib/library_sync/sync_info.py | 3 +- resources/lib/librarysync.py | 38 +++++++++---------- resources/lib/playqueue.py | 14 +++---- resources/lib/userclient.py | 10 ++--- resources/lib/utils.py | 32 ++++++++-------- resources/lib/websocket_client.py | 20 +++++----- 13 files changed, 82 insertions(+), 83 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 9b2a5048..a201a4d4 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -221,8 +221,8 @@ class PlexCompanion(Thread): httpd = self.httpd # Cache for quicker while loops client = self.client - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + stopped = self.stopped + suspended = self.suspended # Start up instances request_mgr = httppersist.RequestMgr() @@ -259,12 +259,12 @@ class PlexCompanion(Thread): if httpd: thread = Thread(target=httpd.handle_request) - while not thread_stopped(): + while not stopped(): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - while thread_suspended(): - if thread_stopped(): + while suspended(): + if stopped(): break sleep(1000) try: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index a7d2466d..0565b7b7 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -50,15 +50,15 @@ class Image_Cache_Thread(Thread): Thread.__init__(self) def run(self): - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + stopped = self.stopped + suspended = self.suspended queue = self.queue sleep_between = self.sleep_between - while not thread_stopped(): + while not stopped(): # In the event the server goes offline - while thread_suspended(): + while suspended(): # Set in service.py - if thread_stopped(): + if stopped(): # Abort was requested while waiting. We should exit LOG.info("---===### Stopped Image_Cache_Thread ###===---") return @@ -84,7 +84,7 @@ class Image_Cache_Thread(Thread): # download. All is well break except requests.ConnectionError: - if thread_stopped(): + if stopped(): # Kodi terminated break # Server thinks its a DOS attack, ('error 10053') diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 20d37d4a..0702313f 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -22,10 +22,10 @@ class Monitor_Window(Thread): Adjusts state.py accordingly """ def run(self): - thread_stopped = self.thread_stopped + stopped = self.stopped queue = state.COMMAND_PIPELINE_QUEUE LOG.info("----===## Starting Kodi_Play_Client ##===----") - while not thread_stopped(): + while not stopped(): if window('plex_command'): value = window('plex_command') window('plex_command', clear=True) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 18df1810..daea2468 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -400,7 +400,7 @@ class SpecialMonitor(Thread): LOG.info("----====# Starting Special Monitor #====----") # "Start from beginning", "Play from beginning" strings = (getLocalizedString(12021), getLocalizedString(12023)) - while not self.thread_stopped(): + while not self.stopped(): if (getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and getInfoLabel('Control.GetLabel(1002)') in strings): # Remember that the item IS indeed resumable diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 620f341d..a9c735ec 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -54,14 +54,14 @@ class Process_Fanart_Thread(Thread): Do the work """ log.debug("---===### Starting FanartSync ###===---") - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + stopped = self.stopped + suspended = self.suspended queue = self.queue - while not thread_stopped(): + while not stopped(): # In the event the server goes offline - while thread_suspended(): + while suspended(): # Set in service.py - if thread_stopped(): + if stopped(): # Abort was requested while waiting. We should exit log.info("---===### Stopped FanartSync ###===---") return diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index afe47b5b..7b97ead6 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -47,7 +47,7 @@ class Threaded_Get_Metadata(Thread): continue else: self.queue.task_done() - if self.thread_stopped(): + if self.stopped(): # Shutdown from outside requested; purge out_queue as well while not self.out_queue.empty(): # Still try because remaining item might have been taken @@ -78,8 +78,8 @@ class Threaded_Get_Metadata(Thread): # cache local variables because it's faster queue = self.queue out_queue = self.out_queue - thread_stopped = self.thread_stopped - while thread_stopped() is False: + stopped = self.stopped + while stopped() is False: # grabs Plex item from queue try: item = queue.get(block=False) diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index cdefa952..67f62974 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -68,9 +68,9 @@ class Threaded_Process_Metadata(Thread): item_fct = getattr(itemtypes, self.item_type) # cache local variables because it's faster queue = self.queue - thread_stopped = self.thread_stopped + stopped = self.stopped with item_fct() as item_class: - while thread_stopped() is False: + while stopped() is False: # grabs item from queue try: item = queue.get(block=False) diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 494b499a..3cd46811 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -52,14 +52,13 @@ class Threaded_Show_Sync_Info(Thread): # cache local variables because it's faster total = self.total dialog = DialogProgressBG('dialoglogProgressBG') - thread_stopped = self.thread_stopped dialog.create("%s %s: %s %s" % (lang(39714), self.item_type, str(total), lang(39715))) player = Player() total = 2 * total totalProgress = 0 - while thread_stopped() is False and not player.isPlaying(): + while self.stopped() is False and not player.isPlaying(): with LOCK: get_progress = GET_METADATA_COUNT process_progress = PROCESS_METADATA_COUNT diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 06e08f80..b7004df2 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -262,8 +262,8 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: - if (self.thread_stopped() or - self.thread_suspended() or + if (self.stopped() or + self.suspended() or not process[itemtype]()): xbmc.executebuiltin('InhibitIdleShutdown(false)') js.set_setting('screensaver.mode', screensaver) @@ -705,7 +705,7 @@ class LibrarySync(Thread): for thread in threads: # Threads might already have quit by themselves (e.g. Kodi exit) try: - thread.stop_thread() + thread.stop() except AttributeError: pass log.debug("Stop sent to all threads") @@ -753,7 +753,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False # Get items per view viewId = view['id'] @@ -773,7 +773,7 @@ class LibrarySync(Thread): self.GetAndProcessXMLs(itemType) # Update viewstate for EVERY item for view in views: - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -847,7 +847,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False # Get items per view viewId = view['id'] @@ -876,7 +876,7 @@ class LibrarySync(Thread): # PROCESS TV Seasons ##### # Cycle through tv shows for tvShowId in allPlexTvShowsId: - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False # Grab all seasons to tvshow from PMS seasons = GetAllPlexChildren(tvShowId) @@ -901,7 +901,7 @@ class LibrarySync(Thread): # PROCESS TV Episodes ##### # Cycle through tv shows for view in views: - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False # Grab all episodes to tvshow from PMS episodes = GetAllPlexLeaves(view['id']) @@ -936,7 +936,7 @@ class LibrarySync(Thread): # Update viewstate: for view in views: - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -973,7 +973,7 @@ class LibrarySync(Thread): for kind in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_SONG): - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False log.debug("Start processing music %s" % kind) self.allKodiElementsId = {} @@ -990,7 +990,7 @@ class LibrarySync(Thread): # Update viewstate for EVERY item for view in views: - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1017,7 +1017,7 @@ class LibrarySync(Thread): for view in views: if self.installSyncDone is not True: state.PATH_VERIFIED = False - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): return False # Get items per view itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) @@ -1105,7 +1105,7 @@ class LibrarySync(Thread): now = unix_timestamp() deleteListe = [] for i, item in enumerate(self.itemsToProcess): - if self.thread_stopped() or self.thread_suspended(): + if self.stopped() or self.suspended(): # Chances are that Kodi gets shut down break if item['state'] == 9: @@ -1484,8 +1484,8 @@ class LibrarySync(Thread): def run_internal(self): # Re-assign handles to have faster calls - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + stopped = self.stopped + suspended = self.suspended installSyncDone = self.installSyncDone background_sync = state.BACKGROUND_SYNC fullSync = self.fullSync @@ -1511,12 +1511,12 @@ class LibrarySync(Thread): if settings('FanartTV') == 'true': self.fanartthread.start() - while not thread_stopped(): + while not stopped(): # In the event the server goes offline - while thread_suspended(): + while suspended(): # Set in service.py - if thread_stopped(): + if stopped(): # Abort was requested while waiting. We should exit log.info("###===--- LibrarySync Stopped ---===###") return @@ -1613,7 +1613,7 @@ class LibrarySync(Thread): log.info('Doing scheduled full library scan') state.DB_SCAN = True window('plex_dbScan', value="true") - if fullSync() is False and not thread_stopped(): + if fullSync() is False and not stopped(): log.error('Could not finish scheduled full sync') self.force_dialog = True self.showKodiNote(lang(39410), diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 5cf61332..46b04af2 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -149,7 +149,7 @@ class PlayqueueMonitor(Thread): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.thread_stopped(): + if self.stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -194,7 +194,7 @@ class PlayqueueMonitor(Thread): for j in range(i, len(index)): index[j] += 1 for i in reversed(index): - if self.thread_stopped(): + if self.stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -203,12 +203,12 @@ class PlayqueueMonitor(Thread): LOG.debug('Done comparing playqueues') def run(self): - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + stopped = self.stopped + suspended = self.suspended LOG.info("----===## Starting PlayqueueMonitor ##===----") - while not thread_stopped(): - while thread_suspended(): - if thread_stopped(): + while not stopped(): + while suspended(): + if stopped(): break sleep(1000) with LOCK: diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 69af21f0..460220d7 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -287,11 +287,11 @@ class UserClient(Thread): def run(self): LOG.info("----===## Starting UserClient ##===----") - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended - while not thread_stopped(): - while thread_suspended(): - if thread_stopped(): + stopped = self.stopped + suspended = self.suspended + while not stopped(): + while suspended(): + if stopped(): break sleep(1000) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 0a6e62c5..87084144 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -1004,13 +1004,13 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): """ Decorator to add the following methods to a threading class: - suspend_thread(): pauses the thread - resume_thread(): resumes the thread - stop_thread(): stopps/kills the thread + suspend(): pauses the thread + resume(): resumes the thread + stop(): stopps/kills the thread - thread_suspended(): returns True if thread is suspended - thread_stopped(): returns True if thread is stopped (or should stop ;-)) - ALSO returns True if PKC should exit + suspended(): returns True if thread is suspended + stopped(): returns True if thread is stopped (or should stop ;-)) + ALSO returns True if PKC should exit Also adds the following class attributes: thread_stopped @@ -1043,28 +1043,28 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): cls.thread_suspended = False # Define new class methods and attach them to class - def stop_thread(self): + def stop(self): """ Call to stop this thread """ self.thread_stopped = True - cls.stop_thread = stop_thread + cls.stop = stop - def suspend_thread(self): + def suspend(self): """ Call to suspend this thread """ self.thread_suspended = True - cls.suspend_thread = suspend_thread + cls.suspend = suspend - def resume_thread(self): + def resume(self): """ Call to revive a suspended thread back to life """ self.thread_suspended = False - cls.resume_thread = resume_thread + cls.resume = resume - def thread_suspended(self): + def suspended(self): """ Returns True if the thread is suspended """ @@ -1074,9 +1074,9 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): if getattr(state, suspend): return True return False - cls.thread_suspended = thread_suspended + cls.suspended = suspended - def thread_stopped(self): + def stopped(self): """ Returns True if the thread is stopped """ @@ -1086,7 +1086,7 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): if getattr(state, stop): return True return False - cls.thread_stopped = thread_stopped + cls.stopped = stopped # Return class to render this a decorator return cls diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index a494fb7a..cbc2f681 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -58,16 +58,16 @@ class WebSocket(Thread): counter = 0 handshake_counter = 0 - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended - while not thread_stopped(): + stopped = self.stopped + suspended = self.suspended + while not stopped(): # In the event the server goes offline - while thread_suspended(): + while suspended(): # Set in service.py if self.ws is not None: self.ws.close() self.ws = None - if thread_stopped(): + if stopped(): # Abort was requested while waiting. We should exit LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) @@ -255,16 +255,16 @@ class Alexa_Websocket(WebSocket): pass # Path in thread_methods - def stop_thread(self): + def stop(self): self.thread_stopped = True - def suspend_thread(self): + def suspend(self): self.thread_suspended = True - def resume_thread(self): + def resume(self): self.thread_suspended = False - def thread_stopped(self): + def stopped(self): if self.thread_stopped is True: return True if state.STOP_PKC: @@ -272,7 +272,7 @@ class Alexa_Websocket(WebSocket): return False # The culprit - def thread_suspended(self): + def suspended(self): """ Overwrite method since we need to check for plex token """ From 0b5cd46d6c509fd937aecb5ba70e17afdb095505 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 08:10:39 +0100 Subject: [PATCH 316/509] API code optimization --- resources/lib/PlexAPI.py | 189 +++++++++++++------------------------ resources/lib/itemtypes.py | 13 +-- 2 files changed, 71 insertions(+), 131 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 0f72d4da..9cc1e7da 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -102,7 +102,7 @@ class API(object): """ Returns the type of media, e.g. 'movie' or 'clip' for trailers """ - return self.item.attrib.get('type') + return self.item.get('type') def checksum(self): """ @@ -110,21 +110,19 @@ class API(object): WATCH OUT - time in Plex, not Kodi ;-) """ # Include a letter to prohibit saving as an int! - checksum = "K%s%s" % (self.plex_id(), - self.item.attrib.get('updatedAt', '')) - return checksum + return "K%s%s" % (self.plex_id(), self.item.get('updatedAt', '')) def plex_id(self): """ Returns the Plex ratingKey such as '246922' as a string or None """ - return self.item.attrib.get('ratingKey') + return self.item.get('ratingKey') def path_and_plex_id(self): """ Returns the Plex key such as '/library/metadata/246922' or None """ - return self.item.attrib.get('key') + return self.item.get('key') def plex_media_streams(self): """ @@ -170,9 +168,9 @@ class API(object): # Let Plex transcode # max width/height supported by plex image transcoder is 1920x1080 path = self.server + PF.transcode_image_path( - self.item[0][0].attrib.get('key'), + self.item[0][0].get('key'), window('pms_token'), - "%s%s" % (self.server, self.item[0][0].attrib.get('key')), + "%s%s" % (self.server, self.item[0][0].get('key')), 1920, 1080) else: @@ -191,14 +189,14 @@ class API(object): res = None for child in self.item: if child.tag == 'Location': - res = child.attrib.get('path') + res = child.get('path') return res def season_number(self): """ Returns the 'index' of an PMS XML reply. Depicts e.g. season number. """ - return self.item.attrib.get('index') + return self.item.get('index') def date_created(self): """ @@ -206,7 +204,7 @@ class API(object): If not found, returns 2000-01-01 10:00:00 """ - res = self.item.attrib.get('addedAt') + res = self.item.get('addedAt') if res is not None: res = unix_date_to_kodi(res) else: @@ -343,8 +341,8 @@ class API(object): name = child.attrib['tag'] name_id = child.attrib['id'] typus = PEOPLE_OF_INTEREST[child.tag] - url = child.attrib.get('thumb') - role = child.attrib.get('role') + url = child.get('thumb') + role = child.get('role') people.append({ 'Name': name, 'Type': typus, @@ -373,9 +371,8 @@ class API(object): Return IMDB, e.g. "tt0903624". Returns None if not found """ - item = self.item.attrib try: - item = item['guid'] + item = self.item.attrib['guid'] except KeyError: return None @@ -402,29 +399,29 @@ class API(object): sorttitle = title, if no sorttitle is found """ - title = self.item.attrib.get('title', 'Missing Title Name') - sorttitle = self.item.attrib.get('titleSort', title) + title = self.item.get('title', 'Missing Title Name') + sorttitle = self.item.get('titleSort', title) return title, sorttitle def plot(self): """ Returns the plot or None. """ - return self.item.attrib.get('summary', None) + return self.item.get('summary') def tagline(self): """ Returns a shorter tagline or None """ - return self.item.attrib.get('tagline', None) + return self.item.get('tagline') def audience_rating(self): """ Returns the audience rating, 'rating' itself or 0.0 """ - res = self.item.attrib.get('audienceRating') + res = self.item.get('audienceRating') if res is None: - res = self.item.attrib.get('rating') + res = self.item.get('rating') try: res = float(res) except (ValueError, TypeError): @@ -435,7 +432,7 @@ class API(object): """ Returns the production(?) year ("year") or None """ - return self.item.attrib.get('year', None) + return self.item.get('year') def resume_point(self): """ @@ -456,17 +453,14 @@ class API(object): Output is the tuple: resume, runtime as ints. 0 if not found """ - item = self.item.attrib - try: - runtime = float(item['duration']) + runtime = float(self.item.attrib['duration']) except (KeyError, ValueError): runtime = 0.0 try: - resume = float(item['viewOffset']) + resume = float(self.item.attrib['viewOffset']) except (KeyError, ValueError): resume = 0.0 - runtime = int(runtime * v.PLEX_TO_KODI_TIMEFACTOR) resume = int(resume * v.PLEX_TO_KODI_TIMEFACTOR) return resume, runtime @@ -475,7 +469,7 @@ class API(object): """ Get the content rating or None """ - mpaa = self.item.attrib.get('contentRating', None) + mpaa = self.item.get('contentRating', None) # Convert more complex cases if mpaa in ("NR", "UR"): # Kodi seems to not like NR, but will accept Rated Not Rated @@ -496,13 +490,13 @@ class API(object): """ Returns the "originallyAvailableAt" or None """ - return self.item.attrib.get('originallyAvailableAt') + return self.item.get('originallyAvailableAt') def music_studio(self): """ Returns the 'studio' or None """ - return self.item.attrib.get('studio') + return self.item.get('studio') def music_studio_list(self): """ @@ -532,7 +526,7 @@ class API(object): @staticmethod def list_to_string(listobject): """ - Smart-joins the listobject into a single string using a " / " separator. + Smart-joins the listobject into a single string using a " / " separator If the list is empty, smart_join returns an empty string. """ string = " / ".join(listobject) @@ -542,7 +536,7 @@ class API(object): """ Returns the 'parentRatingKey' as a string or None """ - return self.item.attrib.get('parentRatingKey') + return self.item.get('parentRatingKey') def episode_data(self): """ @@ -581,13 +575,9 @@ class API(object): Returns current playQueueItemID or if unsuccessful the playListItemID If not found, None is returned """ - try: - answ = self.item.attrib['playQueueItemID'] - except KeyError: - try: - answ = self.item.attrib['playListItemID'] - except KeyError: - answ = None + answ = self.item.get('playQueueItemID') + if answ is None: + answ = self.item.get('playListItemID') return answ def _data_from_part_or_media(self, key): @@ -597,17 +587,10 @@ class API(object): If all fails, None is returned. """ - media = self.item[0].attrib - part = self.item[0][self.part].attrib - - try: - try: - value = part[key] - except KeyError: - value = media[key] - except KeyError: - value = None - return value + answ = self.item[0][self.part].get(key) + if answ is None: + answ = self.item[0].get(key) + return answ def video_codec(self): """ @@ -638,53 +621,24 @@ class API(object): 'container': self._data_from_part_or_media('container'), } try: - answ['bitDepth'] = self.item[0][self.part][self.mediastream].get('bitDepth') + answ['bitDepth'] = self.item[0][self.part][self.mediastream].get( + 'bitDepth') except (TypeError, AttributeError, KeyError, IndexError): answ['bitDepth'] = None return answ - def extras_list(self): + def trailer_id(self): """ - Currently ONLY returns the very first trailer found! - - Returns a list of trailer and extras from PMS XML. Returns [] if - no extras are found. - Extratypes: - 1: Trailer - 5: Behind the scenes - - Output: list of dicts with one entry of the form: - 'ratingKey': e.g. '12345' - 'title': - 'thumb': artwork - 'duration': - 'extraType': - 'originallyAvailableAt': - 'year': + Returns the ratingKey (plex_id) of the trailer or None """ - elements = [] - extras = self.item.find('Extras') - if extras is None: - return elements - for extra in extras: + for extra in self.item.iterfind('Extras'): try: typus = int(extra.attrib['extraType']) except (KeyError, TypeError): typus = None if typus != 1: continue - duration = float(extra.attrib.get('duration', 0.0)) - elements.append({ - 'ratingKey': extra.attrib.get('ratingKey'), - 'title': extra.attrib.get('title'), - 'thumb': extra.attrib.get('thumb'), - 'duration': int(duration * v.PLEX_TO_KODI_TIMEFACTOR), - 'extraType': typus, - 'originallyAvailableAt': extra.attrib.get('originallyAvailableAt'), - 'year': extra.attrib.get('year') - }) - break - return elements + return extra.get('ratingKey') def mediastreams(self): """ @@ -704,7 +658,7 @@ class API(object): subtitlelanguages = [] try: # Sometimes, aspectratio is on the "toplevel" - aspect = self.item[0].attrib.get('aspectRatio') + aspect = self.item[0].get('aspectRatio') except IndexError: # There is no stream info at all, returning empty return { @@ -714,7 +668,7 @@ class API(object): } # Loop over parts for child in self.item[0]: - container = child.attrib.get('container') + container = child.get('container') # Loop over Streams for grandchild in child: stream = grandchild.attrib @@ -789,30 +743,19 @@ class API(object): } """ allartworks = { - 'Primary': "", # corresponds to Plex poster ('thumb') + 'Primary': self._one_artwork('thumb'), 'Art': "", - 'Banner': "", # corresponds to Plex banner ('banner') for series + 'Banner': self._one_artwork('banner'), 'Logo': "", - 'Thumb': "", # corresponds to Plex (grand)parent posters (thumb) + 'Thumb': self._one_artwork('grandparentThumb'), 'Disc': "", - 'Backdrop': [] # Corresponds to Plex fanart ('art') + 'Backdrop': [self._one_artwork('art')] } - # Process backdrops - # Get background artwork URL - allartworks['Backdrop'].append(self._one_artwork('art')) - # Get primary "thumb" pictures: - allartworks['Primary'] = self._one_artwork('thumb') - # Banner (usually only on tv series level) - allartworks['Banner'] = self._one_artwork('banner') - # For e.g. TV shows, get series thumb - allartworks['Thumb'] = self._one_artwork('grandparentThumb') - # Process parent items if the main item is missing artwork if parent_info: # Process parent backdrops if not allartworks['Backdrop']: - allartworks['Backdrop'].append( - self._one_artwork('parentArt')) + allartworks['Backdrop'].append(self._one_artwork('parentArt')) if not allartworks['Primary']: allartworks['Primary'] = self._one_artwork('parentThumb') return allartworks @@ -863,7 +806,7 @@ class API(object): LOG.info('Plex did not provide ID for IMDB or TVDB. Start ' 'lookup process') else: - LOG.info('Start movie set/collection lookup on themoviedb using %s', + LOG.info('Start movie set/collection lookup on themoviedb with %s', item.get('title', '')) api_key = settings('themoviedbAPIKey') @@ -936,7 +879,7 @@ class API(object): break # if a match was not found, we accept the closest match from TMDB - if match_found is None and len(data.get("results")): + if match_found is None and data.get("results"): LOG.info('Using very first match from themoviedb') match_found = entry = data.get("results")[0] @@ -1068,14 +1011,16 @@ class API(object): # select image in preferred language for entry in data[fanarttvimage]: if entry.get("lang") == v.KODILANGUAGE: - allartworks[fanarttype[1]] = entry.get("url", "").replace(' ', '%20') + allartworks[fanarttype[1]] = \ + entry.get("url", "").replace(' ', '%20') break # just grab the first english OR undefinded one as fallback # (so we're actually grabbing the more popular one) if not allartworks.get(fanarttype[1]): for entry in data[fanarttvimage]: if entry.get("lang") in ("en", "00"): - allartworks[fanarttype[1]] = entry.get("url", "").replace(' ', '%20') + allartworks[fanarttype[1]] = \ + entry.get("url", "").replace(' ', '%20') break # grab extrafanarts in list @@ -1148,7 +1093,7 @@ class API(object): Returns True if the item's 'optimizedForStreaming' is set, False other- wise """ - return self.item[0].attrib.get('optimizedForStreaming') == '1' + return self.item[0].get('optimizedForStreaming') == '1' def mediastream_number(self): """ @@ -1158,17 +1103,17 @@ class API(object): """ # How many streams do we have? count = 0 - for entry in self.item.findall('./Media'): + for entry in self.item.iterfind('./Media'): count += 1 if (count > 1 and ( (self.plex_type() != 'clip' and settings('bestQuality') == 'false') - or + or (self.plex_type() == 'clip' and settings('bestTrailer') == 'false'))): # Several streams/files available. dialoglist = [] - for entry in self.item.findall('./Media'): + for entry in self.item.iterfind('./Media'): # Get additional info (filename / languages) filename = None if 'file' in entry[0].attrib: @@ -1189,17 +1134,17 @@ class API(object): option = '%s: ' % try_encode(languages) if 'videoResolution' in entry.attrib: option = '%s%sp ' % (option, - entry.attrib.get('videoResolution')) + entry.get('videoResolution')) if 'videoCodec' in entry.attrib: option = '%s%s' % (option, - entry.attrib.get('videoCodec')) + entry.get('videoCodec')) option = option.strip() + ' - ' if 'audioProfile' in entry.attrib: option = '%s%s ' % (option, - entry.attrib.get('audioProfile')) + entry.get('audioProfile')) if 'audioCodec' in entry.attrib: option = '%s%s ' % (option, - entry.attrib.get('audioCodec')) + entry.get('audioCodec')) dialoglist.append(option) media = dialog('select', 'Select stream', dialoglist) else: @@ -1262,7 +1207,7 @@ class API(object): 'directPlay': 0, 'directStream': 1, 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' - 'session': window('plex_client_Id'), + 'session': window('plex_client_Id'), 'fastSeek': 1, 'path': path, 'mediaIndex': self.mediastream, @@ -1293,16 +1238,16 @@ class API(object): for stream in mediastreams: # Since plex returns all possible tracks together, have to pull # only external subtitles - only for these a 'key' exists - if stream.attrib.get('streamType') != "3": + if stream.get('streamType') != "3": # Not a subtitle continue # Only set for additional external subtitles NOT lying beside video - key = stream.attrib.get('key') + key = stream.get('key') # Only set for dedicated subtitle files lying beside video # ext = stream.attrib.get('format') if key: # We do know the language - temporarily download - if stream.attrib.get('languageCode') is not None: + if stream.get('languageCode') is not None: path = self.download_external_subtitles( "{server}%s" % key, "subtitle%02d.%s.%s" % (fileindex, @@ -1390,9 +1335,9 @@ class API(object): listitem.setLabel(title) metadata = { 'date': self.kodi_premiere_date(), - 'size': long(self.item[0][0].attrib.get('size', 0)), - 'exif:width': self.item[0].attrib.get('width', ''), - 'exif:height': self.item[0].attrib.get('height', ''), + 'size': long(self.item[0][0].get('size', 0)), + 'exif:width': self.item[0].get('width', ''), + 'exif:height': self.item[0].get('height', ''), } listitem.setInfo(type='image', infoLabels=metadata) listitem.setProperty('plot', self.plot()) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9c59489a..54c07496 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -255,15 +255,10 @@ class Movies(Items): except IndexError: studio = None - # Find one trailer - trailer = None - extras = api.extras_list() - for extra in extras: - # Only get 1st trailer element - if extra['extraType'] == 1: - trailer = ('plugin://%s?plex_id=%s&plex_type=%s&mode=play' - % (v.ADDON_ID, extra['ratingKey'], v.PLEX_TYPE_CLIP)) - break + trailer = api.trailer_id() + if trailer: + trailer = ('plugin://%s?plex_id=%s&plex_type=%s&mode=play' + % (v.ADDON_ID, trailer, v.PLEX_TYPE_CLIP)) # GET THE FILE AND PATH ##### do_indirect = not state.DIRECT_PATHS From 66a24a39b66c8da0f733dc2ae07f231ad641ee22 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 08:18:55 +0100 Subject: [PATCH 317/509] Fix KeyError --- resources/lib/PlexCompanion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index a201a4d4..f0fc4e0c 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -95,7 +95,7 @@ class PlexCompanion(Thread): params = { 'mode': 'plex_node', 'key': '{server}%s' % data.get('key'), - 'view_offset': data.get('offset'), + 'offset': data.get('offset'), 'play_directly': 'true' } executebuiltin('RunPlugin(plugin://%s?%s)' From e595bd5e797bd72ca9587685cadef9a32226f9ac Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 08:26:32 +0100 Subject: [PATCH 318/509] Use api --- resources/lib/entrypoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index dd228719..7650a5b8 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -831,7 +831,7 @@ def __build_item(xml_element): api.path_and_plex_id().startswith('http')): params = { 'mode': 'plex_node', - 'key': xml_element.attrib.get('key'), + 'key': âpi.path_and_plex_id(), 'offset': xml_element.attrib.get('viewOffset', '0'), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) From 1d791905744d083f646ef34af78462baae246fc1 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 09:22:39 +0100 Subject: [PATCH 319/509] Revert "Use api" This reverts commit e595bd5e797bd72ca9587685cadef9a32226f9ac. --- resources/lib/entrypoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 7650a5b8..dd228719 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -831,7 +831,7 @@ def __build_item(xml_element): api.path_and_plex_id().startswith('http')): params = { 'mode': 'plex_node', - 'key': âpi.path_and_plex_id(), + 'key': xml_element.attrib.get('key'), 'offset': xml_element.attrib.get('viewOffset', '0'), } url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) From af961dbaf48f9d889d82fc945829b5fabadd58e3 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 20:14:25 +0100 Subject: [PATCH 320/509] Fix GB content ratings - Should fix #401 --- resources/lib/PlexAPI.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 9cc1e7da..019c51ef 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -469,11 +469,15 @@ class API(object): """ Get the content rating or None """ - mpaa = self.item.get('contentRating', None) + mpaa = self.item.get('contentRating') + if mpaa is None: + return # Convert more complex cases if mpaa in ("NR", "UR"): # Kodi seems to not like NR, but will accept Rated Not Rated mpaa = "Rated Not Rated" + elif mpaa.startswith('gb/'): + mpaa = mpaa.replace('gb/', 'UK:', 1) return mpaa def country_list(self): From 754432f5bc2cbf554b49ebb5b369e854631ddd98 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 21:20:26 +0100 Subject: [PATCH 321/509] Fix Kodi boot loop - Fixes #402 --- resources/lib/music.py | 2 +- resources/lib/utils.py | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/resources/lib/music.py b/resources/lib/music.py index 726ff0b1..3f9ff941 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -55,7 +55,7 @@ def excludefromscan_music_folders(): else: LOG.info('New Plex music library detected: %s', path) xml.set_setting(['audio', 'excludefromscan', 'regexp'], - value=path, check_existing=False) + value=path, append=True) # We only need to reboot if we ADD new paths! reboot = xml.write_xml # Delete obsolete entries diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 87084144..28620155 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -705,8 +705,7 @@ class XmlKodiSetting(object): break return element - def set_setting(self, node_list, value=None, attrib=None, - check_existing=True): + def set_setting(self, node_list, value=None, attrib=None, append=False): """ node_list is a list of node names starting from the outside, ignoring the outter advancedsettings. @@ -730,29 +729,29 @@ class XmlKodiSetting(object): If the dict attrib is set, the Element's attributs will be appended accordingly - If check_existing is True, it will return the FIRST matching element of - node_list. Set to False if there are several elements of the same tag! + If append is True, the last element of node_list with value and attrib + will always be added. WARNING: this will set self.write_xml to True! Returns the (last) etree element """ attrib = attrib or {} value = value or '' - if check_existing is True: + if not append: old = self.get_setting(node_list) - if old is not None: - already_set = True - if old.text.strip() != value: - already_set = False - elif old.attrib != attrib: - already_set = False - if already_set is True: - LOG.debug('Element has already been found') - return old - # Need to set new setting, indeed + if (old is not None and + old.text.strip() == value and + old.attrib == attrib): + # Already set exactly these values + return old + LOG.debug('Adding etree to: %s, value: %s, attrib: %s, append: %s', + node_list, value, attrib, append) self.write_xml = True element = self.root - for node in node_list: + nodes = node_list[:-1] if append else node_list + for node in nodes: element = self._set_sub_element(element, node) + if append: + element = etree.SubElement(element, node_list[-1]) # Write new values element.text = value if attrib: From 3fabb21dac1e59753c28c5a7c32abf947ffa1da2 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 12 Feb 2018 21:27:22 +0100 Subject: [PATCH 322/509] Fix indent of xmls --- resources/lib/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 28620155..88beb332 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -590,8 +590,8 @@ def indent(elem, level=0): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i - for item in elem: - indent(item, level+1) + for elem in elem: + indent(elem, level+1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: From 74210184033b6b511cb35280ceb4bfbcdcf35c86 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 13 Feb 2018 07:24:39 +0100 Subject: [PATCH 323/509] Fix KeyError when browsing On Deck --- resources/lib/videonodes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 5636fb3c..619fdc59 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -11,6 +11,7 @@ from xbmcvfs import exists from utils import window, settings, language as lang, try_encode, indent, \ normalize_nodes, exists_dir, try_decode import variables as v +import state ############################################################################### @@ -46,7 +47,7 @@ class VideoNodes(object): def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False): # Plex: reassign mediatype due to Kodi inner workings # How many items do we get at most? - limit = window('fetch_pms_item_number') + limit = state.FETCH_PMS_ITEM_NUMBER mediatypes = { 'movie': 'movies', 'show': 'tvshows', From 0f2c3813a2a6c9128dc24aaa7d004ad86a320426 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 13 Feb 2018 12:14:04 +0100 Subject: [PATCH 324/509] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab3012c8..0a14c3c9 100644 --- a/README.md +++ b/README.md @@ -103,11 +103,11 @@ I'm not in any way affiliated with Plex. Thank you very much for a small donatio [![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB) ![ETH-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F) -**Ethereum (ETH) address: +**Ethereum address: 0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F** ![BTX-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT) -**Bitcoin (BTX) address: +**Bitcoin address: 3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT** From 97d777fdee42c89d3df91feae3e6b2c81c43054d Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 19:52:53 +0100 Subject: [PATCH 325/509] Make sure that empty XML elements get deleted - Fixes #402 --- resources/lib/utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 88beb332..d73acf09 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -664,11 +664,40 @@ class XmlKodiSetting(object): raise # Only safe to file if we did not botch anything if self.write_xml is True: + self._remove_empty_elements() # Indent and make readable indent(self.root) # Safe the changed xml self.tree.write(self.path, encoding="UTF-8") + def _is_empty(self, element, empty_elements): + empty = True + for child in element: + empty_child = True + if list(child): + empty_child = self._is_empty(child, empty_elements) + if empty_child and (child.attrib or + (child.text and child.text.strip())): + empty_child = False + if empty_child: + empty_elements.append((element, child)) + else: + # At least one non-empty entry - hence we cannot delete the + # original element itself + empty = False + return empty + + def _remove_empty_elements(self): + """ + Deletes all empty XML elements, otherwise Kodi/PKC gets confused + This is recursive, so an empty element with empty children will also + get deleted + """ + empty_elements = [] + self._is_empty(self.root, empty_elements) + for element, child in empty_elements: + element.remove(child) + @staticmethod def _set_sub_element(element, subelement): """ From 952ad796dd428646de51aebb1b82c36763e7b9a0 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 20:10:11 +0100 Subject: [PATCH 326/509] Remove obsolete code --- resources/lib/downloadutils.py | 11 ----------- resources/lib/userclient.py | 3 --- 2 files changed, 14 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index ff23b7c1..b4536c7e 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -48,16 +48,6 @@ class DownloadUtils(): self.server = server LOG.debug("Set server: %s", server) - def setToken(self, token): - """ - Reserved for userclient only - """ - self.token = token - if token == '': - LOG.debug('Set token: empty token!') - else: - LOG.debug("Set token: xxxxxxx") - def setSSL(self, verifySSL=None, certificate=None): """ Reserved for userclient only @@ -94,7 +84,6 @@ class DownloadUtils(): # Set other stuff self.setServer(window('pms_server')) - self.setToken(window('pms_token')) # Counters to declare PMS dead or unauthorized # Use window variables because start of movies will be called with a diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 460220d7..e5438397 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -276,9 +276,6 @@ class UserClient(Thread): settings('userid', value='') settings('accessToken', value='') - # Reset token in downloads - self.doUtils.setToken('') - self.currToken = None self.auth = True self.currUser = None From feb91127cdb0ede07b92381b3e3bab6b0f851fa0 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 20:11:32 +0100 Subject: [PATCH 327/509] Clear transient token, just in case --- resources/lib/userclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index e5438397..47fe4e3f 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -263,6 +263,7 @@ class UserClient(Thread): state.AUTHENTICATED = False window('pms_token', clear=True) state.PLEX_TOKEN = None + state.PLEX_TRANSIENT_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) From 66b8559eab4904dc651019703750545a5b989d21 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 20:38:50 +0100 Subject: [PATCH 328/509] Fix using plex instead of user token - Fixes #407 --- resources/lib/plexbmchelper/subscribers.py | 4 ++-- resources/lib/state.py | 3 +++ resources/lib/userclient.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index aa3bb8ab..2db7e765 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -388,8 +388,8 @@ class SubscriptionMgr(object): xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN elif playqueue.plex_transient_token: xargs['X-Plex-Token'] = playqueue.plex_transient_token - elif state.PLEX_TOKEN: - xargs['X-Plex-Token'] = state.PLEX_TOKEN + elif state.PMS_TOKEN: + xargs['X-Plex-Token'] = state.PMS_TOKEN url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) diff --git a/resources/lib/state.py b/resources/lib/state.py index f982b67c..31085897 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -75,6 +75,9 @@ AUTHENTICATED = False PLEX_USERNAME = None # Token for that user for plex.tv PLEX_TOKEN = None +# Plex token for the active PMS for the active user +# (might be diffent to PLEX_TOKEN) +PMS_TOKEN = None # Plex ID of that user (e.g. for plex.tv) as a STRING PLEX_USER_ID = None # Token passed along, e.g. if playback initiated by Plex Companion. Might be diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 47fe4e3f..8aaec05f 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -141,7 +141,8 @@ class UserClient(Thread): state.PLEX_USER_ID = userId or None state.PLEX_USERNAME = username # This is the token for the current PMS (might also be '') - window('pms_token', value=self.currToken) + window('pms_token', value=usertoken) + state.PMS_TOKEN = usertoken # This is the token for plex.tv for the current user # Is only '' if user is not signed in to plex.tv window('plex_token', value=settings('plexToken')) @@ -264,6 +265,7 @@ class UserClient(Thread): window('pms_token', clear=True) state.PLEX_TOKEN = None state.PLEX_TRANSIENT_TOKEN = None + state.PMS_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) From 2e7e7fef609be2019d48c492302a49a58f1fc29a Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 21:02:04 +0100 Subject: [PATCH 329/509] Fallback if we didn't get any info on playing element --- resources/lib/playlist_func.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index de480620..ba0457bf 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -367,6 +367,11 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ LOG.debug('Initializing the playlist on the Plex side: %s', playlist) playlist.clear(kodi=False) + if plex_id is None and kodi_item.get('id') is None: + LOG.debug('We dont know plex nor Kodi id, starting lookup') + for playerid in js.get_player_ids(): + json_item = js.get_item(playerid) + kodi_item.update(json_item) try: if plex_id: item = playlist_item_from_plex(plex_id) From d46b7b0225c3e1a7ff1eda2b92bd9207897eb7fe Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Feb 2018 21:07:04 +0100 Subject: [PATCH 330/509] Version bump --- README.md | 2 +- addon.xml | 13 +++++++++++-- changelog.txt | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a14c3c9..2d55e6b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.2-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.3-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 79f9f9a6..ebcf682d 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -59,7 +59,16 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.2 (beta only): + version 2.0.3 (beta only): +- Fix Alexa playback +- Fix Kodi boot loop +- Fix playback being reported to the wrong Plex user +- Fix GB/BBFC content ratings +- Fix KeyError when browsing On Deck +- Make sure that empty XML elements get deleted +- Code optimizations + +version 2.0.2 (beta only): - Fix playback reporting not starting up correctly - Fix playback cleanup if PKC causes stop - Always detect if user resumes playback diff --git a/changelog.txt b/changelog.txt index 721c483b..0b108210 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +version 2.0.3 (beta only): +- Fix Alexa playback +- Fix Kodi boot loop +- Fix playback being reported to the wrong Plex user +- Fix GB/BBFC content ratings +- Fix KeyError when browsing On Deck +- Make sure that empty XML elements get deleted +- Code optimizations + version 2.0.2 (beta only): - Fix playback reporting not starting up correctly - Fix playback cleanup if PKC causes stop From be0eb19794a00e577965fe00520ee3ecad513a8d Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 07:41:49 +0100 Subject: [PATCH 331/509] Revert "Fallback if we didn't get any info on playing element" This reverts commit 2e7e7fef609be2019d48c492302a49a58f1fc29a. --- resources/lib/playlist_func.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index ba0457bf..de480620 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -367,11 +367,6 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ LOG.debug('Initializing the playlist on the Plex side: %s', playlist) playlist.clear(kodi=False) - if plex_id is None and kodi_item.get('id') is None: - LOG.debug('We dont know plex nor Kodi id, starting lookup') - for playerid in js.get_player_ids(): - json_item = js.get_item(playerid) - kodi_item.update(json_item) try: if plex_id: item = playlist_item_from_plex(plex_id) From 55a64d56b1e732c97be2768bd195b0997e9f78d0 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 08:09:57 +0100 Subject: [PATCH 332/509] Add resiliance when adding items to Plex playqueue --- resources/lib/kodidb_functions.py | 4 ++-- resources/lib/playlist_func.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 5c263a30..439fbab6 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1585,11 +1585,11 @@ def kodiid_from_filename(path, kodi_type): try: kodi_id, _ = kodi_db.music_id_from_filename(filename, path) except TypeError: - log.info('No Kodi audio db element found for path %s', path) + log.debug('No Kodi audio db element found for path %s', path) else: with GetKodiDB('video') as kodi_db: try: kodi_id, _ = kodi_db.video_id_from_filename(filename, path) except TypeError: - log.info('No kodi video db element found for path %s', path) + log.debug('No kodi video db element found for path %s', path) return kodi_id diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index de480620..607fedae 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -11,6 +11,7 @@ from downloadutils import DownloadUtils as DU from utils import try_encode, escape_html from PlexAPI import API from PlexFunctions import GetPlexMetadata +from kodidb_functions import kodiid_from_filename import json_rpc as js import variables as v @@ -248,6 +249,42 @@ def playlist_item_from_kodi(kodi_item): return item +def verify_kodi_item(plex_id, kodi_item): + """ + Tries to lookup kodi_id and kodi_type for kodi_item (with kodi_item['file'] + supplied) - if and only if plex_id is None. + + Returns the kodi_item with kodi_item['id'] and kodi_item['type'] possibly + set to None if unsuccessful. + + Will raise a PlaylistError if plex_id is None and kodi_item['file'] starts + with either 'plugin' or 'http' + """ + if plex_id is not None or kodi_item.get('id') is not None: + # Got all the info we need + return kodi_item + # Need more info since we don't have kodi_id nor type. Use file path. + if (kodi_item['file'].startswith('plugin') or + kodi_item['file'].startswith('http')): + raise PlaylistError('Cannot start our plex playlist, aborting') + LOG.debug('Starting research for Kodi id since we didnt get one: %s', + kodi_item) + kodi_id = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_MOVIE) + kodi_item['type'] = v.KODI_TYPE_MOVIE + if kodi_id is None: + kodi_id = kodiid_from_filename(kodi_item['file'], + v.KODI_TYPE_EPISODE) + kodi_item['type'] = v.KODI_TYPE_EPISODE + if kodi_id is None: + kodi_id = kodiid_from_filename(kodi_item['file'], + v.KODI_TYPE_SONG) + kodi_item['type'] = v.KODI_TYPE_SONG + kodi_item['id'] = kodi_id + kodi_item['type'] = None if kodi_id is None else kodi_item['type'] + LOG.debug('Research results for kodi_item: %s', kodi_item) + return kodi_item + + def playlist_item_from_plex(plex_id): """ Returns a playlist element providing the plex_id ("ratingKey") @@ -367,6 +404,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): """ LOG.debug('Initializing the playlist on the Plex side: %s', playlist) playlist.clear(kodi=False) + verify_kodi_item(plex_id, kodi_item) try: if plex_id: item = playlist_item_from_plex(plex_id) @@ -459,6 +497,7 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): Returns the PKC PlayList item or raises PlaylistError """ + verify_kodi_item(plex_id, kodi_item) if plex_id: item = playlist_item_from_plex(plex_id) else: From 9b654f034c13d0e8ad133dc72c63259c7886599f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 08:15:50 +0100 Subject: [PATCH 333/509] Fix Exception text --- 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 607fedae..76f1d23b 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -266,7 +266,7 @@ def verify_kodi_item(plex_id, kodi_item): # Need more info since we don't have kodi_id nor type. Use file path. if (kodi_item['file'].startswith('plugin') or kodi_item['file'].startswith('http')): - raise PlaylistError('Cannot start our plex playlist, aborting') + raise PlaylistError('kodi_item cannot be used for Plex playback') LOG.debug('Starting research for Kodi id since we didnt get one: %s', kodi_item) kodi_id = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_MOVIE) From aa756e60bc0258ca88f08d304331177d13219d8a Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 08:22:37 +0100 Subject: [PATCH 334/509] Ensure that we have unicode paths for database lookup --- resources/lib/kodidb_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 439fbab6..a05ed406 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -5,7 +5,7 @@ from logging import getLogger from ntpath import dirname import artwork -from utils import kodi_sql +from utils import kodi_sql, tryDecode import variables as v ############################################################################### @@ -1574,6 +1574,7 @@ def kodiid_from_filename(path, kodi_type): Returns None if not possible """ kodi_id = None + path = tryDecode(path) try: filename = path.rsplit('/', 1)[1] path = path.rsplit('/', 1)[0] + '/' From c55b687495785b41892592fb4481266954f83ddb Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 16:45:34 +0100 Subject: [PATCH 335/509] Catch PlaylistError in PlayqueueMonitor --- resources/lib/playqueue.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 46b04af2..8d5756c0 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -184,15 +184,20 @@ class PlayqueueMonitor(Thread): else: LOG.debug('Detected new Kodi element at position %s: %s ', i, new_item) - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, - kodi_item=new_item) + try: + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, + kodi_item=new_item) + else: + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + except PL.PlaylistError: + # Could not add the element + pass else: - PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=new_item) - for j in range(i, len(index)): - index[j] += 1 + for j in range(i, len(index)): + index[j] += 1 for i in reversed(index): if self.stopped(): # Chances are that we got an empty Kodi playlist due to From fc1d77eff23721ea0892126a2fdf8d7eb5ca3975 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 16:52:25 +0100 Subject: [PATCH 336/509] Fix ImportError --- resources/lib/kodidb_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index a05ed406..2d7cc34e 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -5,7 +5,7 @@ from logging import getLogger from ntpath import dirname import artwork -from utils import kodi_sql, tryDecode +from utils import kodi_sql, try_decode import variables as v ############################################################################### @@ -1574,7 +1574,7 @@ def kodiid_from_filename(path, kodi_type): Returns None if not possible """ kodi_id = None - path = tryDecode(path) + path = try_decode(path) try: filename = path.rsplit('/', 1)[1] path = path.rsplit('/', 1)[0] + '/' From 7d61f153c346ed8f664f517eb38cbdebdd20ff1e Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 16:59:12 +0100 Subject: [PATCH 337/509] Increase logging --- resources/lib/itemtypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 54c07496..46ffef1c 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -158,6 +158,8 @@ class Items(object): """ # If the playback was stopped, check whether we need to increment the # playcount. PMS won't tell us the playcount via websockets + LOG.debug('Set playstate for file_id %s: viewcount: %s, resume: %s', + file_id, view_count, resume) if mark_played: LOG.info('Marking as completely watched in Kodi') try: From a2d0f98c9bdf4ddd61533906765670afba99eab9 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 17:19:12 +0100 Subject: [PATCH 338/509] Fix ignoring Companion updates for the playing item --- resources/lib/librarysync.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index b7004df2..6d4dc344 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1290,11 +1290,14 @@ class LibrarySync(Thread): if status == 'buffering': # Drop buffering messages immediately continue - plex_id = str(item['ratingKey']) + plex_id = item['ratingKey'] + skip = False for pid in (0, 1, 2): if plex_id == state.PLAYER_STATES[pid]['plex_id']: # Kodi is playing this item - no need to set the playstate - continue + skip = True + if skip: + continue sessionKey = item['sessionKey'] # Do we already have a sessionKey stored? if sessionKey not in self.sessionKeys: From 74bed60c32577b5c296e372740c0ea2b674d195e Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 17:36:28 +0100 Subject: [PATCH 339/509] Don't mess with Kodi's screensaver settings --- resources/lib/librarysync.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 6d4dc344..0bffaa22 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -244,13 +244,10 @@ class LibrarySync(Thread): def _fullSync(self): xbmc.executebuiltin('InhibitIdleShutdown(true)') - screensaver = js.get_setting('screensaver.mode') - js.set_setting('screensaver.mode', '') if self.new_items_only is True: # Set views. Abort if unsuccessful if not self.maintainViews(): xbmc.executebuiltin('InhibitIdleShutdown(false)') - js.set_setting('screensaver.mode', screensaver) return False process = { @@ -266,7 +263,6 @@ class LibrarySync(Thread): self.suspended() or not process[itemtype]()): xbmc.executebuiltin('InhibitIdleShutdown(false)') - js.set_setting('screensaver.mode', screensaver) return False # Let kodi update the views in any case, since we're doing a full sync @@ -276,7 +272,6 @@ class LibrarySync(Thread): window('plex_initialScan', clear=True) xbmc.executebuiltin('InhibitIdleShutdown(false)') - js.set_setting('screensaver.mode', screensaver) if window('plex_scancrashed') == 'true': # Show warning if itemtypes.py crashed at some point dialog('ok', heading='{plex}', line1=lang(39408)) @@ -295,7 +290,6 @@ class LibrarySync(Thread): except Exception as e: # Empty movies, tv shows? log.error('Path hack failed with error message: %s' % str(e)) - js.set_setting('screensaver.mode', screensaver) return True def processView(self, folderItem, kodi_db, plex_db, totalnodes): From e321559121df71957ebb00015b2525fb7296dbb6 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 17:39:31 +0100 Subject: [PATCH 340/509] Do not inhibit idle shutdown --- resources/lib/librarysync.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 0bffaa22..08d2d790 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -243,11 +243,9 @@ class LibrarySync(Thread): return True def _fullSync(self): - xbmc.executebuiltin('InhibitIdleShutdown(true)') if self.new_items_only is True: # Set views. Abort if unsuccessful if not self.maintainViews(): - xbmc.executebuiltin('InhibitIdleShutdown(false)') return False process = { @@ -262,7 +260,6 @@ class LibrarySync(Thread): if (self.stopped() or self.suspended() or not process[itemtype]()): - xbmc.executebuiltin('InhibitIdleShutdown(false)') return False # Let kodi update the views in any case, since we're doing a full sync @@ -271,7 +268,6 @@ class LibrarySync(Thread): xbmc.executebuiltin('UpdateLibrary(music)') window('plex_initialScan', clear=True) - xbmc.executebuiltin('InhibitIdleShutdown(false)') if window('plex_scancrashed') == 'true': # Show warning if itemtypes.py crashed at some point dialog('ok', heading='{plex}', line1=lang(39408)) From 9f8c9a1636656b60fbbbada5959bf15e401c7550 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 17:44:58 +0100 Subject: [PATCH 341/509] Fix KeyError for server discovery - Fixes #409 --- resources/lib/PlexFunctions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 1cbebb88..b54279f9 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -444,7 +444,7 @@ def _poke_pms(pms, queue): if xml.get('machineIdentifier') == pms['machineIdentifier']: # process later pms['baseURL'] = url - pms['protocol'] = protocol + pms['scheme'] = protocol pms['ip'] = address pms['port'] = port queue.put(pms) From 9e2ff58bc745853a515835fe8a574570808c69a3 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 17:47:45 +0100 Subject: [PATCH 342/509] Inhibit idle shutdown only during initial sync --- resources/lib/librarysync.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 08d2d790..61128f6e 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1575,7 +1575,9 @@ class LibrarySync(Thread): delete_playlists() delete_nodes() log.info("Initial start-up full sync starting") + xbmc.executebuiltin('InhibitIdleShutdown(true)') librarySync = fullSync() + xbmc.executebuiltin('InhibitIdleShutdown(false)') window('plex_dbScan', clear=True) state.DB_SCAN = False if librarySync: From 4df5851bc0be68453568db57377de4d015f007f2 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 19:47:01 +0100 Subject: [PATCH 343/509] Optimize context menu --- contextmenu.py | 55 ++++++++++++++----------------- resources/lib/command_pipeline.py | 5 +-- resources/lib/context_entry.py | 23 ++----------- resources/lib/playback_starter.py | 8 ++--- 4 files changed, 34 insertions(+), 57 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index 138ad83a..c763e586 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -1,41 +1,36 @@ # -*- coding: utf-8 -*- - ############################################################################### -from os import path as os_path -from sys import path as sys_path +from sys import listitem +from urllib import urlencode -from xbmcaddon import Addon -from xbmc import translatePath, sleep, log, LOGERROR +from xbmc import getCondVisibility, sleep from xbmcgui import Window -_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 = translatePath(os_path.join( - _ADDON_PATH, - 'resources', - 'lib')).decode('utf-8') -except TypeError: - _BASE_RESOURCE = translatePath(os_path.join( - _ADDON_PATH, - 'resources', - 'lib')).decode() -sys_path.append(_BASE_RESOURCE) - -from pickler import unpickle_me, pickl_window - ############################################################################### + +def _get_kodi_type(): + kodi_type = listitem.getVideoInfoTag().getMediaType().decode('utf-8') + if not kodi_type: + if getCondVisibility('Container.Content(albums)'): + kodi_type = "album" + elif getCondVisibility('Container.Content(artists)'): + kodi_type = "artist" + elif getCondVisibility('Container.Content(songs)'): + kodi_type = "song" + elif getCondVisibility('Container.Content(pictures)'): + kodi_type = "picture" + return kodi_type + + if __name__ == "__main__": WINDOW = Window(10000) + KODI_ID = listitem.getVideoInfoTag().getDbId() + KODI_TYPE = _get_kodi_type() + ARGS = { + 'kodi_id': KODI_ID, + 'kodi_type': KODI_TYPE + } while WINDOW.getProperty('plex_command'): sleep(20) - WINDOW.setProperty('plex_command', 'CONTEXT_menu') - while not pickl_window('plex_result'): - sleep(50) - RESULT = unpickle_me() - if RESULT is None: - log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR) + WINDOW.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(ARGS)) diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 0702313f..64bda2e4 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -54,8 +54,9 @@ class Monitor_Window(Thread): value.replace('PLEX_USERNAME-', '') or None elif value.startswith('RUN_LIB_SCAN-'): state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '') - elif value == 'CONTEXT_menu': - queue.put('dummy?mode=context_menu') + elif value.startswith('CONTEXT_menu?'): + queue.put('dummy?mode=context_menu&%s' + % value.replace('CONTEXT_menu?', '')) else: raise NotImplementedError('%s not implemented' % value) else: diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 0b1a7e25..96ea0777 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -38,12 +38,12 @@ class ContextMenu(object): """ _selected_option = None - def __init__(self): + def __init__(self, kodi_id=None, kodi_type=None): """ Simply instantiate with ContextMenu() - no need to call any methods """ - self.kodi_id = getInfoLabel('ListItem.DBID').decode('utf-8') - self.kodi_type = self._get_kodi_type() + self.kodi_id = kodi_id + self.kodi_type = kodi_type self.plex_id = self._get_plex_id(self.kodi_id, self.kodi_type) if self.kodi_type: self.plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[self.kodi_type] @@ -61,23 +61,6 @@ class ContextMenu(object): sleep(500) executebuiltin('Container.Refresh') - @staticmethod - def _get_kodi_type(): - kodi_type = getInfoLabel('ListItem.DBTYPE').decode('utf-8') - if not kodi_type: - if getCondVisibility('Container.Content(albums)'): - kodi_type = v.KODI_TYPE_ALBUM - elif getCondVisibility('Container.Content(artists)'): - kodi_type = v.KODI_TYPE_ARTIST - elif getCondVisibility('Container.Content(songs)'): - kodi_type = v.KODI_TYPE_SONG - elif getCondVisibility('Container.Content(pictures)'): - kodi_type = v.KODI_TYPE_PHOTO - else: - LOG.info("kodi_type is unknown") - kodi_type = None - return kodi_type - @staticmethod def _get_plex_id(kodi_id, kodi_type): plex_id = getInfoLabel('ListItem.Property(plexid)') or None diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index dcc7a59f..ccdca94e 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -32,11 +32,9 @@ class Playback_Starter(Thread): elif mode == 'plex_node': playback.process_indirect(params['key'], params['offset']) elif mode == 'context_menu': - ContextMenu() - result = Playback_Successful() - # Let default.py know! - pickle_me(result) - + ContextMenu(kodi_id=params['kodi_id'], + kodi_type=params['kodi_type']) + def run(self): queue = state.COMMAND_PIPELINE_QUEUE LOG.info("----===## Starting Playback_Starter ##===----") From 7ce157accd969691e9feffddd48edae235534f31 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Feb 2018 20:15:53 +0100 Subject: [PATCH 344/509] Wipe all resume points before resyncing them --- resources/lib/kodidb_functions.py | 6 ++++++ resources/lib/librarysync.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 2d7cc34e..6d53b142 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -970,6 +970,12 @@ class Kodidb_Functions(): resume = None return resume + def delete_all_playstates(self): + """ + Entirely resets the table bookmark and thus all resume points + """ + self.cursor.execute("DELETE FROM bookmark") + def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): # Delete existing resume point query = ' '.join(( diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 61128f6e..d9c37a6f 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -247,6 +247,9 @@ class LibrarySync(Thread): # Set views. Abort if unsuccessful if not self.maintainViews(): return False + # Delete all existing resume points first + with kodidb.GetKodiDB('video') as kodi_db: + kodi_db.delete_all_playstates() process = { 'movies': self.PlexMovies, From 121e8e02433540ffe7020d9ad6a539eb9167ffe7 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 16 Feb 2018 17:25:17 +0100 Subject: [PATCH 345/509] Hack for repeatedly starting same video using Addon Paths --- resources/lib/kodimonitor.py | 1 + resources/lib/playqueue.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index daea2468..7cf72e64 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -217,6 +217,7 @@ class KodiMonitor(Monitor): # Hack we need for RESUMABLE items because Kodi lost the path of the # last played item that is now being replayed (see playback.py's # Player().play()) + # Also see playqueue.py _compare_playqueues() LOG.info('Detected re-start of playback of last item') old = state.OLD_PLAYER_STATES[data['playlistid']] kwargs = { diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 8d5756c0..84cee9a0 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -195,6 +195,13 @@ class PlayqueueMonitor(Thread): except PL.PlaylistError: # Could not add the element pass + except IndexError: + # This is really a hack - happens when using Addon Paths + # and repeatedly starting the same element. Kodi will then + # not pass kodi id nor file path AND will also not + # start-up playback. Hence kodimonitor kicks off playback. + # Also see kodimonitor.py - _playlist_onadd() + pass else: for j in range(i, len(index)): index[j] += 1 From ab1f28bb8830364f7de1aa12f139ed7ed59159b5 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 16 Feb 2018 18:23:55 +0100 Subject: [PATCH 346/509] Fix TypeError when DB yet empty --- resources/lib/librarysync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index d9c37a6f..2797373b 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1525,7 +1525,7 @@ class LibrarySync(Thread): currentVersion = settings('dbCreatedWithVersion') if not compare_version(currentVersion, v.MIN_DB_VERSION): log.warn("Db version out of date: %s minimum version " - "required: %s", (currentVersion, v.MIN_DB_VERSION)) + "required: %s", currentVersion, v.MIN_DB_VERSION) # DB out of date. Proceed to recreate? resp = dialog('yesno', heading=lang(29999), From 8cd9deef40c5b48442d85648b44b7ebd3e3cbbda Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 17 Feb 2018 13:42:08 +0100 Subject: [PATCH 347/509] Attempt to fix Kodi overwriting paths in Kodi DB --- resources/lib/playback.py | 21 ++++++++++++++++++--- resources/lib/playback_starter.py | 2 +- resources/lib/playlist_func.py | 3 +++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 4619bcf5..b29e4364 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -36,6 +36,9 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): Hit this function for addon path playback, Plex trailers, etc. Will setup playback first, then on second call complete playback. + path: either the complete plugin://plugin.video.plexkodiconnect path + OR just the query '?plex_id=458160&mode=play&plex_type=movie' + Will set Playback_Successful() with potentially a PKC_ListItem() attached (to be consumed by setResolvedURL in default.py) @@ -65,7 +68,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): LOG.debug('playQueue position: %s for %s', pos, playqueue) # Have we already initiated playback? try: - playqueue.items[pos] + item = playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance if resolve is True: @@ -75,8 +78,20 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): pickle_me(result) playback_init(plex_id, plex_type, playqueue) else: - # kick off playback on second pass - conclude_playback(playqueue, pos) + if item.playback_init is False: + # Hack: we need to use setResolvedUrl twice. Otherwise, Kodi + # overwrites the path in the Kodi database (addon-path) with the + # result of the first setResolvedUrl + item.playback_init = True + if not path.startswith('plugin://'): + path = 'plugin://%s%s' % (v.ADDON_ID, path) + LOG.debug('Initializing playback for one item using path %s', path) + result = Playback_Successful() + result.listitem = PKC_ListItem(path=path) + pickle_me(result) + else: + # kick off playback on second pass + conclude_playback(playqueue, pos) def playback_init(plex_id, plex_type, playqueue): diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index ccdca94e..d9a6414d 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -28,7 +28,7 @@ class Playback_Starter(Thread): if mode == 'play': playback.playback_triage(plex_id=params.get('plex_id'), plex_type=params.get('plex_type'), - path=params.get('path')) + path=item) elif mode == 'plex_node': playback.process_indirect(params['key'], params['offset']) elif mode == 'context_menu': diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 76f1d23b..07ed82ba 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -138,6 +138,8 @@ class Playlist_Item(object): offset = None [int] the item's view offset UPON START in Plex time part = 0 [int] part number if Plex video consists of mult. parts force_transcode [bool] defaults to False + playback_init [bool] Hack to use setResolvedUrl twice (and thus only + "correctly") if playback_init is set to True """ def __init__(self): self.id = None @@ -156,6 +158,7 @@ class Playlist_Item(object): # If Plex video consists of several parts; part number self.part = 0 self.force_transcode = False + self.playback_init = False def __repr__(self): """ From a1eb926dc3692d6c26afd06b60b35711d6160718 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 17 Feb 2018 13:48:57 +0100 Subject: [PATCH 348/509] Revert "Attempt to fix Kodi overwriting paths in Kodi DB" This reverts commit 8cd9deef40c5b48442d85648b44b7ebd3e3cbbda. --- resources/lib/playback.py | 21 +++------------------ resources/lib/playback_starter.py | 2 +- resources/lib/playlist_func.py | 3 --- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index b29e4364..4619bcf5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -36,9 +36,6 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): Hit this function for addon path playback, Plex trailers, etc. Will setup playback first, then on second call complete playback. - path: either the complete plugin://plugin.video.plexkodiconnect path - OR just the query '?plex_id=458160&mode=play&plex_type=movie' - Will set Playback_Successful() with potentially a PKC_ListItem() attached (to be consumed by setResolvedURL in default.py) @@ -68,7 +65,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): LOG.debug('playQueue position: %s for %s', pos, playqueue) # Have we already initiated playback? try: - item = playqueue.items[pos] + playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance if resolve is True: @@ -78,20 +75,8 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): pickle_me(result) playback_init(plex_id, plex_type, playqueue) else: - if item.playback_init is False: - # Hack: we need to use setResolvedUrl twice. Otherwise, Kodi - # overwrites the path in the Kodi database (addon-path) with the - # result of the first setResolvedUrl - item.playback_init = True - if not path.startswith('plugin://'): - path = 'plugin://%s%s' % (v.ADDON_ID, path) - LOG.debug('Initializing playback for one item using path %s', path) - result = Playback_Successful() - result.listitem = PKC_ListItem(path=path) - pickle_me(result) - else: - # kick off playback on second pass - conclude_playback(playqueue, pos) + # kick off playback on second pass + conclude_playback(playqueue, pos) def playback_init(plex_id, plex_type, playqueue): diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index d9a6414d..ccdca94e 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -28,7 +28,7 @@ class Playback_Starter(Thread): if mode == 'play': playback.playback_triage(plex_id=params.get('plex_id'), plex_type=params.get('plex_type'), - path=item) + path=params.get('path')) elif mode == 'plex_node': playback.process_indirect(params['key'], params['offset']) elif mode == 'context_menu': diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 07ed82ba..76f1d23b 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -138,8 +138,6 @@ class Playlist_Item(object): offset = None [int] the item's view offset UPON START in Plex time part = 0 [int] part number if Plex video consists of mult. parts force_transcode [bool] defaults to False - playback_init [bool] Hack to use setResolvedUrl twice (and thus only - "correctly") if playback_init is set to True """ def __init__(self): self.id = None @@ -158,7 +156,6 @@ class Playlist_Item(object): # If Plex video consists of several parts; part number self.part = 0 self.force_transcode = False - self.playback_init = False def __repr__(self): """ From fe6ccad95942dd71b77e80b27522f2061ee44fae Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 17 Feb 2018 14:06:01 +0100 Subject: [PATCH 349/509] Leave 'movies' in addon path in Kodi DB --- resources/lib/itemtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 46ffef1c..ba8c251f 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -259,7 +259,7 @@ class Movies(Items): trailer = api.trailer_id() if trailer: - trailer = ('plugin://%s?plex_id=%s&plex_type=%s&mode=play' + trailer = ('plugin://%s/movies/?plex_id=%s&plex_type=%s&mode=play' % (v.ADDON_ID, trailer, v.PLEX_TYPE_CLIP)) # GET THE FILE AND PATH ##### @@ -283,7 +283,7 @@ class Movies(Items): path = playurl.replace(filename, "") if do_indirect: # Set plugin path and media flags using real filename - path = 'plugin://%s' % v.ADDON_ID + path = 'plugin://%s/movies/' % v.ADDON_ID params = { 'mode': 'play', 'plex_id': itemid, From 0173129ffc56a75541841749eb440472ea336781 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 20 Feb 2018 10:19:11 +0100 Subject: [PATCH 350/509] Correctly set-up paths table --- resources/lib/kodidb_functions.py | 74 ++++++++++++++++++++++++++----- resources/lib/librarysync.py | 12 ++--- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 6d53b142..84abc884 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -44,7 +44,7 @@ class Kodidb_Functions(): self.cursor = cursor self.artwork = artwork.Artwork() - def pathHack(self): + def setup_path_table(self): """ Use with Kodi video DB @@ -53,15 +53,69 @@ class Kodidb_Functions(): For some reason, Kodi ignores this if done via itemtypes while e.g. adding or updating items. (addPath method does NOT work) """ - query = ' '.join(( - "UPDATE path", - "SET strContent = ?, strScraper = ?", - "WHERE strPath LIKE ?" - )) - self.cursor.execute( - query, ('movies', - 'metadata.local', - 'plugin://plugin.video.plexkodiconnect/movies%%')) + root_path_id = self.getPath('plugin://%s/' % v.ADDON_ID) + if root_path_id is not None: + return + # add the very root plugin://plugin.video.plexkodiconnect to paths + self.cursor.execute("select coalesce(max(idPath),0) from path") + root_path_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, + strPath, + useFolderNames, + noUpdate, + exclude) + VALUES (?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (root_path_id, + 'plugin://%s/' % v.ADDON_ID, + False, + True, + True)) + # Now add the root folders for movies + self.cursor.execute("select coalesce(max(idPath),0) from path") + path_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, + strPath, + strContent, + strScraper, + useFolderNames, + noUpdate, + exclude, + idParentPath) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (path_id, + 'plugin://%s/movies/' % v.ADDON_ID, + 'movies', + 'metadata.local', + False, + True, + True, + root_path_id)) + # And TV shows + self.cursor.execute("select coalesce(max(idPath),0) from path") + path_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, + strPath, + strContent, + strScraper, + useFolderNames, + noUpdate, + exclude, + idParentPath) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (path_id, + 'plugin://%s/tvshows/' % v.ADDON_ID, + 'tvshows', + 'metadata.local', + False, + True, + True, + root_path_id)) def getParentPathId(self, path): """ diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 2797373b..f012e59b 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -249,6 +249,9 @@ class LibrarySync(Thread): return False # Delete all existing resume points first with kodidb.GetKodiDB('video') as kodi_db: + # Setup the paths for addon-paths (even when using direct paths) + kodi_db.setup_path_table() + # Delete all resume points because we'll get new ones kodi_db.delete_all_playstates() process = { @@ -280,15 +283,6 @@ class LibrarySync(Thread): if state.PMS_STATUS not in ('401', 'Auth'): # Plex server had too much and returned ERROR dialog('ok', heading='{plex}', line1=lang(39409)) - - # Path hack, so Kodis Information screen works - with kodidb.GetKodiDB('video') as kodi_db: - try: - kodi_db.pathHack() - log.info('Path hack successful') - except Exception as e: - # Empty movies, tv shows? - log.error('Path hack failed with error message: %s' % str(e)) return True def processView(self, folderItem, kodi_db, plex_db, totalnodes): From aac22c33693336fa8af82d41b9209bed7433c72a Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 20 Feb 2018 18:02:34 +0100 Subject: [PATCH 351/509] Monitor for DB file path change --- resources/lib/kodidb_functions.py | 14 ++++++++++++++ resources/lib/playqueue.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 84abc884..840774b0 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -44,6 +44,20 @@ class Kodidb_Functions(): self.cursor = cursor self.artwork = artwork.Artwork() + def check_path(self): + query = ' '.join(( + "SELECT idPath", + "FROM path", + "WHERE strPath = ?" + )) + self.cursor.execute(query, ('smb://TOMSNAS/PlexMovies/',)) + try: + pathid = self.cursor.fetchone()[0] + except TypeError: + pathid = None + + return pathid + def setup_path_table(self): """ Use with Kodi video DB diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 84cee9a0..6eea9de8 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -15,6 +15,7 @@ from plexbmchelper.subscribers import LOCK from playback import play_xml import json_rpc as js import variables as v +import kodidb_functions as kodidb ############################################################################### LOG = getLogger("PLEX." + __name__) @@ -218,6 +219,7 @@ class PlayqueueMonitor(Thread): stopped = self.stopped suspended = self.suspended LOG.info("----===## Starting PlayqueueMonitor ##===----") + tested = False while not stopped(): while suspended(): if stopped(): @@ -230,5 +232,10 @@ class PlayqueueMonitor(Thread): # compare old and new playqueue self._compare_playqueues(playqueue, kodi_pl) playqueue.old_kodi_pl = list(kodi_pl) - sleep(200) + with kodidb.GetKodiDB('video') as kodi_db: + # Setup the paths for addon-paths (even when using direct paths) + if kodi_db.check_path() and not tested: + tested = True + LOG.error('NOW!') + sleep(50) LOG.info("----===## PlayqueueMonitor stopped ##===----") From bba42bb1bb148776a2c28c0d1dc1541d8d62bdd0 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 20 Feb 2018 19:43:12 +0100 Subject: [PATCH 352/509] Fail setResolvedUrl on 1st run - Using add-on paths in the Kodi library, we need to make sure that the subsequent call of xbmc.setresolvedUrl fails or is forwarded once again --- resources/lib/playqueue.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 6eea9de8..3d71cfd6 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -185,14 +185,13 @@ class PlayqueueMonitor(Thread): else: LOG.debug('Detected new Kodi element at position %s: %s ', i, new_item) + if playqueue.id is None: + LOG.debug('Not yet initiating playback') + return try: - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, - kodi_item=new_item) - else: - PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=new_item) + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) except PL.PlaylistError: # Could not add the element pass From cc37ffd809d0b3564b0b64a6293aa8520d56a0cc Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 07:59:19 +0100 Subject: [PATCH 353/509] Allow playback init for direct paths and context menu --- resources/lib/playqueue.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 3d71cfd6..f55dd76c 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -15,6 +15,7 @@ from plexbmchelper.subscribers import LOCK from playback import play_xml import json_rpc as js import variables as v +import state import kodidb_functions as kodidb ############################################################################### @@ -185,13 +186,19 @@ class PlayqueueMonitor(Thread): else: LOG.debug('Detected new Kodi element at position %s: %s ', i, new_item) - if playqueue.id is None: + if playqueue.id is None and (not state.DIRECT_PATHS or + state.CONTEXT_MENU_PLAY): + # Only initialize if directly fired up using direct paths. + # Otherwise let default.py do its magic LOG.debug('Not yet initiating playback') return try: - PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=new_item) + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, kodi_item=new_item) + else: + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) except PL.PlaylistError: # Could not add the element pass From 933bd44ad5f4c451370a2ed8e37cbce9b377ae4b Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 08:01:00 +0100 Subject: [PATCH 354/509] Revert "Monitor for DB file path change" This reverts commit aac22c33693336fa8af82d41b9209bed7433c72a. --- resources/lib/kodidb_functions.py | 14 -------------- resources/lib/playqueue.py | 9 +-------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 840774b0..84abc884 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -44,20 +44,6 @@ class Kodidb_Functions(): self.cursor = cursor self.artwork = artwork.Artwork() - def check_path(self): - query = ' '.join(( - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - self.cursor.execute(query, ('smb://TOMSNAS/PlexMovies/',)) - try: - pathid = self.cursor.fetchone()[0] - except TypeError: - pathid = None - - return pathid - def setup_path_table(self): """ Use with Kodi video DB diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index f55dd76c..aec7872e 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -16,7 +16,6 @@ from playback import play_xml import json_rpc as js import variables as v import state -import kodidb_functions as kodidb ############################################################################### LOG = getLogger("PLEX." + __name__) @@ -225,7 +224,6 @@ class PlayqueueMonitor(Thread): stopped = self.stopped suspended = self.suspended LOG.info("----===## Starting PlayqueueMonitor ##===----") - tested = False while not stopped(): while suspended(): if stopped(): @@ -238,10 +236,5 @@ class PlayqueueMonitor(Thread): # compare old and new playqueue self._compare_playqueues(playqueue, kodi_pl) playqueue.old_kodi_pl = list(kodi_pl) - with kodidb.GetKodiDB('video') as kodi_db: - # Setup the paths for addon-paths (even when using direct paths) - if kodi_db.check_path() and not tested: - tested = True - LOG.error('NOW!') - sleep(50) + sleep(200) LOG.info("----===## PlayqueueMonitor stopped ##===----") From 40d670d0023b20aefb1f24f09a68bb1633ecdf14 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 08:03:40 +0100 Subject: [PATCH 355/509] Move check for direct paths and context menu play --- resources/lib/playqueue.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index aec7872e..551d89b0 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -185,12 +185,6 @@ class PlayqueueMonitor(Thread): else: LOG.debug('Detected new Kodi element at position %s: %s ', i, new_item) - if playqueue.id is None and (not state.DIRECT_PATHS or - state.CONTEXT_MENU_PLAY): - # Only initialize if directly fired up using direct paths. - # Otherwise let default.py do its magic - LOG.debug('Not yet initiating playback') - return try: if playqueue.id is None: PL.init_Plex_playlist(playqueue, kodi_item=new_item) @@ -233,8 +227,14 @@ class PlayqueueMonitor(Thread): for playqueue in PLAYQUEUES: kodi_pl = js.playlist_get_items(playqueue.playlistid) if playqueue.old_kodi_pl != kodi_pl: - # compare old and new playqueue - self._compare_playqueues(playqueue, kodi_pl) + if playqueue.id is None and (not state.DIRECT_PATHS or + state.CONTEXT_MENU_PLAY): + # Only initialize if directly fired up using direct + # paths. Otherwise let default.py do its magic + LOG.debug('Not yet initiating playback') + else: + # compare old and new playqueue + self._compare_playqueues(playqueue, kodi_pl) playqueue.old_kodi_pl = list(kodi_pl) sleep(200) LOG.info("----===## PlayqueueMonitor stopped ##===----") From 4be376faac21fae8848f859db56d9897619d93c7 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 08:47:41 +0100 Subject: [PATCH 356/509] Attempt to fix widget playback --- resources/lib/playback.py | 67 +++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 4619bcf5..43f29ac8 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -5,7 +5,7 @@ from logging import getLogger from threading import Thread from urllib import urlencode -from xbmc import Player, sleep +from xbmc import Player, sleep, getCondVisibility from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue @@ -47,8 +47,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): the first pass - e.g. if you're calling this function from the original service.py Python instance """ - LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', - plex_id, plex_type, path) + # Widget playback? Pain in the butt... + widget = getCondVisibility('Window.IsActive(home)') + LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s,' + ' widget playback: %s', plex_id, plex_type, path, widget) if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') if resolve is True: @@ -68,18 +70,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance - if resolve is True: + if resolve is True and not widget: state.PKC_CAUSED_STOP = True result = Playback_Successful() result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) - playback_init(plex_id, plex_type, playqueue) + playback_init(plex_id, plex_type, playqueue, widget) else: # kick off playback on second pass conclude_playback(playqueue, pos) -def playback_init(plex_id, plex_type, playqueue): +def playback_init(plex_id, plex_type, playqueue, widget): """ Playback setup if Kodi starts playing an item for the first time. """ @@ -119,24 +121,47 @@ def playback_init(plex_id, plex_type, playqueue): # Should already be empty, but just in case PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) - # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(200) - _process_stack(playqueue, stack) + if widget: + # Need to use setResolvedUrl when using widgets :-( + # Grab the very first item of our stack + stack_item = stack.pop(0) + # Get the PKC playlist element + item = PL.playlist_item_from_xml(playqueue, + stack_item['xml_video_element'], + stack_item['kodi_id'], + stack_item['kodi_type']) + item.playcount = stack_item['playcount'] + item.offset = stack_item['offset'] + item.part = stack_item['part'] + item.id = stack_item['id'] + item.force_transcode = state.FORCE_TRANSCODE + # Only add our very first item to our playqueue + playqueue.items.append(item) + # Conclude the playback with setResolvedUrl + LOG.debug('Start concluding playback for 1st widget element') + conclude_playback(playqueue, 0) + LOG.debug('Add remaining items to playqueues for widget playback') + _process_stack(playqueue, stack) + else: + # Sleep a bit to let setResolvedUrl do its thing - bit ugly + sleep(200) + _process_stack(playqueue, stack) + # New thread to release this one sooner (e.g. harddisk spinning up) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, )) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + # By design, PKC will start Kodi playback using Player().play(). Kodi + # caches paths like our plugin://pkc. If we use Player().play() between + # 2 consecutive startups of exactly the same Kodi library item, Kodi's + # cache will have been flushed for some reason. Hence the 2nd call for + # plugin://pkc will be lost; Kodi will try to startup playback for an + # empty path: log entry is "CGUIWindowVideoBase::OnPlayMedia " + thread.start() # Reset some playback variables state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False - # New thread to release this one sooner (e.g. harddisk spinning up) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, )) - thread.setDaemon(True) - LOG.info('Done initializing PKC playback, starting Kodi player') - # By design, PKC will start Kodi playback using Player().play(). Kodi - # caches paths like our plugin://pkc. If we use Player().play() between - # 2 consecutive startups of exactly the same Kodi library item, Kodi's - # cache will have been flushed for some reason. Hence the 2nd call for - # plugin://pkc will be lost; Kodi will try to startup playback for an empty - # path: log entry is "CGUIWindowVideoBase::OnPlayMedia " - thread.start() def _prep_playlist_stack(xml): From d4b5dc99a139b325e1b1ca4a4f25528717db99f7 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 08:47:44 +0100 Subject: [PATCH 357/509] Revert "Attempt to fix widget playback" This reverts commit 4be376faac21fae8848f859db56d9897619d93c7. --- resources/lib/playback.py | 67 ++++++++++++--------------------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 43f29ac8..4619bcf5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -5,7 +5,7 @@ from logging import getLogger from threading import Thread from urllib import urlencode -from xbmc import Player, sleep, getCondVisibility +from xbmc import Player, sleep from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue @@ -47,10 +47,8 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): the first pass - e.g. if you're calling this function from the original service.py Python instance """ - # Widget playback? Pain in the butt... - widget = getCondVisibility('Window.IsActive(home)') - LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s,' - ' widget playback: %s', plex_id, plex_type, path, widget) + LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', + plex_id, plex_type, path) if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') if resolve is True: @@ -70,18 +68,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): playqueue.items[pos] except IndexError: # Release our default.py before starting our own Kodi player instance - if resolve is True and not widget: + if resolve is True: state.PKC_CAUSED_STOP = True result = Playback_Successful() result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) - playback_init(plex_id, plex_type, playqueue, widget) + playback_init(plex_id, plex_type, playqueue) else: # kick off playback on second pass conclude_playback(playqueue, pos) -def playback_init(plex_id, plex_type, playqueue, widget): +def playback_init(plex_id, plex_type, playqueue): """ Playback setup if Kodi starts playing an item for the first time. """ @@ -121,47 +119,24 @@ def playback_init(plex_id, plex_type, playqueue, widget): # Should already be empty, but just in case PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) - if widget: - # Need to use setResolvedUrl when using widgets :-( - # Grab the very first item of our stack - stack_item = stack.pop(0) - # Get the PKC playlist element - item = PL.playlist_item_from_xml(playqueue, - stack_item['xml_video_element'], - stack_item['kodi_id'], - stack_item['kodi_type']) - item.playcount = stack_item['playcount'] - item.offset = stack_item['offset'] - item.part = stack_item['part'] - item.id = stack_item['id'] - item.force_transcode = state.FORCE_TRANSCODE - # Only add our very first item to our playqueue - playqueue.items.append(item) - # Conclude the playback with setResolvedUrl - LOG.debug('Start concluding playback for 1st widget element') - conclude_playback(playqueue, 0) - LOG.debug('Add remaining items to playqueues for widget playback') - _process_stack(playqueue, stack) - else: - # Sleep a bit to let setResolvedUrl do its thing - bit ugly - sleep(200) - _process_stack(playqueue, stack) - # New thread to release this one sooner (e.g. harddisk spinning up) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, )) - thread.setDaemon(True) - LOG.info('Done initializing PKC playback, starting Kodi player') - # By design, PKC will start Kodi playback using Player().play(). Kodi - # caches paths like our plugin://pkc. If we use Player().play() between - # 2 consecutive startups of exactly the same Kodi library item, Kodi's - # cache will have been flushed for some reason. Hence the 2nd call for - # plugin://pkc will be lost; Kodi will try to startup playback for an - # empty path: log entry is "CGUIWindowVideoBase::OnPlayMedia " - thread.start() + # Sleep a bit to let setResolvedUrl do its thing - bit ugly + sleep(200) + _process_stack(playqueue, stack) # Reset some playback variables state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False + # New thread to release this one sooner (e.g. harddisk spinning up) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, )) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + # By design, PKC will start Kodi playback using Player().play(). Kodi + # caches paths like our plugin://pkc. If we use Player().play() between + # 2 consecutive startups of exactly the same Kodi library item, Kodi's + # cache will have been flushed for some reason. Hence the 2nd call for + # plugin://pkc will be lost; Kodi will try to startup playback for an empty + # path: log entry is "CGUIWindowVideoBase::OnPlayMedia " + thread.start() def _prep_playlist_stack(xml): From faacbc6108af97c617cf9430a997307400aa9af7 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 20:23:43 +0100 Subject: [PATCH 358/509] New method for grandparent ratingKey --- resources/lib/PlexAPI.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 019c51ef..49346f60 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -542,6 +542,13 @@ class API(object): """ return self.item.get('parentRatingKey') + def grandparent_id(self): + """ + Returns the ratingKey for the corresponding grandparent, e.g. a TV show + for episodes, or None + """ + return self.item.get('grandparentRatingKey') + def episode_data(self): """ Call on a single episode. From be5c1e6b8a40f3ee6411beac3e260a3bb422e0a7 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 21 Feb 2018 20:24:31 +0100 Subject: [PATCH 359/509] Re-introduce dependency add-ons, part 1 We need them in order to keep the Kodi DB straight --- addon.xml | 2 + resources/lib/itemtypes.py | 12 ++-- resources/lib/kodidb_functions.py | 104 ++++++++++++------------------ resources/lib/playback.py | 11 +++- resources/lib/variables.py | 11 +++- 5 files changed, 67 insertions(+), 73 deletions(-) diff --git a/addon.xml b/addon.xml index ebcf682d..6a161cc5 100644 --- a/addon.xml +++ b/addon.xml @@ -3,6 +3,8 @@ + + video audio image diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index ba8c251f..f77e8209 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -259,7 +259,7 @@ class Movies(Items): trailer = api.trailer_id() if trailer: - trailer = ('plugin://%s/movies/?plex_id=%s&plex_type=%s&mode=play' + trailer = ('plugin://%s.movies/?plex_id=%s&plex_type=%s&mode=play' % (v.ADDON_ID, trailer, v.PLEX_TYPE_CLIP)) # GET THE FILE AND PATH ##### @@ -283,7 +283,7 @@ class Movies(Items): path = playurl.replace(filename, "") if do_indirect: # Set plugin path and media flags using real filename - path = 'plugin://%s/movies/' % v.ADDON_ID + path = 'plugin://%s.movies/' % v.ADDON_ID params = { 'mode': 'play', 'plex_id': itemid, @@ -588,7 +588,7 @@ class TVShows(Items): toplevelpath = "%s/" % dirname(dirname(path)) if do_indirect: # Set plugin path - toplevelpath = "plugin://%s/tvshows/" % v.ADDON_ID + toplevelpath = "plugin://%s.tvshows/" % v.ADDON_ID path = "%s%s/" % (toplevelpath, itemid) # Add top path @@ -910,7 +910,7 @@ class TVShows(Items): filename = playurl.rsplit('/', 1)[1] else: filename = 'file_not_found.mkv' - path = 'plugin://%s/tvshows/%s/' % (v.ADDON_ID, series_id) + path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) params = { 'plex_id': itemid, 'plex_type': v.PLEX_TYPE_EPISODE, @@ -918,7 +918,7 @@ class TVShows(Items): } filename = "%s?%s" % (path, urlencode(params)) playurl = filename - parent_path_id = self.kodi_db.addPath('plugin://%s/tvshows/' + parent_path_id = self.kodi_db.addPath('plugin://%s.tvshows/' % v.ADDON_ID) # add/retrieve pathid and fileid @@ -1101,7 +1101,7 @@ class TVShows(Items): if not state.DIRECT_PATHS and resume: # Create additional entry for widgets. This is only required for # plugin/episode. - temppathid = self.kodi_db.getPath('plugin://%s/tvshows/' + temppathid = self.kodi_db.getPath('plugin://%s.tvshows/' % v.ADDON_ID) tempfileid = self.kodi_db.addFile(filename, temppathid) query = ''' diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 84abc884..17d88fd0 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -53,69 +53,49 @@ class Kodidb_Functions(): For some reason, Kodi ignores this if done via itemtypes while e.g. adding or updating items. (addPath method does NOT work) """ - root_path_id = self.getPath('plugin://%s/' % v.ADDON_ID) - if root_path_id is not None: - return - # add the very root plugin://plugin.video.plexkodiconnect to paths - self.cursor.execute("select coalesce(max(idPath),0) from path") - root_path_id = self.cursor.fetchone()[0] + 1 - query = ''' - INSERT INTO path(idPath, - strPath, - useFolderNames, - noUpdate, - exclude) - VALUES (?, ?, ?, ?, ?) - ''' - self.cursor.execute(query, (root_path_id, - 'plugin://%s/' % v.ADDON_ID, - False, - True, - True)) - # Now add the root folders for movies - self.cursor.execute("select coalesce(max(idPath),0) from path") - path_id = self.cursor.fetchone()[0] + 1 - query = ''' - INSERT INTO path(idPath, - strPath, - strContent, - strScraper, - useFolderNames, - noUpdate, - exclude, - idParentPath) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - self.cursor.execute(query, (path_id, - 'plugin://%s/movies/' % v.ADDON_ID, - 'movies', - 'metadata.local', - False, - True, - True, - root_path_id)) + path_id = self.getPath('plugin://%s.movies/' % v.ADDON_ID) + if path_id is None: + self.cursor.execute("select coalesce(max(idPath),0) from path") + path_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, + strPath, + strContent, + strScraper, + useFolderNames, + noUpdate, + exclude) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (path_id, + 'plugin://%s.movies/' % v.ADDON_ID, + 'movies', + 'metadata.local', + 0, + 1, + 1)) # And TV shows - self.cursor.execute("select coalesce(max(idPath),0) from path") - path_id = self.cursor.fetchone()[0] + 1 - query = ''' - INSERT INTO path(idPath, - strPath, - strContent, - strScraper, - useFolderNames, - noUpdate, - exclude, - idParentPath) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - self.cursor.execute(query, (path_id, - 'plugin://%s/tvshows/' % v.ADDON_ID, - 'tvshows', - 'metadata.local', - False, - True, - True, - root_path_id)) + path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID) + if path_id is None: + self.cursor.execute("select coalesce(max(idPath),0) from path") + path_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, + strPath, + strContent, + strScraper, + useFolderNames, + noUpdate, + exclude) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, (path_id, + 'plugin://%s.tvshows/' % v.ADDON_ID, + 'tvshows', + 'metadata.local', + 0, + 1, + 1)) def getParentPathId(self, path): """ diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 4619bcf5..102216d2 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -164,8 +164,15 @@ def _prep_playlist_stack(xml): 'plex_id': api.plex_id(), 'plex_type': api.plex_type() } - path = ('plugin://plugin.video.plexkodiconnect?%s' - % urlencode(params)) + if api.plex_type() == v.PLEX_TYPE_EPISODE: + path = ('plugin://%s/%s/?%s' + % (v.ADDON_TYPE[api.plex_type()], + api.grandparent_id(), + urlencode(params))) + else: + path = ('plugin://%s/?%s' + % (v.ADDON_TYPE[api.plex_type()], + urlencode(params))) listitem = api.create_listitem() listitem.setPath(try_encode(path)) else: diff --git a/resources/lib/variables.py b/resources/lib/variables.py index fef8e4ca..484d245c 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -171,9 +171,6 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo' KODI_TYPE_PHOTO = 'photo' - -# Translation tables - KODI_VIDEOTYPES = ( KODI_TYPE_VIDEO, KODI_TYPE_MOVIE, @@ -189,6 +186,14 @@ KODI_AUDIOTYPES = ( KODI_TYPE_ARTIST, ) +# Translation tables + +ADDON_TYPE = { + PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies', + PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies', + PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows' +} + ITEMTYPE_FROM_PLEXTYPE = { PLEX_TYPE_MOVIE: 'Movies', PLEX_TYPE_SEASON: 'TVShows', From b62a7a1a1d6793917504e99753b26e0180cd9126 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 08:05:07 +0100 Subject: [PATCH 360/509] Fix add-on paths for tv shows --- resources/lib/itemtypes.py | 52 ++++++++++---------------------------- resources/lib/playback.py | 12 +++------ 2 files changed, 16 insertions(+), 48 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f77e8209..5f7cac79 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -815,7 +815,6 @@ class TVShows(Items): # episodeid kodicursor.execute("select coalesce(max(idEpisode),0) from episode") episodeid = kodicursor.fetchone()[0] + 1 - else: # Verification the item is still in Kodi query = "SELECT * FROM episode WHERE idEpisode = ?" @@ -902,15 +901,9 @@ class TVShows(Items): path = playurl.replace(filename, "") parent_path_id = self.kodi_db.getParentPathId(path) if do_indirect: - # Set plugin path and media flags using real filename - if playurl is not None: - if '\\' in playurl: - filename = playurl.rsplit('\\', 1)[1] - else: - filename = playurl.rsplit('/', 1)[1] - else: - filename = 'file_not_found.mkv' - path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) + # Set plugin path - do NOT use "intermediate" paths for the show + # as with direct paths! + path = 'plugin://%s.tvshows/' % v.ADDON_ID params = { 'plex_id': itemid, 'plex_type': v.PLEX_TYPE_EPISODE, @@ -918,8 +911,6 @@ class TVShows(Items): } filename = "%s?%s" % (path, urlencode(params)) playurl = filename - parent_path_id = self.kodi_db.addPath('plugin://%s.tvshows/' - % v.ADDON_ID) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value @@ -1060,15 +1051,16 @@ class TVShows(Items): checksum=checksum, view_id=viewid) - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?, ", - "idParentPath = ?" - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, None, None, 1, parent_path_id, pathid)) + # Update the path for Direct Paths only + if not do_indirect: + query = ''' + UPDATE path + SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?, + idParentPath = ? + WHERE idPath = ? + ''' + kodicursor.execute(query, (path, None, None, 1, parent_path_id, + pathid)) # Update the file query = ' '.join(( @@ -1098,24 +1090,6 @@ class TVShows(Items): runtime, playcount, dateplayed) - if not state.DIRECT_PATHS and resume: - # Create additional entry for widgets. This is only required for - # plugin/episode. - temppathid = self.kodi_db.getPath('plugin://%s.tvshows/' - % v.ADDON_ID) - tempfileid = self.kodi_db.addFile(filename, temppathid) - query = ''' - UPDATE files - SET idPath = ?, strFilename = ?, dateAdded = ? - WHERE idFile = ? - ''' - kodicursor.execute(query, - (temppathid, filename, dateadded, tempfileid)) - self.kodi_db.addPlaystate(tempfileid, - resume, - runtime, - playcount, - dateplayed) def remove(self, itemid): """ diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 102216d2..f72eb3b6 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -164,15 +164,9 @@ def _prep_playlist_stack(xml): 'plex_id': api.plex_id(), 'plex_type': api.plex_type() } - if api.plex_type() == v.PLEX_TYPE_EPISODE: - path = ('plugin://%s/%s/?%s' - % (v.ADDON_TYPE[api.plex_type()], - api.grandparent_id(), - urlencode(params))) - else: - path = ('plugin://%s/?%s' - % (v.ADDON_TYPE[api.plex_type()], - urlencode(params))) + path = ('plugin://%s/?%s' + % (v.ADDON_TYPE[api.plex_type()], + urlencode(params))) listitem = api.create_listitem() listitem.setPath(try_encode(path)) else: From eeeb3efb7e2e993bd2f4e3e885a9738b6029608f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 08:13:24 +0100 Subject: [PATCH 361/509] Adjust Kodi bookmarks DB entries to resemble Kodi entries --- resources/lib/kodidb_functions.py | 56 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 17d88fd0..4a7acb03 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -807,16 +807,11 @@ class Kodidb_Functions(): Returns all Kodi idFile that have a resume point set (not unwatched ones or items that have already been completely watched) """ - cursor = self.cursor - - query = ' '.join(( - "SELECT idFile", - "FROM bookmark" - )) - try: - rows = cursor.execute(query) - except: - return [] + query = ''' + SELECT idFile + FROM bookmark + ''' + rows = self.cursor.execute(query) ids = [] for row in rows: ids.append(row[0]) @@ -1010,37 +1005,40 @@ class Kodidb_Functions(): """ self.cursor.execute("DELETE FROM bookmark") - def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): + def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, + dateplayed): # Delete existing resume point - query = ' '.join(( - - "DELETE FROM bookmark", - "WHERE idFile = ?" - )) + query = ''' + DELETE FROM bookmark + WHERE idFile = ? + ''' self.cursor.execute(query, (fileid,)) - # Set watched count - query = ' '.join(( - - "UPDATE files", - "SET playCount = ?, lastPlayed = ?", - "WHERE idFile = ?" - )) + query = ''' + UPDATE files + SET playCount = ?, lastPlayed = ? + WHERE idFile = ? + ''' self.cursor.execute(query, (playcount, dateplayed, fileid)) - # Set the resume bookmark if resume_seconds: - self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") - bookmarkId = self.cursor.fetchone()[0] + 1 + self.cursor.execute( + 'select coalesce(max(idBookmark),0) from bookmark') + bookmark_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO bookmark( idBookmark, idFile, timeInSeconds, totalTimeInSeconds, thumbNailImage, player, playerState, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''' - self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, - total_seconds, None, "DVDPlayer", - None, 1)) + self.cursor.execute(query, (bookmark_id, + fileid, + resume_seconds, + total_seconds, + '', + "VideoPlayer", + '', + 1)) def addTags(self, kodiid, tags, mediatype): # First, delete any existing tags associated to the id From 7f20309dc5680a460b7b290f7fa98a5ae93184b8 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 08:18:38 +0100 Subject: [PATCH 362/509] Increse python requests dependency to 2.9.1 --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 6a161cc5..f16c60bd 100644 --- a/addon.xml +++ b/addon.xml @@ -2,7 +2,7 @@ - + From a33b93a6a140ebb0fd8060f60fdb2058747670ee Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 17:38:52 +0100 Subject: [PATCH 363/509] Enable add-on paths for music --- resources/lib/variables.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 484d245c..d19fb226 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -191,7 +191,8 @@ KODI_AUDIOTYPES = ( ADDON_TYPE = { PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies', PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies', - PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows' + PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows', + PLEX_TYPE_SONG: 'plugin.video.plexkodiconnect' } ITEMTYPE_FROM_PLEXTYPE = { From d004152bd80279ca58ee0d2814df95df1d375f70 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 18:13:38 +0100 Subject: [PATCH 364/509] Refactor contextmenu.py --- contextmenu.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/contextmenu.py b/contextmenu.py index c763e586..da260bff 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -23,14 +23,26 @@ def _get_kodi_type(): return kodi_type -if __name__ == "__main__": - WINDOW = Window(10000) - KODI_ID = listitem.getVideoInfoTag().getDbId() - KODI_TYPE = _get_kodi_type() - ARGS = { - 'kodi_id': KODI_ID, - 'kodi_type': KODI_TYPE +def main(): + """ + Grabs kodi_id and kodi_type and sends a request to our main python instance + that context menu needs to be displayed + """ + window = Window(10000) + kodi_id = listitem.getVideoInfoTag().getDbId() + if kodi_id == -1: + # There is no getDbId() method for getMusicInfoTag + # YET TO BE IMPLEMENTED - lookup ID using path + kodi_id = listitem.getMusicInfoTag().getURL() + kodi_type = _get_kodi_type() + args = { + 'kodi_id': kodi_id, + 'kodi_type': kodi_type } - while WINDOW.getProperty('plex_command'): + while window.getProperty('plex_command'): sleep(20) - WINDOW.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(ARGS)) + window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args)) + + +if __name__ == "__main__": + main() From 4909b4bc14bc5220f10c38df5598564188f3fa50 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 22 Feb 2018 18:20:42 +0100 Subject: [PATCH 365/509] Remove obsolete imports --- resources/lib/playback_starter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index ccdca94e..4bb2e0b4 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -4,7 +4,6 @@ from logging import getLogger from threading import Thread from urlparse import parse_qsl -from pickler import pickle_me, Playback_Successful import playback from context_entry import ContextMenu import state @@ -34,7 +33,7 @@ class Playback_Starter(Thread): elif mode == 'context_menu': ContextMenu(kodi_id=params['kodi_id'], kodi_type=params['kodi_type']) - + def run(self): queue = state.COMMAND_PIPELINE_QUEUE LOG.info("----===## Starting Playback_Starter ##===----") From 861f6213f18e962d348fb93b993db6cccacbb097 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 12:41:18 +0100 Subject: [PATCH 366/509] New API method for guid --- resources/lib/PlexAPI.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 49346f60..72c71935 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -41,7 +41,7 @@ from xbmcvfs import exists import clientinfo as client from downloadutils import DownloadUtils as DU from utils import window, settings, language as lang, try_decode, try_encode, \ - unix_date_to_kodi, exists_dir, slugify, dialog + unix_date_to_kodi, exists_dir, slugify, dialog, escape_html import PlexFunctions as PF import plexdb_functions as plexdb import variables as v @@ -365,6 +365,17 @@ class API(object): genre.append(child.attrib['tag']) return genre + def guid_html_escaped(self): + """ + Returns the 'guid' attribute, e.g. + 'com.plexapp.agents.thetvdb://76648/2/4?lang=en' + as an HTML-escaped string or None + """ + answ = self.item.get('guid') + if answ is not None: + answ = escape_html(answ) + return answ + def provider(self, providername=None): """ providername: e.g. 'imdb', 'tvdb' From 0b2592be5ea3ef59d00cacb8e8c65a1b93ea8967 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 13:06:18 +0100 Subject: [PATCH 367/509] Improvements to building PKC playlist elements --- resources/lib/playlist_func.py | 53 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 76f1d23b..b02e8d75 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -8,7 +8,7 @@ from re import compile as re_compile import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU -from utils import try_encode, escape_html +from utils import try_encode from PlexAPI import API from PlexFunctions import GetPlexMetadata from kodidb_functions import kodiid_from_filename @@ -308,8 +308,7 @@ def playlist_item_from_plex(plex_id): return item -def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, - kodi_type=None): +def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None): """ Returns a playlist element for the playqueue using the Plex xml @@ -319,13 +318,9 @@ def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, api = API(xml_video_element) item.plex_id = api.plex_id() item.plex_type = api.plex_type() - try: - item.id = xml_video_element.attrib['%sItemID' % playlist.kind] - except KeyError: - pass - item.guid = xml_video_element.attrib.get('guid') - if item.guid is not None: - item.guid = escape_html(item.guid) + # item.id will only be set if you passed in an xml_video_element from e.g. + # a playQueue + item.id = api.item_id() if kodi_id is not None: item.kodi_id = kodi_id item.kodi_type = kodi_type @@ -333,9 +328,12 @@ def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, 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] + item.kodi_id, item.kodi_type = db_element[0], db_element[4] except TypeError: pass + item.guid = api.guid_html_escaped() + item.playcount = api.viewcount() + item.offset = api.resume_point() item.xml = xml_video_element LOG.debug('Created new playlist item from xml: %s', item) return item @@ -420,7 +418,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): parameters=params) get_playlist_details_from_xml(playlist, xml) # Need to get the details for the playlist item - item = playlist_item_from_xml(playlist, xml[0]) + item = playlist_item_from_xml(xml[0]) except (KeyError, IndexError, TypeError): raise PlaylistError('Could not init Plex playlist with plex_id %s and ' 'kodi_item %s' % (plex_id, kodi_item)) @@ -484,8 +482,8 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None, params['item'] = {'file': item.file} reply = js.playlist_insert(params) if reply.get('error') is not None: - raise PlaylistError('Could not add item to playlist. Kodi reply. %s', - reply) + raise PlaylistError('Could not add item to playlist. Kodi reply. %s' + % reply) return item @@ -506,17 +504,16 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None): # Will always put the new item at the end of the Plex playlist xml = DU().downloadUrl(url, action_type="PUT") try: - item.xml = xml[-1] - item.id = xml[-1].attrib['%sItemID' % playlist.kind] - except IndexError: - LOG.info('Could not get playlist children. Adding a dummy') - except (TypeError, AttributeError, KeyError): - raise PlaylistError('Could not add item %s to playlist %s', - kodi_item, playlist) - # Get the guid for this item - for plex_item in xml: - if plex_item.attrib['%sItemID' % playlist.kind] == item.id: - item.guid = escape_html(plex_item.attrib['guid']) + xml[-1].attrib + except (TypeError, AttributeError, KeyError, IndexError): + raise PlaylistError('Could not add item %s to playlist %s' + % (kodi_item, playlist)) + api = API(xml[-1]) + item.xml = xml[-1] + item.id = api.item_id() + item.guid = api.guid_html_escaped() + item.offset = api.resume_point() + item.playcount = api.viewcount() playlist.items.append(item) if pos == len(playlist.items) - 1: # Item was added at the end @@ -555,7 +552,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, raise PlaylistError('Could not add item to playlist. Kodi reply. %s', reply) if xml_video_element is not None: - item = playlist_item_from_xml(playlist, xml_video_element) + item = playlist_item_from_xml(xml_video_element) item.kodi_id = kodi_id item.kodi_type = kodi_type item.file = file @@ -646,7 +643,7 @@ def add_to_Kodi_playlist(playlist, xml_video_element): Returns a Playlist_Item or raises PlaylistError """ - item = playlist_item_from_xml(playlist, xml_video_element) + item = playlist_item_from_xml(xml_video_element) if item.kodi_id: json_item = {'%sid' % item.kodi_type: item.kodi_id} else: @@ -673,7 +670,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, playlist.kodi_pl.add(url=file, listitem=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 = playlist_item_from_xml(xml_video_element) else: item = playlist_item_from_kodi(kodi_item) if file is not None: From 733e915506f27be8b6a7a89a7646ad044f2b22b7 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 13:18:08 +0100 Subject: [PATCH 368/509] Enable playback of existing Kodi playqueue --- resources/lib/playback.py | 91 ++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index f72eb3b6..543d2dca 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -24,9 +24,9 @@ import variables as v import state ############################################################################### - LOG = getLogger("PLEX." + __name__) - +# Do we need to return ultimately with a setResolvedUrl? +RESOLVE = True ############################################################################### @@ -49,13 +49,13 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): """ LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s', plex_id, plex_type, path) + global RESOLVE + RESOLVE = resolve if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') - if resolve is True: - # Release default.py - pickle_me(Playback_Successful()) # "Unauthorized for PMS" dialog('notification', lang(29999), lang(30017)) + _ensure_resolve() return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) @@ -67,19 +67,13 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): try: playqueue.items[pos] except IndexError: - # Release our default.py before starting our own Kodi player instance - if resolve is True: - state.PKC_CAUSED_STOP = True - result = Playback_Successful() - result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') - pickle_me(result) - playback_init(plex_id, plex_type, playqueue) + _playback_init(plex_id, plex_type, playqueue, pos) else: # kick off playback on second pass - conclude_playback(playqueue, pos) + _conclude_playback(playqueue, pos) -def playback_init(plex_id, plex_type, playqueue): +def _playback_init(plex_id, plex_type, playqueue, pos): """ Playback setup if Kodi starts playing an item for the first time. """ @@ -91,9 +85,23 @@ def playback_init(plex_id, plex_type, playqueue): LOG.error('Could not get a PMS xml for plex id %s', plex_id) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') + _ensure_resolve() return - trailers = False + if playqueue.kodi_pl.size() > 1: + # Special case - we already got a filled Kodi playqueue + try: + _init_existing_kodi_playlist(playqueue) + except PL.PlaylistError: + LOG.error('Aborting playback_init for longer Kodi playlist') + _ensure_resolve() + return + # Now we need to use setResolvedUrl for the item at position pos + _conclude_playback(playqueue, pos) + return + # "Usual" case - consider trailers and parts and build both Kodi and Plex + # playqueues api = API(xml[0]) + trailers = False if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and settings('enableCinema') == "true"): if settings('askCinema') == "true": @@ -115,6 +123,7 @@ def playback_init(plex_id, plex_type, playqueue): plex_id, xml.attrib.get('librarySectionUUID')) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') + _ensure_resolve() return # Should already be empty, but just in case PL.get_playlist_details_from_xml(playqueue, xml) @@ -139,6 +148,39 @@ def playback_init(plex_id, plex_type, playqueue): thread.start() +def _ensure_resolve(): + """ + Will check whether RESOLVE=True and if so, fail Kodi playback startup + with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some + pickling) + + This way we're making sure that other Python instances (calling default.py) + will be destroyed. + """ + if RESOLVE is True: + state.PKC_CAUSED_STOP = True + result = Playback_Successful() + result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') + pickle_me(result) + + +def _init_existing_kodi_playlist(playqueue): + """ + Will take the playqueue's kodi_pl with MORE than 1 element and initiate + playback (without adding trailers) + """ + LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size()) + for i, kodi_item in enumerate(js.playlist_get_items(playqueue.playlistid)): + if i == 0: + item = PL.init_Plex_playlist(playqueue, kodi_item=kodi_item) + else: + item = PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=kodi_item) + item.force_transcode = state.FORCE_TRANSCODE + LOG.debug('Done building Plex playlist from Kodi playlist') + + def _prep_playlist_stack(xml): stack = [] for item in xml: @@ -152,7 +194,9 @@ def _prep_playlist_stack(xml): kodi_id = plex_dbitem[0] if plex_dbitem else None kodi_type = plex_dbitem[4] if plex_dbitem else None else: - # We will never store clips (trailers) in the Kodi DB + # We will never store clips (trailers) in the Kodi DB. + # Also set kodi_id to None for playback via PMS, so that we're + # using add-on paths. kodi_id = None kodi_type = None for part, _ in enumerate(item[0]): @@ -165,8 +209,7 @@ def _prep_playlist_stack(xml): 'plex_type': api.plex_type() } path = ('plugin://%s/?%s' - % (v.ADDON_TYPE[api.plex_type()], - urlencode(params))) + % (v.ADDON_TYPE[api.plex_type()], urlencode(params))) listitem = api.create_listitem() listitem.setPath(try_encode(path)) else: @@ -217,7 +260,7 @@ def _process_stack(playqueue, stack): pos += 1 -def conclude_playback(playqueue, pos): +def _conclude_playback(playqueue, pos): """ ONLY if actually being played (e.g. at 5th position of a playqueue). @@ -281,6 +324,8 @@ def process_indirect(key, offset, resolve=True): setResolvedUrl """ LOG.info('process_indirect called with key: %s, offset: %s', key, offset) + global RESOLVE + RESOLVE = resolve result = Playback_Successful() if key.startswith('http') or key.startswith('{server}'): xml = DU().downloadUrl(key) @@ -292,9 +337,7 @@ def process_indirect(key, offset, resolve=True): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download PMS metadata') - if resolve is True: - # Release default.py - pickle_me(result) + _ensure_resolve() return if offset != '0': offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) @@ -317,9 +360,7 @@ def process_indirect(key, offset, resolve=True): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download last xml for playurl') - if resolve is True: - # Release default.py - pickle_me(result) + _ensure_resolve() return playurl = xml[0].attrib['key'] item.file = playurl From 11ac4fbe4619fb2a5dc4b7d173370731e7d2cc49 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 13:23:49 +0100 Subject: [PATCH 369/509] Fix playback startup failing --- resources/lib/playback.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 543d2dca..0f1efa98 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -100,6 +100,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos): return # "Usual" case - consider trailers and parts and build both Kodi and Plex # playqueues + # Fail the item we're trying to play now so we can restart the player + _ensure_resolve() api = API(xml[0]) trailers = False if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and From edff54bb7e22d9d1e00343235a33ee10aabc6055 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 14:59:11 +0100 Subject: [PATCH 370/509] Don't cache subtitles if direct playing --- resources/lib/playback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 0f1efa98..7c481ad5 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -291,9 +291,9 @@ def _conclude_playback(playqueue, pos): else: playurl = item.file listitem.setPath(try_encode(playurl)) - if item.playmethod in ('DirectStream', 'DirectPlay'): + if item.playmethod == 'DirectStream': listitem.setSubtitles(api.cache_external_subs()) - else: + elif item.playmethod == 'Transcode': playutils.audio_subtitle_prefs(listitem) if state.RESUME_PLAYBACK is True: state.RESUME_PLAYBACK = False From 60b90b1f52e2735475c1062829904b13931894db Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 15:24:26 +0100 Subject: [PATCH 371/509] Fix Companion displaying and selecting wrong subtitle --- resources/lib/playlist_func.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index b02e8d75..ac73cbd9 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -190,8 +190,16 @@ class Playlist_Item(object): """ stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] count = 0 + # Kodi indexes differently than Plex for stream in self.xml[0][self.part]: - if stream.attrib['streamType'] == stream_type: + if (stream.attrib['streamType'] == stream_type and + 'key' in stream.attrib): + if count == kodi_stream_index: + return stream.attrib['id'] + count += 1 + for stream in self.xml[0][self.part]: + if (stream.attrib['streamType'] == stream_type and + 'key' not in stream.attrib): if count == kodi_stream_index: return stream.attrib['id'] count += 1 @@ -208,7 +216,14 @@ class Playlist_Item(object): stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] count = 0 for stream in self.xml[0][self.part]: - if stream.attrib['streamType'] == stream_type: + if (stream.attrib['streamType'] == stream_type and + 'key' in stream.attrib): + if stream.attrib['id'] == plex_stream_index: + return count + count += 1 + for stream in self.xml[0][self.part]: + if (stream.attrib['streamType'] == stream_type and + 'key' not in stream.attrib): if stream.attrib['id'] == plex_stream_index: return count count += 1 From 6ece9ab5cfcdf58da3d001506c3ed2fa7c159afb Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 16:10:11 +0100 Subject: [PATCH 372/509] Start cast order with 0 like Kodi --- resources/lib/kodidb_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 4a7acb03..ab3609de 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -365,7 +365,7 @@ class Kodidb_Functions(): return castorder def addPeople(self, kodiid, people, mediatype): - castorder = 1 + castorder = 0 for person in people: # Kodi Isengard, Jarvis, Krypton if v.KODIVERSION > 14: From e6631c3c78987608e76be60de14a23fcbbca27c0 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 16:10:52 +0100 Subject: [PATCH 373/509] Get rid of Helix code --- resources/lib/kodidb_functions.py | 150 ++---------------------------- 1 file changed, 8 insertions(+), 142 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index ab3609de..3612bf3b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -367,148 +367,14 @@ class Kodidb_Functions(): def addPeople(self, kodiid, people, mediatype): castorder = 0 for person in people: - # Kodi Isengard, Jarvis, Krypton - if v.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(( - - "SELECT idActor", - "FROM actors", - "WHERE strActor = ?", - "COLLATE NOCASE" - )) - 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, person['Name'])) - finally: - # Link person to content - if "Actor" == person['Type']: - role = person.get('Role') - - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkmovie( - idActor, idMovie, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinktvshow( - idActor, idShow, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkepisode( - idActor, idEpisode, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - else: - # Item is invalid - return - self.cursor.execute(query, (actorid, kodiid, role, castorder)) - castorder += 1 - - elif "Director" == person['Type']: - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmovie( - idDirector, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinktvshow( - idDirector, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmusicvideo( - idDirector, idMVideo) - - VALUES (?, ?) - ''' - ) - - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkepisode( - idDirector, idEpisode) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (actorid, kodiid)) - - elif person['Type'] == "Writer": - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkmovie( - idWriter, idMovie) - - VALUES (?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkepisode( - idWriter, idEpisode) - - VALUES (?, ?) - ''' - ) - else: - # Item is invalid - return - self.cursor.execute(query, (actorid, kodiid)) - elif "Artist" == person['Type']: - query = ( - ''' - INSERT OR REPLACE INTO artistlinkmusicvideo( - idArtist, idMVideo) - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid)) - + actorid = self._getactorid(person['Name']) + # Link person to content + castorder = self._addPerson(person.get('Role'), + person['Type'], + actorid, + kodiid, + mediatype, + castorder) # Add person image to art table if person['imageurl']: self.artwork.addOrUpdateArt(person['imageurl'], actorid, From c059856691708e12c1514f56383e9e43c8eb4b27 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 16:18:10 +0100 Subject: [PATCH 374/509] Simplify code --- resources/lib/PlexAPI.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 72c71935..201cf734 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -337,22 +337,14 @@ class API(object): """ people = [] for child in self.item: - if child.tag in PEOPLE_OF_INTEREST.keys(): - name = child.attrib['tag'] - name_id = child.attrib['id'] - typus = PEOPLE_OF_INTEREST[child.tag] - url = child.get('thumb') - role = child.get('role') + if child.tag in PEOPLE_OF_INTEREST: people.append({ - 'Name': name, - 'Type': typus, - 'Id': name_id, - 'imageurl': url + 'Name': child.attrib['tag'], + 'Type': PEOPLE_OF_INTEREST[child.tag], + 'Id': child.attrib['id'], + 'imageurl': child.get('thumb'), + 'Role': child.get('role') }) - if url: - people[-1].update({'imageurl': url}) - if role: - people[-1].update({'Role': role}) return people def genre_list(self): From bf5616069018eae1cef32103460d820c2b5dd7c1 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 16:53:06 +0100 Subject: [PATCH 375/509] Fix trailers --- resources/lib/PlexAPI.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 201cf734..e83293f2 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -645,14 +645,15 @@ class API(object): """ Returns the ratingKey (plex_id) of the trailer or None """ - for extra in self.item.iterfind('Extras'): - try: - typus = int(extra.attrib['extraType']) - except (KeyError, TypeError): - typus = None - if typus != 1: - continue - return extra.get('ratingKey') + for extras in self.item.iterfind('Extras'): + for extra in extras: + try: + typus = int(extra.attrib['extraType']) + except (KeyError, TypeError): + typus = None + if typus != 1: + continue + return extra.get('ratingKey') def mediastreams(self): """ From 919cd6ddfd1465a1bb05e74c0d43ee00999124fe Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 17:22:57 +0100 Subject: [PATCH 376/509] Fix info screen and actors not working --- resources/lib/kodidb_functions.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 3612bf3b..1db8ff95 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -62,18 +62,16 @@ class Kodidb_Functions(): strPath, strContent, strScraper, - useFolderNames, noUpdate, exclude) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (path_id, 'plugin://%s.movies/' % v.ADDON_ID, 'movies', 'metadata.local', - 0, 1, - 1)) + 0)) # And TV shows path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID) if path_id is None: @@ -84,18 +82,16 @@ class Kodidb_Functions(): strPath, strContent, strScraper, - useFolderNames, noUpdate, exclude) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (path_id, 'plugin://%s.tvshows/' % v.ADDON_ID, 'tvshows', 'metadata.local', - 0, 1, - 1)) + 0)) def getParentPathId(self, path): """ From af0f03e534b96894d0ceac2ad95c7ed0d3032273 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 23 Feb 2018 17:40:42 +0100 Subject: [PATCH 377/509] Version bump --- README.md | 2 +- addon.xml | 19 +++++++++++++++++-- changelog.txt | 15 +++++++++++++++ resources/lib/variables.py | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2d55e6b2..510bbd1d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.3-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.4-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index f16c60bd..b702fefd 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,22 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.3 (beta only): + version 2.0.4 (beta only): +- WARNING: You will need to reset the Kodi database! +- Many improvements to the Kodi database handling which should get rid of some weird bugs +- Many improvements to playback startup +- Fix info screen and actors not working +- Fix Companion displaying and selecting wrong subtitle +- Don't cache subtitles if direct playing +- Wipe all existing resume point, e.g. on user switch +- Don't mess with Kodi's screensaver settings +- Inhibit idle shutdown only during initial sync +- Fix KeyError for server discovery +- Increase Python requests dependency to version 2.9.1 +- Re-introduce PlexKodiConnect dependency add-ons for movies and tv shows +- And a lot of other stuff + +version 2.0.3 (beta only): - Fix Alexa playback - Fix Kodi boot loop - Fix playback being reported to the wrong Plex user diff --git a/changelog.txt b/changelog.txt index 0b108210..6f79e610 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,18 @@ +version 2.0.4 (beta only): +- WARNING: You will need to reset the Kodi database! +- Many improvements to the Kodi database handling which should get rid of some weird bugs +- Many improvements to playback startup +- Fix info screen and actors not working +- Fix Companion displaying and selecting wrong subtitle +- Don't cache subtitles if direct playing +- Wipe all existing resume point, e.g. on user switch +- Don't mess with Kodi's screensaver settings +- Inhibit idle shutdown only during initial sync +- Fix KeyError for server discovery +- Increase Python requests dependency to version 2.9.1 +- Re-introduce PlexKodiConnect dependency add-ons for movies and tv shows +- And a lot of other stuff + version 2.0.3 (beta only): - Fix Alexa playback - Fix Kodi boot loop diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 484d245c..c5867800 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -75,7 +75,7 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort')) PKC_MACHINE_IDENTIFIER = None # Minimal PKC version needed for the Kodi database - otherwise need to recreate -MIN_DB_VERSION = '2.0.0' +MIN_DB_VERSION = '2.0.4' # Database paths _DB_VIDEO_VERSION = { From ca001a951f04fea306784b9192bad610d6015c44 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:00:46 +0100 Subject: [PATCH 378/509] Don't repeatedly check plex.tv connection if offline - Fixes #415 --- .../resource.language.en_gb/strings.po | 4 ---- resources/lib/initialsetup.py | 20 ------------------- 2 files changed, 24 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 4e6a0254..b1a9193b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1670,10 +1670,6 @@ msgctxt "#39213" msgid "is offline" msgstr "" -msgctxt "#39214" -msgid "Even though we signed in to plex.tv, we could not authorize for PMS" -msgstr "" - msgctxt "#39215" msgid "Enter your Plex Media Server's IP or URL, Examples are:" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index bd756f6b..aa92173f 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -317,7 +317,6 @@ class InitialSetup(object): Returns server or None if unsuccessful """ https_updated = False - checked_plex_tv = False server = None while True: if https_updated is False: @@ -340,25 +339,6 @@ class InitialSetup(object): server['scheme'] = 'https' https_updated = True continue - if chk == 401: - LOG.warn('Not yet authorized for Plex server %s', - server['name']) - if self.check_plex_tv_sign_in() is True: - if checked_plex_tv is False: - # Try again - checked_plex_tv = True - https_updated = False - continue - else: - LOG.warn('Not authorized even though we are signed ' - ' in to plex.tv correctly') - dialog('ok', - lang(29999), - '%s %s' % (lang(39214), - try_encode(server['name']))) - return - else: - return # Problems connecting elif chk >= 400 or chk is False: LOG.warn('Problems connecting to server %s. chk is %s', From 3f1da3c1eab00be2aa03e7ffb19b7c426868a33c Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:10:48 +0100 Subject: [PATCH 379/509] Don't list collections/sets also as Kodi tags --- resources/lib/itemtypes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 5f7cac79..f9724c57 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -447,7 +447,6 @@ class Movies(Items): self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags tags = [viewtag] - tags.extend(collections) if userdata['Favorite']: tags.append("Favorite movies") self.kodi_db.addTags(movieid, tags, "movie") @@ -736,7 +735,6 @@ class TVShows(Items): self.kodi_db.addStudios(showid, studios, "tvshow") # Process tags: view, PMS collection tags tags = [viewtag] - tags.extend(collections) self.kodi_db.addTags(showid, tags, "tvshow") @catch_exceptions(warnuser=True) From eb0d1d21bba681907a263ea9c9c38ab5acbe58f9 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:25:31 +0100 Subject: [PATCH 380/509] Revert "Don't list collections/sets also as Kodi tags" This reverts commit 3f1da3c1eab00be2aa03e7ffb19b7c426868a33c. --- resources/lib/itemtypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f9724c57..5f7cac79 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -447,6 +447,7 @@ class Movies(Items): self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags tags = [viewtag] + tags.extend(collections) if userdata['Favorite']: tags.append("Favorite movies") self.kodi_db.addTags(movieid, tags, "movie") @@ -735,6 +736,7 @@ class TVShows(Items): self.kodi_db.addStudios(showid, studios, "tvshow") # Process tags: view, PMS collection tags tags = [viewtag] + tags.extend(collections) self.kodi_db.addTags(showid, tags, "tvshow") @catch_exceptions(warnuser=True) From ae6fb9ecfa903525393867bd2edb6aadec77c175 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:35:09 +0100 Subject: [PATCH 381/509] Remove Kodi Helix support --- resources/lib/kodidb_functions.py | 644 +++++++++--------------------- 1 file changed, 180 insertions(+), 464 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 1db8ff95..43f39458 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -234,76 +234,38 @@ class Kodidb_Functions(): self.cursor.execute(query, (pathid, filename,)) def addCountries(self, kodiid, countries, mediatype): - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - for country in countries: - query = ' '.join(( + for country in countries: + query = ' '.join(( - "SELECT country_id", - "FROM country", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) + "SELECT country_id", + "FROM country", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (country,)) - try: - country_id = self.cursor.fetchone()[0] + try: + country_id = self.cursor.fetchone()[0] - except TypeError: - # Country entry does not exists - self.cursor.execute("select coalesce(max(country_id),0) from country") - country_id = self.cursor.fetchone()[0] + 1 + except TypeError: + # Country entry does not exists + self.cursor.execute("select coalesce(max(country_id),0) from country") + country_id = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO country(country_id, name) values(?, ?)" - self.cursor.execute(query, (country_id, country)) - log.debug("Add country to media, processing: %s" % country) + query = "INSERT INTO country(country_id, name) values(?, ?)" + self.cursor.execute(query, (country_id, country)) + log.debug("Add country to media, processing: %s" % country) - finally: # Assign country to content - query = ( - ''' - INSERT OR REPLACE INTO country_link( - country_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (country_id, kodiid, mediatype)) - else: - # Kodi Helix - for country in countries: - query = ' '.join(( - - "SELECT idCountry", - "FROM country", - "WHERE strCountry = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) - - try: - idCountry = self.cursor.fetchone()[0] - - except TypeError: - # Country entry does not exists - self.cursor.execute("select coalesce(max(idCountry),0) from country") - idCountry = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" - self.cursor.execute(query, (idCountry, country)) - log.debug("Add country to media, processing: %s" % country) - - finally: - # Only movies have a country field - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO countrylinkmovie( - idCountry, idMovie) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (idCountry, kodiid)) + finally: # Assign country to content + query = ( + ''' + INSERT OR REPLACE INTO country_link( + country_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (country_id, kodiid, mediatype)) def _getactorid(self, name): """ @@ -420,202 +382,82 @@ class Kodidb_Functions(): return result def addGenres(self, kodiid, genres, mediatype): + # Delete current genres for clean slate + query = ' '.join(( - - # Kodi Isengard, Jarvis, Krypton - if v.KODIVERSION > 14: - # Delete current genres for clean slate + "DELETE FROM genre_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodiid, mediatype,)) + + # Add genres + for genre in genres: + query = ' '.join(( - "DELETE FROM genre_link", - "WHERE media_id = ?", - "AND media_type = ?" + "SELECT genre_id", + "FROM genre", + "WHERE name = ?", + "COLLATE NOCASE" )) - self.cursor.execute(query, (kodiid, mediatype,)) - - # Add genres - for genre in genres: + self.cursor.execute(query, (genre,)) + + try: + genre_id = self.cursor.fetchone()[0] + + except TypeError: + # Create genre in database + self.cursor.execute("select coalesce(max(genre_id),0) from genre") + genre_id = self.cursor.fetchone()[0] + 1 - query = ' '.join(( + query = "INSERT INTO genre(genre_id, name) values(?, ?)" + self.cursor.execute(query, (genre_id, genre)) + log.debug("Add Genres to media, processing: %s" % genre) + + finally: + # Assign genre to item + query = ( + ''' + INSERT OR REPLACE INTO genre_link( + genre_id, media_id, media_type) - "SELECT genre_id", - "FROM genre", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - genre_id = self.cursor.fetchone()[0] - - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(genre_id),0) from genre") - genre_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(genre_id, name) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - log.debug("Add Genres to media, processing: %s" % genre) - - finally: - # Assign genre to item - query = ( - ''' - INSERT OR REPLACE INTO genre_link( - genre_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (genre_id, kodiid, mediatype)) - else: - # Kodi Helix - # Delete current genres for clean slate - if "movie" in mediatype: - self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,)) - elif "tvshow" in mediatype: - self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) - elif "musicvideo" in mediatype: - self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - idGenre = self.cursor.fetchone()[0] - - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - idGenre = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (idGenre, genre)) - log.debug("Add Genres to media, processing: %s" % genre) - - finally: - # Assign genre to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmovie( - idGenre, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinktvshow( - idGenre, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmusicvideo( - idGenre, idMVideo) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (idGenre, kodiid)) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (genre_id, kodiid, mediatype)) def addStudios(self, kodiid, studios, mediatype): for studio in studios: - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( + query = ' '.join(( - "SELECT studio_id", - "FROM studio", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studioid = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(studio_id),0) from studio") - studioid = self.cursor.fetchone()[0] + 1 + "SELECT studio_id", + "FROM studio", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (studio,)) + try: + studioid = self.cursor.fetchone()[0] + + except TypeError: + # Studio does not exists. + self.cursor.execute("select coalesce(max(studio_id),0) from studio") + studioid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO studio(studio_id, name) values(?, ?)" - self.cursor.execute(query, (studioid, studio)) - log.debug("Add Studios to media, processing: %s" % studio) + query = "INSERT INTO studio(studio_id, name) values(?, ?)" + self.cursor.execute(query, (studioid, studio)) + log.debug("Add Studios to media, processing: %s" % studio) - finally: # Assign studio to item - query = ( - ''' - INSERT OR REPLACE INTO studio_link( - studio_id, media_id, media_type) - - VALUES (?, ?, ?) - ''') - self.cursor.execute(query, (studioid, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idstudio", - "FROM studio", - "WHERE strstudio = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studioid = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(idstudio),0) from studio") - studioid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" - self.cursor.execute(query, (studioid, studio)) - log.debug("Add Studios to media, processing: %s" % studio) - - finally: # Assign studio to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) - VALUES (?, ?) - ''') - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) - VALUES (?, ?) - ''') - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) - VALUES (?, ?) - ''') - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) - VALUES (?, ?) - ''') - self.cursor.execute(query, (studioid, kodiid)) + finally: # Assign studio to item + query = ( + ''' + INSERT OR REPLACE INTO studio_link( + studio_id, media_id, media_type) + + VALUES (?, ?, ?) + ''') + self.cursor.execute(query, (studioid, kodiid, mediatype)) def addStreams(self, fileid, streamdetails, runtime): @@ -904,24 +746,13 @@ class Kodidb_Functions(): def addTags(self, kodiid, tags, mediatype): # First, delete any existing tags associated to the id - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( + query = ' '.join(( - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype)) + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodiid, mediatype)) # Add tags log.debug("Adding Tags: %s" % tags) @@ -929,205 +760,101 @@ class Kodidb_Functions(): self.addTag(kodiid, tag, mediatype) def addTag(self, kodiid, tag, mediatype): - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( + query = ' '.join(( - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag, because it does not exist - tag_id = self.createTag(tag) - log.debug("Adding tag: %s" % tag) + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + # Create the tag, because it does not exist + tag_id = self.createTag(tag) + log.debug("Adding tag: %s" % tag) - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO tag_link( - tag_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag - tag_id = self.createTag(tag) - log.debug("Adding tag: %s" % tag) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO taglinks( - idTag, idMedia, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodiid, mediatype)) + finally: + # Assign tag to item + query = ( + ''' + INSERT OR REPLACE INTO tag_link( + tag_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (tag_id, kodiid, mediatype)) def createTag(self, name): # This will create and return the tag_id - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( + query = ' '.join(( - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - self.cursor.execute("select coalesce(max(tag_id),0) from tag") - tag_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(tag_id, name) values(?, ?)" - self.cursor.execute(query, (tag_id, name)) - log.debug("Create tag_id: %s name: %s" % (tag_id, name)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - self.cursor.execute("select coalesce(max(idTag),0) from tag") - tag_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO tag(idTag, strTag) values(?, ?)" - self.cursor.execute(query, (tag_id, name)) - log.debug("Create idTag: %s name: %s" % (tag_id, name)) + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + self.cursor.execute("select coalesce(max(tag_id),0) from tag") + tag_id = self.cursor.fetchone()[0] + 1 + query = "INSERT INTO tag(tag_id, name) values(?, ?)" + self.cursor.execute(query, (tag_id, name)) + log.debug("Create tag_id: %s name: %s" % (tag_id, name)) return tag_id def updateTag(self, oldtag, newtag, kodiid, mediatype): - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - try: - query = ' '.join(( + try: + query = ' '.join(( - "UPDATE tag_link", - "SET tag_id = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) - except Exception as e: - # The new tag we are going to apply already exists for this item - # delete current tag instead - query = ' '.join(( + "UPDATE tag_link", + "SET tag_id = ?", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) + except Exception as e: + # The new tag we are going to apply already exists for this item + # delete current tag instead + query = ' '.join(( - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, oldtag,)) - else: - # Kodi Helix - try: - query = ' '.join(( - - "UPDATE taglinks", - "SET idTag = ?", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) - except Exception as e: - # The new tag we are going to apply already exists for this item - # delete current tag instead - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, oldtag,)) + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + self.cursor.execute(query, (kodiid, mediatype, oldtag,)) def removeTag(self, kodiid, tagname, mediatype): - if v.KODIVERSION > 14: - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( + query = ' '.join(( - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tagname,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, tag_id,)) + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tagname,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + return else: - # Kodi Helix query = ' '.join(( - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" )) - self.cursor.execute(query, (tagname,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, tag_id,)) + self.cursor.execute(query, (kodiid, mediatype, tag_id,)) def addSets(self, movieid, collections, kodicursor): for setname in collections: @@ -1263,25 +990,14 @@ class Kodidb_Functions(): # Create the album self.cursor.execute("select coalesce(max(idAlbum),0) from album") albumid = self.cursor.fetchone()[0] + 1 - if v.KODIVERSION > 14: - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (albumid, name, musicbrainz, "album")) - else: # Helix - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (albumid, name, musicbrainz)) + query = ( + ''' + INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (albumid, name, musicbrainz, "album")) return albumid def addMusicGenres(self, kodiid, genres, mediatype): From b42a9e2062d7f19d5cbff2fc9ec4891c46c3d5c2 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:37:30 +0100 Subject: [PATCH 382/509] Prettify logging --- resources/lib/kodidb_functions.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 43f39458..7809b816 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -10,7 +10,7 @@ import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger("PLEX." + __name__) ############################################################################### @@ -254,7 +254,7 @@ class Kodidb_Functions(): query = "INSERT INTO country(country_id, name) values(?, ?)" self.cursor.execute(query, (country_id, country)) - log.debug("Add country to media, processing: %s" % country) + LOG.debug("Add country to media, processing: %s", country) finally: # Assign country to content query = ( @@ -413,7 +413,7 @@ class Kodidb_Functions(): query = "INSERT INTO genre(genre_id, name) values(?, ?)" self.cursor.execute(query, (genre_id, genre)) - log.debug("Add Genres to media, processing: %s" % genre) + LOG.debug("Add Genres to media, processing: %s", genre) finally: # Assign genre to item @@ -447,7 +447,7 @@ class Kodidb_Functions(): query = "INSERT INTO studio(studio_id, name) values(?, ?)" self.cursor.execute(query, (studioid, studio)) - log.debug("Add Studios to media, processing: %s" % studio) + LOG.debug("Add Studios to media, processing: %s", studio) finally: # Assign studio to item query = ( @@ -557,7 +557,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (filename,)) files = self.cursor.fetchall() if len(files) == 0: - log.info('Did not find any file, abort') + LOG.info('Did not find any file, abort') return query = ' '.join(( "SELECT strPath", @@ -581,12 +581,12 @@ class Kodidb_Functions(): if strPath == path: result.append(file[0]) if len(result) == 0: - log.info('Did not find matching paths, abort') + LOG.info('Did not find matching paths, abort') return # Kodi seems to make ONE temporary entry; we only want the earlier, # permanent one if len(result) > 2: - log.warn('We found too many items with matching filenames and ' + LOG.warn('We found too many items with matching filenames and ' ' paths, aborting') return idFile = result[0] @@ -613,7 +613,7 @@ class Kodidb_Functions(): itemId = self.cursor.fetchone()[0] typus = v.KODI_TYPE_EPISODE except TypeError: - log.warn('Unexpectantly did not find a match!') + LOG.warn('Unexpectantly did not find a match!') return return itemId, typus @@ -630,7 +630,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (path,)) path_id = self.cursor.fetchall() if len(path_id) != 1: - log.error('Found wrong number of path ids: %s for path %s, abort', + LOG.error('Found wrong number of path ids: %s for path %s, abort', path_id, path) return query = ''' @@ -641,7 +641,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (filename, path_id[0])) song_id = self.cursor.fetchall() if len(song_id) != 1: - log.info('Found wrong number of songs %s, abort', song_id) + LOG.info('Found wrong number of songs %s, abort', song_id) return return song_id[0] @@ -755,7 +755,7 @@ class Kodidb_Functions(): self.cursor.execute(query, (kodiid, mediatype)) # Add tags - log.debug("Adding Tags: %s" % tags) + LOG.debug("Adding Tags: %s", tags) for tag in tags: self.addTag(kodiid, tag, mediatype) @@ -774,7 +774,7 @@ class Kodidb_Functions(): except TypeError: # Create the tag, because it does not exist tag_id = self.createTag(tag) - log.debug("Adding tag: %s" % tag) + LOG.debug("Adding tag: %s", tag) finally: # Assign tag to item @@ -807,7 +807,7 @@ class Kodidb_Functions(): query = "INSERT INTO tag(tag_id, name) values(?, ?)" self.cursor.execute(query, (tag_id, name)) - log.debug("Create tag_id: %s name: %s" % (tag_id, name)) + LOG.debug("Create tag_id: %s name: %s", tag_id, name) return tag_id def updateTag(self, oldtag, newtag, kodiid, mediatype): @@ -863,7 +863,7 @@ class Kodidb_Functions(): def createBoxset(self, boxsetname): - log.debug("Adding boxset: %s" % boxsetname) + LOG.debug("Adding boxset: %s", boxsetname) query = ' '.join(( "SELECT idSet", @@ -1202,11 +1202,11 @@ def kodiid_from_filename(path, kodi_type): try: kodi_id, _ = kodi_db.music_id_from_filename(filename, path) except TypeError: - log.debug('No Kodi audio db element found for path %s', path) + LOG.debug('No Kodi audio db element found for path %s', path) else: with GetKodiDB('video') as kodi_db: try: kodi_id, _ = kodi_db.video_id_from_filename(filename, path) except TypeError: - log.debug('No kodi video db element found for path %s', path) + LOG.debug('No kodi video db element found for path %s', path) return kodi_id From 5c944cd092fdf5dae1824df48152e032eddbf154 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 13:42:20 +0100 Subject: [PATCH 383/509] Fix kodidb_function.py classes --- resources/lib/itemtypes.py | 4 ++-- resources/lib/kodidb_functions.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 5f7cac79..64f3c452 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -48,7 +48,7 @@ class Items(object): self.kodiconn = kodi_sql('video') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) - self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) + self.kodi_db = kodidb.KodiDBMethods(self.kodicursor) return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -1250,7 +1250,7 @@ class Music(Items): self.kodiconn = kodi_sql('music') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) - self.kodi_db = kodidb.Kodidb_Functions(self.kodicursor) + self.kodi_db = kodidb.KodiDBMethods(self.kodicursor) return self @catch_exceptions(warnuser=True) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 7809b816..ee10e2ea 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -15,7 +15,7 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### -class GetKodiDB(): +class GetKodiDB(object): """ Usage: with GetKodiDB(db_type) as kodi_db: do stuff with kodi_db @@ -27,19 +27,25 @@ class GetKodiDB(): and the db gets closed """ def __init__(self, db_type): + self.kodiconn = None self.db_type = db_type def __enter__(self): self.kodiconn = kodi_sql(self.db_type) - kodi_db = Kodidb_Functions(self.kodiconn.cursor()) + kodi_db = KodiDBMethods(self.kodiconn.cursor()) return kodi_db - def __exit__(self, type, value, traceback): + def __exit__(self, typus, value, traceback): self.kodiconn.commit() self.kodiconn.close() -class Kodidb_Functions(): +class KodiDBMethods(object): + """ + Best used indirectly with another Class GetKodiDB: + with GetKodiDB(db_type) as kodi_db: + kodi_db.method() + """ def __init__(self, cursor): self.cursor = cursor self.artwork = artwork.Artwork() From b79ed87ea7d42e8acd4754b755a697112d7dc82f Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 14:15:50 +0100 Subject: [PATCH 384/509] Ensure deletion of countries in Kodi DB for movies --- resources/lib/itemtypes.py | 1 + resources/lib/kodidb_functions.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 64f3c452..50f72129 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -480,6 +480,7 @@ class Movies(Items): artwork.deleteArtwork(kodi_id, kodi_type, kodicursor) if kodi_type == v.KODI_TYPE_MOVIE: + self.kodi_db.delete_countries(kodi_id, kodi_type) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index ee10e2ea..5cda6fc7 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -273,6 +273,31 @@ class KodiDBMethods(object): ) self.cursor.execute(query, (country_id, kodiid, mediatype)) + def delete_countries(self, kodi_id, kodi_type): + """ + Assuming that video kodi_id, kodi_type gets deleted, will delete any + associated country links in the table country_link and also deletes + orphaned countries in the table country + """ + # Get all existing links + query = ''' + SELECT country_id FROM country_link + WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + country_ids = self.cursor.fetchall() + # Delete all links + query = 'DELETE FROM country_link WHERE media_id = ? AND media_type = ?' + self.cursor.execute(query, (kodi_id, kodi_type)) + # Which countries are now orphaned? + query = 'SELECT country_id FROM country_link WHERE country_id = ?' + query_delete = 'DELETE FROM country WHERE country_id = ?' + for country_id in country_ids: + # country_id still in table? + self.cursor.execute(query, (country_id,)) + if self.cursor.fetchone() is None: + self.cursor.execute(query_delete, (country_id,)) + def _getactorid(self, name): """ Crucial für sync speed! From 411f691547bec5709ee4af5ec6d1ffcf41bd0f2b Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 17:45:38 +0100 Subject: [PATCH 385/509] Delete people entries from Kodi DB --- resources/lib/itemtypes.py | 2 ++ resources/lib/kodidb_functions.py | 58 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 50f72129..7d9aa578 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -481,6 +481,7 @@ class Movies(Items): if kodi_type == v.KODI_TYPE_MOVIE: self.kodi_db.delete_countries(kodi_id, kodi_type) + self.kodi_db.delete_people(kodi_id, kodi_type) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1226,6 +1227,7 @@ class TVShows(Items): Remove an episode, and episode only """ kodicursor = self.kodicursor + self.kodi_db.delete_people(kodi_id, v.KODI_TYPE_EPISODE) self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 5cda6fc7..c7788a01 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -370,6 +370,64 @@ class KodiDBMethods(object): person['Type'].lower(), "thumb", self.cursor) + def delete_people(self, kodi_id, kodi_type): + """ + Assuming that the video kodi_id, kodi_type gets deleted, will delete any + associated actor_, director_, writer_links and also deletes + orphaned actors + """ + # Actors + query = ''' + SELECT actor_id FROM actor_link + WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + actor_ids = self.cursor.fetchall() + query = 'DELETE FROM actor_link WHERE media_id = ? AND media_type = ?' + self.cursor.execute(query, (kodi_id, kodi_type)) + # Directors + query = ''' + SELECT actor_id FROM director_link + WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + actor_ids.extend(self.cursor.fetchall()) + query = ''' + DELETE FROM director_link WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + # Writers + query = ''' + SELECT actor_id FROM writer_link + WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + actor_ids.extend(self.cursor.fetchall()) + query = ''' + DELETE FROM writer_link WHERE media_id = ? AND media_type = ? + ''' + self.cursor.execute(query, (kodi_id, kodi_type)) + # Which people are now orphaned? + query_actor = 'SELECT actor_id FROM actor_link WHERE actor_id = ?' + query_director = 'SELECT actor_id FROM director_link WHERE actor_id = ?' + query_writer = 'SELECT actor_id FROM writer_link WHERE actor_id = ?' + query_delete = 'DELETE FROM actor WHERE actor_id = ?' + # Delete orphaned people + for actor_id in actor_ids: + self.cursor.execute(query_actor, (actor_id,)) + if self.cursor.fetchone() is None: + self.cursor.execute(query_director, (actor_id,)) + if self.cursor.fetchone() is None: + self.cursor.execute(query_writer, (actor_id,)) + if self.cursor.fetchone() is None: + # Delete the person itself from actor table + self.cursor.execute(query_delete, (actor_id,)) + # Delete any associated artwork + self.artwork.deleteArtwork(actor_id, + 'actor', + self.cursor) + + def existingArt(self, kodiId, mediaType, refresh=False): """ For kodiId, returns an artwork dict with already existing art from From 818f370c46ba8996a0b687d856b7a8c686c34571 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 17:51:36 +0100 Subject: [PATCH 386/509] Prettify --- resources/lib/kodidb_functions.py | 39 +++++++++---------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index c7788a01..d747d28b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -470,51 +470,34 @@ class KodiDBMethods(object): result['Backdrop'] = [d[0] for d in data] return result - def addGenres(self, kodiid, genres, mediatype): + def addGenres(self, kodi_id, genres, kodi_type): + """ + Adds the genres (list of strings) to the Kodi DB and associates them + with the element kodi_id, kodi_type + """ # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM genre_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype,)) - + query = 'DELETE FROM genre_link WHERE media_id = ? AND media_type = ?' + self.cursor.execute(query, (kodi_id, kodi_type,)) # Add genres for genre in genres: - - query = ' '.join(( - - "SELECT genre_id", - "FROM genre", - "WHERE name = ?", - "COLLATE NOCASE" - )) + query = ' SELECT genre_id FROM genre WHERE name = ? COLLATE NOCASE' self.cursor.execute(query, (genre,)) - try: genre_id = self.cursor.fetchone()[0] - except TypeError: # Create genre in database self.cursor.execute("select coalesce(max(genre_id),0) from genre") genre_id = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(genre_id, name) values(?, ?)" self.cursor.execute(query, (genre_id, genre)) - LOG.debug("Add Genres to media, processing: %s", genre) - finally: # Assign genre to item - query = ( - ''' + query = ''' INSERT OR REPLACE INTO genre_link( genre_id, media_id, media_type) - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (genre_id, kodiid, mediatype)) + ''' + self.cursor.execute(query, (genre_id, kodi_id, kodi_type)) def addStudios(self, kodiid, studios, mediatype): for studio in studios: From 1a774275917aeb96b9073d97118645ee0fd3643e Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:06:33 +0100 Subject: [PATCH 387/509] Optimize code --- resources/lib/artwork.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 0565b7b7..ee626508 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -310,15 +310,9 @@ class Artwork(): self.cacheTexture(imageUrl) def deleteArtwork(self, kodiId, mediaType, cursor): - query = ' '.join(( - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?" - )) + query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?' cursor.execute(query, (kodiId, mediaType,)) - rows = cursor.fetchall() - for row in rows: + for row in cursor.fetchall(): self.deleteCachedArtwork(row[0]) def deleteCachedArtwork(self, url): From eedabf58881d01ecf395832d17b2f54cbe2ed219 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:07:48 +0100 Subject: [PATCH 388/509] Fix TypeErrors because SQL returns tuplex --- resources/lib/kodidb_functions.py | 53 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index d747d28b..0007fc9e 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -273,30 +273,37 @@ class KodiDBMethods(object): ) self.cursor.execute(query, (country_id, kodiid, mediatype)) + def _delete_from_link_and_table(self, kodi_id, kodi_type, link_table, + table, key): + # Get all existing links + query = ('SELECT %s FROM %s WHERE media_id = ? AND media_type = ? ' + % (key, link_table)) + self.cursor.execute(query, (kodi_id, kodi_type)) + key_list = self.cursor.fetchall() + # Delete all links + query = ('DELETE FROM %s WHERE media_id = ? AND media_type = ?' + % link_table) + self.cursor.execute(query, (kodi_id, kodi_type)) + # Which countries are now orphaned? + query = 'SELECT %s FROM %s WHERE %s = ?' % (key, link_table, key) + query_delete = 'DELETE FROM %s WHERE %s = ?' % (table, key) + for entry in key_list: + # country_id still in table? + self.cursor.execute(query, (entry[0],)) + if self.cursor.fetchone() is None: + self.cursor.execute(query_delete, (entry[0],)) + def delete_countries(self, kodi_id, kodi_type): """ Assuming that video kodi_id, kodi_type gets deleted, will delete any associated country links in the table country_link and also deletes orphaned countries in the table country """ - # Get all existing links - query = ''' - SELECT country_id FROM country_link - WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - country_ids = self.cursor.fetchall() - # Delete all links - query = 'DELETE FROM country_link WHERE media_id = ? AND media_type = ?' - self.cursor.execute(query, (kodi_id, kodi_type)) - # Which countries are now orphaned? - query = 'SELECT country_id FROM country_link WHERE country_id = ?' - query_delete = 'DELETE FROM country WHERE country_id = ?' - for country_id in country_ids: - # country_id still in table? - self.cursor.execute(query, (country_id,)) - if self.cursor.fetchone() is None: - self.cursor.execute(query_delete, (country_id,)) + self._delete_from_link_and_table(kodi_id, + kodi_type, + 'country_link', + 'country', + 'country_id') def _getactorid(self, name): """ @@ -414,16 +421,16 @@ class KodiDBMethods(object): query_delete = 'DELETE FROM actor WHERE actor_id = ?' # Delete orphaned people for actor_id in actor_ids: - self.cursor.execute(query_actor, (actor_id,)) + self.cursor.execute(query_actor, (actor_id[0],)) if self.cursor.fetchone() is None: - self.cursor.execute(query_director, (actor_id,)) + self.cursor.execute(query_director, (actor_id[0],)) if self.cursor.fetchone() is None: - self.cursor.execute(query_writer, (actor_id,)) + self.cursor.execute(query_writer, (actor_id[0],)) if self.cursor.fetchone() is None: # Delete the person itself from actor table - self.cursor.execute(query_delete, (actor_id,)) + self.cursor.execute(query_delete, (actor_id[0],)) # Delete any associated artwork - self.artwork.deleteArtwork(actor_id, + self.artwork.deleteArtwork(actor_id[0], 'actor', self.cursor) From bad32e90ab7a3afc9933bdf30578ca9daf83f8b5 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:15:17 +0100 Subject: [PATCH 389/509] Delete genres in Kodi DB --- resources/lib/itemtypes.py | 2 ++ resources/lib/kodidb_functions.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 7d9aa578..bc750fc9 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -482,6 +482,7 @@ class Movies(Items): if kodi_type == v.KODI_TYPE_MOVIE: self.kodi_db.delete_countries(kodi_id, kodi_type) self.kodi_db.delete_people(kodi_id, kodi_type) + self.kodi_db.delete_genre(kodi_id, kodi_type) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1205,6 +1206,7 @@ class TVShows(Items): Remove a TV show, and only the show, no seasons or episodes """ kodicursor = self.kodicursor + self.kodi_db.delete_genre(kodi_id, v.KODI_TYPE_SHOW) self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 0007fc9e..02d5e1e9 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -506,6 +506,16 @@ class KodiDBMethods(object): ''' self.cursor.execute(query, (genre_id, kodi_id, kodi_type)) + def delete_genre(self, kodi_id, kodi_type): + """ + Removes the genre links as well as orphaned genres from the Kodi DB + """ + self._delete_from_link_and_table(kodi_id, + kodi_type, + 'genre_link', + 'genre', + 'genre_id') + def addStudios(self, kodiid, studios, mediatype): for studio in studios: query = ' '.join(( From 652f5757cfd9b0e67828ac9b1aec3feb88fa6b90 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:20:44 +0100 Subject: [PATCH 390/509] Delete studios from the Kodi DB --- resources/lib/itemtypes.py | 2 ++ resources/lib/kodidb_functions.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index bc750fc9..2b61d1ab 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -483,6 +483,7 @@ class Movies(Items): self.kodi_db.delete_countries(kodi_id, kodi_type) self.kodi_db.delete_people(kodi_id, kodi_type) self.kodi_db.delete_genre(kodi_id, kodi_type) + self.kodi_db.delete_studios(kodi_id, kodi_type) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1207,6 +1208,7 @@ class TVShows(Items): """ kodicursor = self.kodicursor self.kodi_db.delete_genre(kodi_id, v.KODI_TYPE_SHOW) + self.kodi_db.delete_studios(kodi_id, v.KODI_TYPE_SHOW) self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 02d5e1e9..93b8396c 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -548,6 +548,16 @@ class KodiDBMethods(object): ''') self.cursor.execute(query, (studioid, kodiid, mediatype)) + def delete_studios(self, kodi_id, kodi_type): + """ + Removes the studio links as well as orphaned studios from the Kodi DB + """ + self._delete_from_link_and_table(kodi_id, + kodi_type, + 'studio_link', + 'studio', + 'studio_id') + def addStreams(self, fileid, streamdetails, runtime): # First remove any existing entries From 1a2e8bf6eec566ccfbe4164f1d621ec1d87c0a0b Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:31:45 +0100 Subject: [PATCH 391/509] Delete tags from Kodi DB --- resources/lib/itemtypes.py | 2 ++ resources/lib/kodidb_functions.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 2b61d1ab..3ba7b452 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -484,6 +484,7 @@ class Movies(Items): self.kodi_db.delete_people(kodi_id, kodi_type) self.kodi_db.delete_genre(kodi_id, kodi_type) self.kodi_db.delete_studios(kodi_id, kodi_type) + self.kodi_db.delete_tags(kodi_id, kodi_type) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1209,6 +1210,7 @@ class TVShows(Items): kodicursor = self.kodicursor self.kodi_db.delete_genre(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.delete_studios(kodi_id, v.KODI_TYPE_SHOW) + self.kodi_db.delete_tags(kodi_id, v.KODI_TYPE_SHOW) self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 93b8396c..6205196e 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -858,6 +858,16 @@ class KodiDBMethods(object): for tag in tags: self.addTag(kodiid, tag, mediatype) + def delete_tags(self, kodi_id, kodi_type): + """ + Removes the genre links as well as orphaned genres from the Kodi DB + """ + self._delete_from_link_and_table(kodi_id, + kodi_type, + 'tag_link', + 'tag', + 'tag_id') + def addTag(self, kodiid, tag, mediatype): query = ' '.join(( From 9540e3505c6495e37a91da5fb7cbabbd1dbaf5bc Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 25 Feb 2018 18:42:53 +0100 Subject: [PATCH 392/509] Remove obsolete code --- resources/lib/kodidb_functions.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 6205196e..8015ef33 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -942,29 +942,6 @@ class KodiDBMethods(object): )) self.cursor.execute(query, (kodiid, mediatype, oldtag,)) - def removeTag(self, kodiid, tagname, mediatype): - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tagname,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, tag_id,)) - def addSets(self, movieid, collections, kodicursor): for setname in collections: setid = self.createBoxset(setname) From 769fe8b926cab0fe4769194ca76d8f8bbcd8f01d Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 09:06:35 +0100 Subject: [PATCH 393/509] Delete empty movie sets from Kodi DB --- resources/lib/itemtypes.py | 3 +++ resources/lib/kodidb_functions.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 3ba7b452..31fca00b 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -480,6 +480,7 @@ class Movies(Items): artwork.deleteArtwork(kodi_id, kodi_type, kodicursor) if kodi_type == v.KODI_TYPE_MOVIE: + set_id = self.kodi_db.get_set_id(kodi_id) self.kodi_db.delete_countries(kodi_id, kodi_type) self.kodi_db.delete_people(kodi_id, kodi_type) self.kodi_db.delete_genre(kodi_id, kodi_type) @@ -490,6 +491,8 @@ class Movies(Items): (kodi_id,)) kodicursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) + if set_id: + self.kodi_db.delete_possibly_empty_set(set_id) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, kodi_type) self.kodi_db.remove_ratings(kodi_id, kodi_type) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 8015ef33..f77a09e8 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -990,6 +990,29 @@ class KodiDBMethods(object): )) self.cursor.execute(query, (movieid,)) + def get_set_id(self, kodi_id): + """ + Returns the set_id for the movie with kodi_id or None + """ + query = 'SELECT idSet FROM movie WHERE idMovie = ?' + self.cursor.execute(query, (kodi_id,)) + try: + answ = self.cursor.fetchone()[0] + except TypeError: + answ = None + return answ + + def delete_possibly_empty_set(self, set_id): + """ + Checks whether there are other movies in the set set_id. If not, + deletes the set + """ + query = 'SELECT idSet FROM movie WHERE idSet = ?' + self.cursor.execute(query, (set_id,)) + if self.cursor.fetchone() is None: + query = 'DELETE FROM sets WHERE idSet = ?' + self.cursor.execute(query, (set_id,)) + def addSeason(self, showid, seasonnumber): query = ' '.join(( From 725132131c894c83e873cacb7b668e7dc687908f Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 09:18:44 +0100 Subject: [PATCH 394/509] Delete streamdetails from Kodi DB --- resources/lib/itemtypes.py | 6 ++++-- resources/lib/kodidb_functions.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 31fca00b..36d563e7 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -442,7 +442,7 @@ class Movies(Items): # Process artwork artwork.addArtwork(api.artwork(), movieid, "movie", kodicursor) # Process stream details - self.kodi_db.addStreams(fileid, api.mediastreams(), runtime) + self.kodi_db.modify_streams(fileid, api.mediastreams(), runtime) # Process studios self.kodi_db.addStudios(movieid, studios, "movie") # Process tags: view, Plex collection tags @@ -486,6 +486,7 @@ class Movies(Items): self.kodi_db.delete_genre(kodi_id, kodi_type) self.kodi_db.delete_studios(kodi_id, kodi_type) self.kodi_db.delete_tags(kodi_id, kodi_type) + self.kodi_db.modify_streams(file_id) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1091,7 +1092,7 @@ class TVShows(Items): # Process stream details streams = api.mediastreams() - self.kodi_db.addStreams(fileid, streams, runtime) + self.kodi_db.modify_streams(fileid, streams, runtime) # Process playstates self.kodi_db.addPlaystate(fileid, resume, @@ -1237,6 +1238,7 @@ class TVShows(Items): """ kodicursor = self.kodicursor self.kodi_db.delete_people(kodi_id, v.KODI_TYPE_EPISODE) + self.kodi_db.modify_streams(fileid) self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f77a09e8..08c43a79 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -558,8 +558,11 @@ class KodiDBMethods(object): 'studio', 'studio_id') - def addStreams(self, fileid, streamdetails, runtime): - + def modify_streams(self, fileid, streamdetails=None, runtime=None): + """ + Leave streamdetails and runtime empty to delete all stream entries for + fileid + """ # First remove any existing entries self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) if streamdetails: From e21c16f846a1402ac499d5942b3771af2c2af110 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 09:33:13 +0100 Subject: [PATCH 395/509] Delete playstates from Kodi DB --- resources/lib/itemtypes.py | 8 +++++--- resources/lib/kodidb_functions.py | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 36d563e7..be7c7935 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -487,6 +487,7 @@ class Movies(Items): self.kodi_db.delete_studios(kodi_id, kodi_type) self.kodi_db.delete_tags(kodi_id, kodi_type) self.kodi_db.modify_streams(file_id) + self.kodi_db.delete_playstate(file_id) # Delete kodi movie and file kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) @@ -1232,17 +1233,18 @@ class TVShows(Items): (kodi_id,)) LOG.info("Removed season: %s.", kodi_id) - def removeEpisode(self, kodi_id, fileid): + def removeEpisode(self, kodi_id, file_id): """ Remove an episode, and episode only """ kodicursor = self.kodicursor self.kodi_db.delete_people(kodi_id, v.KODI_TYPE_EPISODE) - self.kodi_db.modify_streams(fileid) + self.kodi_db.modify_streams(file_id) + self.kodi_db.delete_playstate(file_id) self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) + kodicursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.remove_ratings(kodi_id, v.KODI_TYPE_EPISODE) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 08c43a79..b1dd77c5 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -846,6 +846,12 @@ class KodiDBMethods(object): '', 1)) + def delete_playstate(self, file_id): + """ + Removes all playstates/bookmarks for the file with file_id + """ + self.cursor.execute('DELETE FROM bookmark where idFile = ?', (file_id,)) + def addTags(self, kodiid, tags, mediatype): # First, delete any existing tags associated to the id query = ' '.join(( From 5f7426da1cf57408c295e2bc30b00574c1320049 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 10:28:48 +0100 Subject: [PATCH 396/509] Less logging --- resources/lib/artwork.py | 2 +- resources/lib/itemtypes.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index ee626508..9c708f22 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -324,7 +324,7 @@ class Artwork(): (url,)) cachedurl = cursor.fetchone()[0] except TypeError: - LOG.info("Could not find cached url.") + LOG.debug("Could not find cached url.") else: # Delete thumbnail as well as the entry path = translatePath("special://thumbnails/%s" % cachedurl) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index be7c7935..ca769465 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -469,7 +469,7 @@ class Movies(Items): kodi_id = plex_dbitem[0] file_id = plex_dbitem[1] kodi_type = plex_dbitem[4] - LOG.info("Removing %sid: %s file_id: %s", + LOG.debug("Removing %sid: %s file_id: %s", kodi_type, kodi_id, file_id) except TypeError: return @@ -509,7 +509,7 @@ class Movies(Items): # Update plex reference plex_db.updateParentId(plexid, None) kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,)) - LOG.info("Deleted %s %s from kodi database", kodi_type, itemid) + LOG.debug("Deleted %s %s from kodi database", kodi_type, itemid) class TVShows(Items): From b6fc820f81cd4b89a35b97557e1ffcc46183e881 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 10:58:27 +0100 Subject: [PATCH 397/509] Optimize DB access for ratings and unique id --- resources/lib/itemtypes.py | 18 ++++++++++++------ resources/lib/kodidb_functions.py | 16 +++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index ca769465..e349b627 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -360,7 +360,8 @@ class Movies(Items): LOG.info("ADD movie itemid: %s - Title: %s", itemid, title) if v.KODIVERSION >= 17: # add new ratings Kodi 17 - rating_id = self.kodi_db.create_entry_rating() + rating_id = self.kodi_db.get_ratingid(movieid, + v.KODI_TYPE_MOVIE) self.kodi_db.add_ratings(rating_id, movieid, v.KODI_TYPE_MOVIE, @@ -369,7 +370,8 @@ class Movies(Items): votecount) # add new uniqueid Kodi 17 if imdb is not None: - uniqueid = self.kodi_db.create_entry_uniqueid() + uniqueid = self.kodi_db.get_uniqueid(movieid, + v.KODI_TYPE_MOVIE) self.kodi_db.add_uniqueid(uniqueid, movieid, v.KODI_TYPE_MOVIE, @@ -687,7 +689,7 @@ class TVShows(Items): view_id=viewid) if v.KODIVERSION >= 17: # add new ratings Kodi 17 - rating_id = self.kodi_db.create_entry_rating() + rating_id = self.kodi_db.get_ratingid(showid, v.KODI_TYPE_SHOW) self.kodi_db.add_ratings(rating_id, showid, v.KODI_TYPE_SHOW, @@ -696,7 +698,8 @@ class TVShows(Items): votecount) # add new uniqueid Kodi 17 if tvdb is not None: - uniqueid = self.kodi_db.create_entry_uniqueid() + uniqueid = self.kodi_db.get_uniqueid(showid, + v.KODI_TYPE_SHOW) self.kodi_db.add_uniqueid(uniqueid, showid, v.KODI_TYPE_SHOW, @@ -995,7 +998,8 @@ class TVShows(Items): # Create the episode entry if v.KODIVERSION >= 17: # add new ratings Kodi 17 - rating_id = self.kodi_db.create_entry_rating() + rating_id = self.kodi_db.get_ratingid(episodeid, + v.KODI_TYPE_EPISODE) self.kodi_db.add_ratings(rating_id, episodeid, v.KODI_TYPE_EPISODE, @@ -1003,7 +1007,9 @@ class TVShows(Items): rating, votecount) # add new uniqueid Kodi 17 - self.kodi_db.add_uniqueid(self.kodi_db.create_entry_uniqueid(), + uniqueid = self.kodi_db.get_uniqueid(episodeid, + v.KODI_TYPE_EPISODE) + self.kodi_db.add_uniqueid(uniqueid, episodeid, v.KODI_TYPE_EPISODE, tvdb, diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index b1dd77c5..06ed63ec 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1197,11 +1197,6 @@ class KodiDBMethods(object): query = '''UPDATE %s SET userrating = ? WHERE ? = ?''' % kodi_type self.cursor.execute(query, (userrating, ID, kodi_id)) - 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: @@ -1227,7 +1222,9 @@ class KodiDBMethods(object): try: uniqueid = self.cursor.fetchone()[0] except TypeError: - uniqueid = None + self.cursor.execute( + 'SELECT COALESCE(MAX(uniqueid_id),0) FROM uniqueid') + uniqueid = self.cursor.fetchone()[0] + 1 return uniqueid def update_uniqueid(self, *args): @@ -1248,10 +1245,6 @@ class KodiDBMethods(object): ''' self.cursor.execute(query, (kodi_id, kodi_type)) - 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, kodi_id, kodi_type): query = ''' SELECT rating_id FROM rating @@ -1261,7 +1254,8 @@ class KodiDBMethods(object): try: ratingid = self.cursor.fetchone()[0] except TypeError: - ratingid = None + self.cursor.execute('SELECT COALESCE(MAX(rating_id),0) FROM rating') + ratingid = self.cursor.fetchone()[0] + 1 return ratingid def update_ratings(self, *args): From 72d222144acfdb77040b6af291bf4b0803e64169 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 11:20:11 +0100 Subject: [PATCH 398/509] Make sure obsolete uniqueid entries get deleted --- resources/lib/itemtypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index e349b627..c6f70e96 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -326,6 +326,7 @@ class Movies(Items): "imdb", uniqueid) else: + self.kodi_db.remove_uniqueid(movieid, v.KODI_TYPE_MOVIE) uniqueid = -1 query = ''' UPDATE movie From 8e2aaa6c092f1d4a0dc126209fe2f3d69e68ee66 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 26 Feb 2018 11:22:18 +0100 Subject: [PATCH 399/509] Make sure obsolete show uniqueids get deleted --- resources/lib/itemtypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index c6f70e96..586f9028 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -640,6 +640,7 @@ class TVShows(Items): "unknown", uniqueid) else: + self.kodi_db.remove_uniqueid(showid, v.KODI_TYPE_SHOW) uniqueid = -1 # Update the tvshow entry query = ''' From 82ed5afb02b75147939bf61f3ef01ec5a5a48014 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 27 Feb 2018 21:14:42 +0100 Subject: [PATCH 400/509] Further optimize DB access --- resources/lib/itemtypes.py | 8 +-- resources/lib/kodidb_functions.py | 97 +++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 586f9028..021acb66 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -437,23 +437,23 @@ class Movies(Items): kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process countries - self.kodi_db.addCountries(movieid, countries, "movie") + self.kodi_db.modify_countries(movieid, v.KODI_TYPE_MOVIE, countries) # Process cast self.kodi_db.addPeople(movieid, api.people_list(), "movie") # Process genres - self.kodi_db.addGenres(movieid, genres, "movie") + self.kodi_db.modify_genres(movieid, v.KODI_TYPE_MOVIE, genres) # Process artwork artwork.addArtwork(api.artwork(), movieid, "movie", kodicursor) # Process stream details self.kodi_db.modify_streams(fileid, api.mediastreams(), runtime) # Process studios - self.kodi_db.addStudios(movieid, studios, "movie") + self.kodi_db.modify_studios(movieid, v.KODI_TYPE_MOVIE, studios) # Process tags: view, Plex collection tags tags = [viewtag] tags.extend(collections) if userdata['Favorite']: tags.append("Favorite movies") - self.kodi_db.addTags(movieid, tags, "movie") + self.kodi_db.modify_tags(movieid, v.KODI_TYPE_MOVIE, tags) # Add any sets from Plex collection tags self.kodi_db.addSets(movieid, collections, kodicursor) # Process playstates diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 06ed63ec..8bf94ec7 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -239,6 +239,103 @@ class KodiDBMethods(object): )) self.cursor.execute(query, (pathid, filename,)) + def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, + table, key): + query = ''' + SELECT %s FROM %s WHERE name = ? COLLATE NOCASE LIMIT 1 + ''' % (key, table) + query_id = 'SELECT COALESCE(MAX(%s),0) FROM %s' % (key, table) + query_new = ('INSERT INTO %s(%s, name) values(?, ?)' + % (table, key)) + entry_ids = [] + for entry in entries: + self.cursor.execute(query, (entry,)) + try: + entry_id = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute(query_id) + entry_id = self.cursor.fetchone()[0] + 1 + self.cursor.execute(query_new, (entry_id, entry)) + finally: + entry_ids.append(entry_id) + # Now process the ids obtained from the names + # Get the existing, old entries + query = ('SELECT %s FROM %s WHERE media_id = ? AND media_type = ?' + % (key, link_table)) + self.cursor.execute(query, (kodi_id, kodi_type)) + old_entries = self.cursor.fetchall() + outdated_entries = [] + for entry_id in old_entries: + try: + entry_ids.remove(entry_id) + except ValueError: + outdated_entries.append(entry_id) + # Add all new entries that haven't already been added + query = 'INSERT INTO %s VALUES (?, ?, ?)' % link_table + for entry_id in entry_ids: + self.cursor.execute(query, (entry_id, kodi_id, kodi_type)) + # Delete all outdated references in the link table. Also check whether + # we need to delete orphaned entries in the master table + query = ''' + DELETE FROM %s WHERE %s = ? AND media_id = ? AND media_type = ? + ''' % (link_table, key) + query_rem = 'SELECT %s FROM %s WHERE %s = ?' % (key, link_table, key) + query_delete = 'DELETE FROM %s WHERE %s = ?' % (table, key) + for entry_id in outdated_entries: + self.cursor.execute(query, (entry_id, kodi_id, kodi_type)) + self.cursor.execute(query_rem, (entry_id,)) + if self.cursor.fetchone() is None: + # Delete in the original table because entry is now orphaned + self.cursor.execute(query_delete, (entry_id,)) + + def modify_countries(self, kodi_id, kodi_type, countries): + """ + Writes a country (string) in the list countries into the Kodi DB. Will + also delete any orphaned country entries. + """ + self._modify_link_and_table(kodi_id, + kodi_type, + countries, + 'country_link', + 'country', + 'country_id') + + def modify_genres(self, kodi_id, kodi_type, genres): + """ + Writes a country (string) in the list countries into the Kodi DB. Will + also delete any orphaned country entries. + """ + self._modify_link_and_table(kodi_id, + kodi_type, + genres, + 'genre_link', + 'genre', + 'genre_id') + + def modify_studios(self, kodi_id, kodi_type, studios): + """ + Writes a country (string) in the list countries into the Kodi DB. Will + also delete any orphaned country entries. + """ + self._modify_link_and_table(kodi_id, + kodi_type, + studios, + 'studio_link', + 'studio', + 'studio_id') + + def modify_tags(self, kodi_id, kodi_type, tags): + """ + Writes a country (string) in the list countries into the Kodi DB. Will + also delete any orphaned country entries. + """ + self._modify_link_and_table(kodi_id, + kodi_type, + tags, + 'tag_link', + 'tag', + 'tag_id') + def addCountries(self, kodiid, countries, mediatype): for country in countries: query = ' '.join(( From db3be4cf098ebdcdeca68862da98b22092967d8d Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 13:45:08 +0100 Subject: [PATCH 401/509] Fix IntegrityError --- resources/lib/kodidb_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 8bf94ec7..2f134c35 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -267,9 +267,9 @@ class KodiDBMethods(object): outdated_entries = [] for entry_id in old_entries: try: - entry_ids.remove(entry_id) + entry_ids.remove(entry_id[0]) except ValueError: - outdated_entries.append(entry_id) + outdated_entries.append(entry_id[0]) # Add all new entries that haven't already been added query = 'INSERT INTO %s VALUES (?, ?, ?)' % link_table for entry_id in entry_ids: From f0bbcb508619d5e294e86fabcc42b401e5135083 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 13:45:34 +0100 Subject: [PATCH 402/509] Start Kodi ids at 0, not 1 --- resources/lib/kodidb_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 2f134c35..01f1fbb8 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -244,7 +244,7 @@ class KodiDBMethods(object): query = ''' SELECT %s FROM %s WHERE name = ? COLLATE NOCASE LIMIT 1 ''' % (key, table) - query_id = 'SELECT COALESCE(MAX(%s),0) FROM %s' % (key, table) + query_id = 'SELECT COALESCE(MAX(%s), -1) FROM %s' % (key, table) query_new = ('INSERT INTO %s(%s, name) values(?, ?)' % (table, key)) entry_ids = [] From f6336feb726bcf5ece250b081908e02b319d9f1d Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 13:45:53 +0100 Subject: [PATCH 403/509] Increase logging --- resources/lib/kodidb_functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 01f1fbb8..aa650bb8 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -240,7 +240,7 @@ class KodiDBMethods(object): self.cursor.execute(query, (pathid, filename,)) def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, - table, key): + table, key): query = ''' SELECT %s FROM %s WHERE name = ? COLLATE NOCASE LIMIT 1 ''' % (key, table) @@ -255,6 +255,7 @@ class KodiDBMethods(object): except TypeError: self.cursor.execute(query_id) entry_id = self.cursor.fetchone()[0] + 1 + LOG.debug('Adding %s: %s with id %s', table, entry, entry_id) self.cursor.execute(query_new, (entry_id, entry)) finally: entry_ids.append(entry_id) @@ -286,6 +287,7 @@ class KodiDBMethods(object): self.cursor.execute(query_rem, (entry_id,)) if self.cursor.fetchone() is None: # Delete in the original table because entry is now orphaned + LOG.debug('Deleting %s from Kodi DB: %s', table, entry_id) self.cursor.execute(query_delete, (entry_id,)) def modify_countries(self, kodi_id, kodi_type, countries): From f4681011b9bb32c4df7d65aa3491631b216908a0 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 17:24:32 +0100 Subject: [PATCH 404/509] Big Kodi DB overhaul - ensure video metadata updates/deletes correctly --- resources/lib/PlexAPI.py | 47 +-- resources/lib/itemtypes.py | 46 ++- resources/lib/kodidb_functions.py | 579 ++++++++---------------------- 3 files changed, 199 insertions(+), 473 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index e83293f2..b9d7e6a8 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -53,13 +53,6 @@ LOG = getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') -# Key of library: Plex-identifier. Value represents the Kodi/emby side -PEOPLE_OF_INTEREST = { - 'Director': 'Director', - 'Writer': 'Writer', - 'Role': 'Actor', - 'Producer': 'Producer' -} # we need to use a little mapping between fanart.tv arttypes and kodi # artttypes FANART_TV_TYPES = [ @@ -326,25 +319,35 @@ class API(object): def people_list(self): """ - Returns a list of people from item, with a list item of the form + Returns a dict with lists of tuples: { - 'Name': xxx, - 'Type': xxx, - 'Id': xxx - 'imageurl': url to picture, None otherwise - ('Role': xxx for cast/actors only, None if not found) + 'actor': [..., (, , , ), ...], + 'director': [..., (, ), ...], + 'writer': [..., (, ), ...] } + Everything in unicode, except which is an int. + Only and may be None if not found. + + Kodi does not yet support a Producer. People may appear several times + per category and overall! """ - people = [] + people = { + 'actor': [], + 'director': [], + 'writer': [] + } + cast_order = 0 for child in self.item: - if child.tag in PEOPLE_OF_INTEREST: - people.append({ - 'Name': child.attrib['tag'], - 'Type': PEOPLE_OF_INTEREST[child.tag], - 'Id': child.attrib['id'], - 'imageurl': child.get('thumb'), - 'Role': child.get('role') - }) + if child.tag == 'Role': + people['actor'].append((child.attrib['tag'], + child.get('thumb'), + child.get('role'), + cast_order)) + cast_order += 1 + elif child.tag == 'Writer': + people['writer'].append((child.attrib['tag'], )) + elif child.tag == 'Director': + people['director'].append((child.attrib['tag'], )) return people def genre_list(self): diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 021acb66..cbaa77a7 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -439,7 +439,9 @@ class Movies(Items): # Process countries self.kodi_db.modify_countries(movieid, v.KODI_TYPE_MOVIE, countries) # Process cast - self.kodi_db.addPeople(movieid, api.people_list(), "movie") + self.kodi_db.modify_people(movieid, + v.KODI_TYPE_MOVIE, + api.people_list()) # Process genres self.kodi_db.modify_genres(movieid, v.KODI_TYPE_MOVIE, genres) # Process artwork @@ -472,8 +474,8 @@ class Movies(Items): kodi_id = plex_dbitem[0] file_id = plex_dbitem[1] kodi_type = plex_dbitem[4] - LOG.debug("Removing %sid: %s file_id: %s", - kodi_type, kodi_id, file_id) + LOG.debug('Removing %sid: %s file_id: %s', + kodi_type, kodi_id, file_id) except TypeError: return @@ -484,11 +486,11 @@ class Movies(Items): if kodi_type == v.KODI_TYPE_MOVIE: set_id = self.kodi_db.get_set_id(kodi_id) - self.kodi_db.delete_countries(kodi_id, kodi_type) - self.kodi_db.delete_people(kodi_id, kodi_type) - self.kodi_db.delete_genre(kodi_id, kodi_type) - self.kodi_db.delete_studios(kodi_id, kodi_type) - self.kodi_db.delete_tags(kodi_id, kodi_type) + self.kodi_db.modify_countries(kodi_id, kodi_type) + self.kodi_db.modify_people(kodi_id, kodi_type) + self.kodi_db.modify_genres(kodi_id, kodi_type) + self.kodi_db.modify_studios(kodi_id, kodi_type) + self.kodi_db.modify_tags(kodi_id, kodi_type) self.kodi_db.modify_streams(file_id) self.kodi_db.delete_playstate(file_id) # Delete kodi movie and file @@ -739,20 +741,15 @@ class TVShows(Items): ''' kodicursor.execute(query, (path, None, None, 1, toppathid, pathid)) - # Process cast - people = api.people_list() - self.kodi_db.addPeople(showid, people, "tvshow") - # Process genres - self.kodi_db.addGenres(showid, genres, "tvshow") - # Process artwork - allartworks = api.artwork() - artwork.addArtwork(allartworks, showid, "tvshow", kodicursor) + self.kodi_db.modify_people(showid, v.KODI_TYPE_SHOW, api.people_list()) + self.kodi_db.modify_genres(showid, v.KODI_TYPE_SHOW, genres) + artwork.addArtwork(api.artwork(), showid, v.KODI_TYPE_SHOW, kodicursor) # Process studios - self.kodi_db.addStudios(showid, studios, "tvshow") + self.kodi_db.modify_studios(showid, v.KODI_TYPE_SHOW, studios) # Process tags: view, PMS collection tags tags = [viewtag] tags.extend(collections) - self.kodi_db.addTags(showid, tags, "tvshow") + self.kodi_db.modify_tags(showid, v.KODI_TYPE_SHOW, tags) @catch_exceptions(warnuser=True) def add_updateSeason(self, item, viewtag=None, viewid=None): @@ -1088,8 +1085,9 @@ class TVShows(Items): )) kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process cast - people = api.people_list() - self.kodi_db.addPeople(episodeid, people, "episode") + self.kodi_db.modify_people(episodeid, + v.KODI_TYPE_EPISODE, + api.people_list()) # Process artwork # Wide "screenshot" of particular episode poster = item.attrib.get('thumb') @@ -1221,9 +1219,9 @@ class TVShows(Items): Remove a TV show, and only the show, no seasons or episodes """ kodicursor = self.kodicursor - self.kodi_db.delete_genre(kodi_id, v.KODI_TYPE_SHOW) - self.kodi_db.delete_studios(kodi_id, v.KODI_TYPE_SHOW) - self.kodi_db.delete_tags(kodi_id, v.KODI_TYPE_SHOW) + self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_SHOW) + self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_SHOW) + self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW) self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: @@ -1246,7 +1244,7 @@ class TVShows(Items): Remove an episode, and episode only """ kodicursor = self.kodicursor - self.kodi_db.delete_people(kodi_id, v.KODI_TYPE_EPISODE) + self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.modify_streams(file_id) self.kodi_db.delete_playstate(file_id) self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index aa650bb8..18ecdba9 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -3,6 +3,7 @@ ############################################################################### from logging import getLogger from ntpath import dirname +from sqlite3 import IntegrityError import artwork from utils import kodi_sql, try_decode @@ -287,14 +288,15 @@ class KodiDBMethods(object): self.cursor.execute(query_rem, (entry_id,)) if self.cursor.fetchone() is None: # Delete in the original table because entry is now orphaned - LOG.debug('Deleting %s from Kodi DB: %s', table, entry_id) + LOG.debug('Removing %s from Kodi DB: %s', table, entry_id) self.cursor.execute(query_delete, (entry_id,)) - def modify_countries(self, kodi_id, kodi_type, countries): + def modify_countries(self, kodi_id, kodi_type, countries=None): """ Writes a country (string) in the list countries into the Kodi DB. Will also delete any orphaned country entries. """ + countries = countries if countries else [] self._modify_link_and_table(kodi_id, kodi_type, countries, @@ -302,11 +304,12 @@ class KodiDBMethods(object): 'country', 'country_id') - def modify_genres(self, kodi_id, kodi_type, genres): + def modify_genres(self, kodi_id, kodi_type, genres=None): """ Writes a country (string) in the list countries into the Kodi DB. Will also delete any orphaned country entries. """ + genres = genres if genres else [] self._modify_link_and_table(kodi_id, kodi_type, genres, @@ -314,11 +317,12 @@ class KodiDBMethods(object): 'genre', 'genre_id') - def modify_studios(self, kodi_id, kodi_type, studios): + def modify_studios(self, kodi_id, kodi_type, studios=None): """ Writes a country (string) in the list countries into the Kodi DB. Will also delete any orphaned country entries. """ + studios = studios if studios else [] self._modify_link_and_table(kodi_id, kodi_type, studios, @@ -326,11 +330,12 @@ class KodiDBMethods(object): 'studio', 'studio_id') - def modify_tags(self, kodi_id, kodi_type, tags): + def modify_tags(self, kodi_id, kodi_type, tags=None): """ Writes a country (string) in the list countries into the Kodi DB. Will also delete any orphaned country entries. """ + tags = tags if tags else [] self._modify_link_and_table(kodi_id, kodi_type, tags, @@ -338,201 +343,123 @@ class KodiDBMethods(object): 'tag', 'tag_id') - def addCountries(self, kodiid, countries, mediatype): - for country in countries: - query = ' '.join(( - - "SELECT country_id", - "FROM country", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) + def modify_people(self, kodi_id, kodi_type, people=None): + """ + Makes sure that actors, directors and writers are recorded correctly + for the elmement kodi_id, kodi_type. + Will also delete a freshly orphaned actor entry. + """ + people = people if people else {'actor': [], + 'director': [], + 'writer': []} + for kind, people_list in people.iteritems(): + self._modify_people_kind(kodi_id, kodi_type, kind, people_list) + def _modify_people_kind(self, kodi_id, kodi_type, kind, people_list): + # Get the people already saved in the DB for this specific item + if kind == 'actor': + query = ''' + SELECT actor.actor_id, actor.name, art.url, actor_link.role, + actor_link.cast_order + FROM actor_link + LEFT JOIN actor ON actor.actor_id = actor_link.actor_id + LEFT JOIN art ON (art.media_id = actor_link.actor_id AND + art.media_type = 'actor') + WHERE actor_link.media_id = ? AND actor_link.media_type = ? + ''' + else: + query = ''' + SELECT actor.actor_id, actor.name + FROM {0}_link + LEFT JOIN actor ON actor.actor_id = {0}_link.actor_id + WHERE {0}_link.media_id = ? AND {0}_link.media_type = ? + '''.format(kind) + self.cursor.execute(query, (kodi_id, kodi_type)) + old_people = self.cursor.fetchall() + # Determine which people we need to save or delete + outdated_people = [] + for person in old_people: try: - country_id = self.cursor.fetchone()[0] + people_list.remove(person[1:]) + except ValueError: + outdated_people.append(person) + # Get rid of old entries + query = ''' + DELETE FROM %s_link + WHERE actor_id = ? AND media_id = ? AND media_type = ? + ''' % kind + query_actor_check = 'SELECT actor_id FROM %s_link WHERE actor_id = ?' + query_actor_delete = 'DELETE FROM actor WHERE actor_id = ?' + for person in outdated_people: + # Delete the outdated entry + self.cursor.execute(query, (person[0], kodi_id, kodi_type)) + # Do we now have orphaned entries? + for person_kind in ('actor', 'writer', 'director'): + self.cursor.execute(query_actor_check % person_kind, + (person[0],)) + if self.cursor.fetchone() is not None: + break + else: + # person entry in actor table is now orphaned + # Delete the person from actor table + LOG.debug('Removing person from Kodi DB: %s', person) + self.cursor.execute(query_actor_delete, (person[0],)) + if kind == 'actor': + # Delete any associated artwork + self.artwork.deleteArtwork(person[0], 'actor', self.cursor) + # Save new people to Kodi DB by iterating over the remaining entries + if kind == 'actor': + query = 'INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)' + for person in people_list: + LOG.debug('Adding actor to Kodi DB: %s', person) + # Make sure the person entry in table actor exists + actor_id = self._get_actor_id(person[0], art_url=person[1]) + # Link the person with the media element + try: + self.cursor.execute(query, (actor_id, kodi_id, kodi_type, + person[2], person[3])) + except IntegrityError: + # With Kodi, an actor may have only one role, unlike Plex + pass + else: + query = 'INSERT INTO %s_link VALUES (?, ?, ?)' % kind + for person in people_list: + LOG.debug('Adding %s to Kodi DB: %s', kind, person[0]) + # Make sure the person entry in table actor exists: + actor_id = self._get_actor_id(person[0]) + # Link the person with the media element + try: + self.cursor.execute(query, (actor_id, kodi_id, kodi_type)) + except IntegrityError: + # Again, Kodi may have only one person assigned to a role + pass - except TypeError: - # Country entry does not exists - self.cursor.execute("select coalesce(max(country_id),0) from country") - country_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(country_id, name) values(?, ?)" - self.cursor.execute(query, (country_id, country)) - LOG.debug("Add country to media, processing: %s", country) - - finally: # Assign country to content - query = ( - ''' - INSERT OR REPLACE INTO country_link( - country_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (country_id, kodiid, mediatype)) - - def _delete_from_link_and_table(self, kodi_id, kodi_type, link_table, - table, key): - # Get all existing links - query = ('SELECT %s FROM %s WHERE media_id = ? AND media_type = ? ' - % (key, link_table)) - self.cursor.execute(query, (kodi_id, kodi_type)) - key_list = self.cursor.fetchall() - # Delete all links - query = ('DELETE FROM %s WHERE media_id = ? AND media_type = ?' - % link_table) - self.cursor.execute(query, (kodi_id, kodi_type)) - # Which countries are now orphaned? - query = 'SELECT %s FROM %s WHERE %s = ?' % (key, link_table, key) - query_delete = 'DELETE FROM %s WHERE %s = ?' % (table, key) - for entry in key_list: - # country_id still in table? - self.cursor.execute(query, (entry[0],)) - if self.cursor.fetchone() is None: - self.cursor.execute(query_delete, (entry[0],)) - - def delete_countries(self, kodi_id, kodi_type): + def _get_actor_id(self, name, art_url=None): """ - Assuming that video kodi_id, kodi_type gets deleted, will delete any - associated country links in the table country_link and also deletes - orphaned countries in the table country - """ - self._delete_from_link_and_table(kodi_id, - kodi_type, - 'country_link', - 'country', - 'country_id') + Returns the actor_id [int] for name [unicode] in table actor (without + ensuring that the name matches). + If not, will create a new record with actor_id, name, art_url - def _getactorid(self, name): + Uses Plex ids and thus assumes that Plex person id is unique! """ - Crucial für sync speed! - """ - query = ' '.join(( - "SELECT actor_id", - "FROM actor", - "WHERE name = ?", - "LIMIT 1" - )) - self.cursor.execute(query, (name,)) + self.cursor.execute('SELECT actor_id FROM actor WHERE name=? LIMIT 1', + (name,)) try: - actorid = self.cursor.fetchone()[0] + actor_id = 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 = 0 - for person in people: - actorid = self._getactorid(person['Name']) - # Link person to content - castorder = self._addPerson(person.get('Role'), - person['Type'], - actorid, - kodiid, - mediatype, - castorder) - # Add person image to art table - if person['imageurl']: - self.artwork.addOrUpdateArt(person['imageurl'], actorid, - person['Type'].lower(), "thumb", + # Not yet in actor DB, add person + self.cursor.execute('SELECT COALESCE(MAX(actor_id),-1) FROM actor') + actor_id = self.cursor.fetchone()[0] + 1 + self.cursor.execute('INSERT INTO actor(actor_id, name) ' + 'VALUES (?, ?)', + (actor_id, name)) + if art_url: + self.artwork.addOrUpdateArt(art_url, + actor_id, + 'actor', + "thumb", self.cursor) - - def delete_people(self, kodi_id, kodi_type): - """ - Assuming that the video kodi_id, kodi_type gets deleted, will delete any - associated actor_, director_, writer_links and also deletes - orphaned actors - """ - # Actors - query = ''' - SELECT actor_id FROM actor_link - WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - actor_ids = self.cursor.fetchall() - query = 'DELETE FROM actor_link WHERE media_id = ? AND media_type = ?' - self.cursor.execute(query, (kodi_id, kodi_type)) - # Directors - query = ''' - SELECT actor_id FROM director_link - WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - actor_ids.extend(self.cursor.fetchall()) - query = ''' - DELETE FROM director_link WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - # Writers - query = ''' - SELECT actor_id FROM writer_link - WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - actor_ids.extend(self.cursor.fetchall()) - query = ''' - DELETE FROM writer_link WHERE media_id = ? AND media_type = ? - ''' - self.cursor.execute(query, (kodi_id, kodi_type)) - # Which people are now orphaned? - query_actor = 'SELECT actor_id FROM actor_link WHERE actor_id = ?' - query_director = 'SELECT actor_id FROM director_link WHERE actor_id = ?' - query_writer = 'SELECT actor_id FROM writer_link WHERE actor_id = ?' - query_delete = 'DELETE FROM actor WHERE actor_id = ?' - # Delete orphaned people - for actor_id in actor_ids: - self.cursor.execute(query_actor, (actor_id[0],)) - if self.cursor.fetchone() is None: - self.cursor.execute(query_director, (actor_id[0],)) - if self.cursor.fetchone() is None: - self.cursor.execute(query_writer, (actor_id[0],)) - if self.cursor.fetchone() is None: - # Delete the person itself from actor table - self.cursor.execute(query_delete, (actor_id[0],)) - # Delete any associated artwork - self.artwork.deleteArtwork(actor_id[0], - 'actor', - self.cursor) - + return actor_id def existingArt(self, kodiId, mediaType, refresh=False): """ @@ -576,134 +503,46 @@ class KodiDBMethods(object): result['Backdrop'] = [d[0] for d in data] return result - def addGenres(self, kodi_id, genres, kodi_type): - """ - Adds the genres (list of strings) to the Kodi DB and associates them - with the element kodi_id, kodi_type - """ - # Delete current genres for clean slate - query = 'DELETE FROM genre_link WHERE media_id = ? AND media_type = ?' - self.cursor.execute(query, (kodi_id, kodi_type,)) - # Add genres - for genre in genres: - query = ' SELECT genre_id FROM genre WHERE name = ? COLLATE NOCASE' - self.cursor.execute(query, (genre,)) - try: - genre_id = self.cursor.fetchone()[0] - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(genre_id),0) from genre") - genre_id = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(genre_id, name) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - finally: - # Assign genre to item - query = ''' - INSERT OR REPLACE INTO genre_link( - genre_id, media_id, media_type) - VALUES (?, ?, ?) - ''' - self.cursor.execute(query, (genre_id, kodi_id, kodi_type)) - - def delete_genre(self, kodi_id, kodi_type): - """ - Removes the genre links as well as orphaned genres from the Kodi DB - """ - self._delete_from_link_and_table(kodi_id, - kodi_type, - 'genre_link', - 'genre', - 'genre_id') - - def addStudios(self, kodiid, studios, mediatype): - for studio in studios: - query = ' '.join(( - - "SELECT studio_id", - "FROM studio", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studioid = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(studio_id),0) from studio") - studioid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(studio_id, name) values(?, ?)" - self.cursor.execute(query, (studioid, studio)) - LOG.debug("Add Studios to media, processing: %s", studio) - - finally: # Assign studio to item - query = ( - ''' - INSERT OR REPLACE INTO studio_link( - studio_id, media_id, media_type) - - VALUES (?, ?, ?) - ''') - self.cursor.execute(query, (studioid, kodiid, mediatype)) - - def delete_studios(self, kodi_id, kodi_type): - """ - Removes the studio links as well as orphaned studios from the Kodi DB - """ - self._delete_from_link_and_table(kodi_id, - kodi_type, - 'studio_link', - 'studio', - 'studio_id') - def modify_streams(self, fileid, streamdetails=None, runtime=None): """ Leave streamdetails and runtime empty to delete all stream entries for fileid """ # First remove any existing entries - self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) - if streamdetails: - # Video details - for videotrack in streamdetails['video']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strVideoCodec, fVideoAspect, - iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 0, videotrack['codec'], - videotrack['aspect'], videotrack['width'], videotrack['height'], - runtime ,videotrack['video3DFormat'])) - - # Audio details - for audiotrack in streamdetails['audio']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 1, audiotrack['codec'], - audiotrack['channels'], audiotrack['language'])) - - # Subtitles details - for subtitletrack in streamdetails['subtitle']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strSubtitleLanguage) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 2, subtitletrack)) + self.cursor.execute('DELETE FROM streamdetails WHERE idFile = ?', + (fileid,)) + if not streamdetails: + return + for videotrack in streamdetails['video']: + query = ''' + INSERT INTO streamdetails( + idFile, iStreamType, strVideoCodec, fVideoAspect, + iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, + (fileid, 0, videotrack['codec'], + videotrack['aspect'], videotrack['width'], + videotrack['height'], runtime, + videotrack['video3DFormat'])) + for audiotrack in streamdetails['audio']: + query = ''' + INSERT INTO streamdetails( + idFile, iStreamType, strAudioCodec, iAudioChannels, + strAudioLanguage) + VALUES (?, ?, ?, ?, ?) + ''' + self.cursor.execute(query, + (fileid, 1, audiotrack['codec'], + audiotrack['channels'], + audiotrack['language'])) + for subtitletrack in streamdetails['subtitle']: + query = ''' + INSERT INTO streamdetails(idFile, iStreamType, + strSubtitleLanguage) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (fileid, 2, subtitletrack)) def resume_points(self): """ @@ -722,26 +561,6 @@ class KodiDBMethods(object): ids.append(row[0]) return ids - def getUnplayedMusicItems(self): - """ - MUSIC - - Returns all Kodi Item idFile that have not yet been completely played - """ - query = ' '.join(( - "SELECT idSong", - "FROM song", - "WHERE iTimesPlayed = ?" - )) - try: - rows = self.cursor.execute(query, (0, )) - except: - return [] - ids = [] - for row in rows: - ids.append(row[0]) - return ids - def video_id_from_filename(self, filename, path): """ Returns the tuple (itemId, type) where @@ -846,46 +665,6 @@ class KodiDBMethods(object): return return song_id[0] - def getUnplayedItems(self): - """ - VIDEOS - - Returns all Kodi Item idFile that have not yet been completely played - """ - query = ' '.join(( - "SELECT idFile", - "FROM files", - "WHERE playCount IS NULL OR playCount = ''" - )) - try: - rows = self.cursor.execute(query) - except: - return [] - ids = [] - for row in rows: - ids.append(row[0]) - return ids - - def getVideoRuntime(self, kodiid, mediatype): - if mediatype == v.KODI_TYPE_MOVIE: - query = ' '.join(( - "SELECT c11", - "FROM movie", - "WHERE idMovie = ?", - )) - elif mediatype == v.KODI_TYPE_EPISODE: - query = ' '.join(( - "SELECT c09", - "FROM episode", - "WHERE idEpisode = ?", - )) - self.cursor.execute(query, (kodiid,)) - try: - runtime = self.cursor.fetchone()[0] - except TypeError: - return None - return int(runtime) - def get_resume(self, file_id): """ Returns the first resume point in seconds (int) if found, else None for @@ -951,60 +730,6 @@ class KodiDBMethods(object): """ self.cursor.execute('DELETE FROM bookmark where idFile = ?', (file_id,)) - def addTags(self, kodiid, tags, mediatype): - # First, delete any existing tags associated to the id - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype)) - - # Add tags - LOG.debug("Adding Tags: %s", tags) - for tag in tags: - self.addTag(kodiid, tag, mediatype) - - def delete_tags(self, kodi_id, kodi_type): - """ - Removes the genre links as well as orphaned genres from the Kodi DB - """ - self._delete_from_link_and_table(kodi_id, - kodi_type, - 'tag_link', - 'tag', - 'tag_id') - - def addTag(self, kodiid, tag, mediatype): - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag, because it does not exist - tag_id = self.createTag(tag) - LOG.debug("Adding tag: %s", tag) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO tag_link( - tag_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodiid, mediatype)) - def createTag(self, name): # This will create and return the tag_id query = ' '.join(( From ae15030bb57205168556b768b38ca5d55b121de5 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 17:42:21 +0100 Subject: [PATCH 405/509] Less logging --- resources/lib/artwork.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 9c708f22..4435eca5 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -324,7 +324,8 @@ class Artwork(): (url,)) cachedurl = cursor.fetchone()[0] except TypeError: - LOG.debug("Could not find cached url.") + # Could not find cached url + pass else: # Delete thumbnail as well as the entry path = translatePath("special://thumbnails/%s" % cachedurl) From f31046bed18b7c3a79696c90c808f71c7c9bf836 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 28 Feb 2018 18:48:39 +0100 Subject: [PATCH 406/509] Greatly speed up switch of PMS --- .../resource.language.en_gb/strings.po | 4 - resources/lib/entrypoint.py | 8 +- resources/lib/utils.py | 97 ++++++++++--------- 3 files changed, 53 insertions(+), 56 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index b1a9193b..939dfbcd 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1851,10 +1851,6 @@ msgctxt "#39601" msgid "Could not stop the database from running. Please try again later." msgstr "" -msgctxt "#39602" -msgid "Remove all cached artwork? (recommended!)" -msgstr "" - msgctxt "#39603" msgid "Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended and unnecessary!)" msgstr "" diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index dd228719..6bb54d2d 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -53,11 +53,9 @@ def chooseServer(): if not __LogOut(): return - from utils import delete_playlists, delete_nodes - # First remove playlists - delete_playlists() - # Remove video nodes - delete_nodes() + from utils import wipe_database + # Wipe Kodi and Plex database as well as playlists and video nodes + wipe_database() # Log in again __LogIn() diff --git a/resources/lib/utils.py b/resources/lib/utils.py index d73acf09..3c3ac8b6 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -368,34 +368,14 @@ def create_actor_db_index(): conn.close() -def reset(): +def wipe_database(): """ - User navigated to the PKC settings, Advanced, and wants to reset the Kodi - database and possibly PKC entirely + Deletes all Plex playlists as well as video nodes, then clears Kodi as well + as Plex databases completely. + Will also delete all cached artwork. """ - # Are you sure you want to reset your local Kodi database? - if not dialog('yesno', - heading='{plex} %s ' % language(30132), - line1=language(39600)): - return - - # first stop any db sync - plex_command('STOP_SYNC', 'True') - count = 10 - while window('plex_dbScan') == "true": - LOG.debug("Sync is running, will retry: %s...", count) - count -= 1 - if count == 0: - # Could not stop the database from running. Please try again later. - dialog('ok', - heading='{plex} %s' % language(30132), - line1=language(39601)) - return - xbmc.sleep(1000) - # Clean up the playlists delete_playlists() - # Clean up the video nodes delete_nodes() @@ -435,36 +415,59 @@ def reset(): tablename = row[0] if tablename != "version": cursor.execute("DELETE FROM %s" % tablename) - cursor.execute('DROP table IF EXISTS plex') - cursor.execute('DROP table IF EXISTS view') connection.commit() cursor.close() - # Remove all cached artwork? (recommended!) - if dialog('yesno', - heading='{plex} %s ' % language(30132), - line1=language(39602)): - LOG.info("Resetting all cached artwork.") - # Remove all existing textures first - path = xbmc.translatePath("special://thumbnails/") - if exists(path): - rmtree(try_decode(path), ignore_errors=True) - # remove all existing data from texture DB - connection = kodi_sql('texture') - cursor = connection.cursor() - query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' - cursor.execute(query, ("table", )) - rows = cursor.fetchall() - for row in rows: - table_name = row[0] - if table_name != "version": - cursor.execute("DELETE FROM %s" % table_name) - connection.commit() - cursor.close() + LOG.info("Resetting all cached artwork.") + # Remove all existing textures first + path = xbmc.translatePath("special://thumbnails/") + if exists(path): + rmtree(try_decode(path), ignore_errors=True) + # remove all existing data from texture DB + connection = kodi_sql('texture') + cursor = connection.cursor() + query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' + cursor.execute(query, ("table", )) + rows = cursor.fetchall() + for row in rows: + table_name = row[0] + if table_name != "version": + cursor.execute("DELETE FROM %s" % table_name) + connection.commit() + cursor.close() # reset the install run flag settings('SyncInstallRunDone', value="false") + +def reset(): + """ + User navigated to the PKC settings, Advanced, and wants to reset the Kodi + database and possibly PKC entirely + """ + # Are you sure you want to reset your local Kodi database? + if not dialog('yesno', + heading='{plex} %s ' % language(30132), + line1=language(39600)): + return + + # first stop any db sync + plex_command('STOP_SYNC', 'True') + count = 10 + while window('plex_dbScan') == "true": + LOG.debug("Sync is running, will retry: %s...", count) + count -= 1 + if count == 0: + # Could not stop the database from running. Please try again later. + dialog('ok', + heading='{plex} %s' % language(30132), + line1=language(39601)) + return + xbmc.sleep(1000) + + # Wipe everything + wipe_database() + # Reset all PlexKodiConnect Addon settings? (this is usually NOT # recommended and unnecessary!) if dialog('yesno', From 688023c906f1caa758fe2437dc8edfcd81eafc42 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 2 Mar 2018 07:36:45 +0100 Subject: [PATCH 407/509] Remove obsolete import --- resources/lib/artwork.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4435eca5..8c3ed719 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -16,13 +16,11 @@ from utils import window, settings, language as lang, kodi_sql, try_encode, \ thread_methods, dialog, exists_dir, try_decode import state -# Disable annoying requests warnings -import requests.packages.urllib3 -requests.packages.urllib3.disable_warnings() ############################################################################### - LOG = getLogger("PLEX." + __name__) +# Disable annoying requests warnings +requests.packages.urllib3.disable_warnings() ############################################################################### ARTWORK_QUEUE = Queue() From 22ddd28f0be5d985b84c67129cd7572cf2408008 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 2 Mar 2018 07:48:38 +0100 Subject: [PATCH 408/509] Start id numbering with 0, not 1 --- resources/lib/itemtypes.py | 11 +++++----- resources/lib/kodidb_functions.py | 34 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index cbaa77a7..c53fb002 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -207,7 +207,7 @@ class Movies(Items): except TypeError: # movieid update_item = False - kodicursor.execute("select coalesce(max(idMovie),0) from movie") + kodicursor.execute("SELECT COALESCE(MAX(idMovie),-1) FROM movie") movieid = kodicursor.fetchone()[0] + 1 else: @@ -544,7 +544,7 @@ class TVShows(Items): pathid = plex_dbitem[2] except TypeError: update_item = False - kodicursor.execute("select coalesce(max(idShow),0) from tvshow") + kodicursor.execute("SELECT COALESCE(MAX(idShow),-1) from tvshow") showid = kodicursor.fetchone()[0] + 1 else: @@ -825,7 +825,8 @@ class TVShows(Items): except TypeError: update_item = False # episodeid - kodicursor.execute("select coalesce(max(idEpisode),0) from episode") + query = 'SELECT COALESCE(MAX(idEpisode),-1) FROM episode' + kodicursor.execute(query) episodeid = kodicursor.fetchone()[0] + 1 else: # Verification the item is still in Kodi @@ -1581,7 +1582,7 @@ class Music(Items): except TypeError: # Songid not found update_item = False - kodicursor.execute("select coalesce(max(idSong),0) from song") + kodicursor.execute("SELECT COALESCE(MAX(idSong),-1) FROM song") songid = kodicursor.fetchone()[0] + 1 # The song details ##### @@ -1734,7 +1735,7 @@ class Music(Items): # No album found, create a single's album LOG.info("Failed to add album. Creating singles.") kodicursor.execute( - "select coalesce(max(idAlbum),0) from album") + "SELECT COALESCE(MAX(idAlbum),-1) FROM album") albumid = kodicursor.fetchone()[0] + 1 if v.KODIVERSION >= 16: # Kodi Jarvis diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 18ecdba9..8f98d442 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -62,7 +62,7 @@ class KodiDBMethods(object): """ path_id = self.getPath('plugin://%s.movies/' % v.ADDON_ID) if path_id is None: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") path_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO path(idPath, @@ -82,7 +82,7 @@ class KodiDBMethods(object): # And TV shows path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID) if path_id is None: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") path_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO path(idPath, @@ -113,7 +113,7 @@ class KodiDBMethods(object): parentpath = "%s/" % dirname(dirname(path)) pathid = self.getPath(parentpath) if pathid is None: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") pathid = self.cursor.fetchone()[0] + 1 query = ' '.join(( "INSERT INTO path(idPath, strPath)", @@ -143,7 +143,7 @@ class KodiDBMethods(object): try: pathid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") pathid = self.cursor.fetchone()[0] + 1 if strHash is None: query = ( @@ -197,7 +197,7 @@ class KodiDBMethods(object): try: fileid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idFile),0) from files") + self.cursor.execute("SELECT COALESCE(MAX(idFile),-1) FROM files") fileid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -707,7 +707,7 @@ class KodiDBMethods(object): # Set the resume bookmark if resume_seconds: self.cursor.execute( - 'select coalesce(max(idBookmark),0) from bookmark') + 'SELECT COALESCE(MAX(idBookmark),-1) FROM bookmark') bookmark_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO bookmark( @@ -744,7 +744,7 @@ class KodiDBMethods(object): tag_id = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(tag_id),0) from tag") + self.cursor.execute("SELECT COALESCE(MAX(tag_id),-1) FROM tag") tag_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO tag(tag_id, name) values(?, ?)" @@ -795,7 +795,7 @@ class KodiDBMethods(object): setid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idSet),0) from sets") + self.cursor.execute("SELECT COALESCE(MAX(idSet),-1) FROM sets") setid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO sets(idSet, strSet) values(?, ?)" @@ -859,9 +859,9 @@ class KodiDBMethods(object): try: seasonid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idSeason),0) from seasons") + self.cursor.execute("SELECT COALESCE(MAX(idSeason),-1) FROM seasons") seasonid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" + query = "INSERT INTO seasons(idSeason, idShow, season) VALUES(?, ?, ?)" self.cursor.execute(query, (seasonid, showid, seasonnumber)) return seasonid @@ -897,10 +897,10 @@ class KodiDBMethods(object): # [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing if v.KODIVERSION >= 17: self.cursor.execute( - "select coalesce(max(idArtist),1) from artist") + "SELECT COALESCE(MAX(idArtist),1) FROM artist") else: self.cursor.execute( - "select coalesce(max(idArtist),0) from artist") + "SELECT COALESCE(MAX(idArtist),-1) FROM artist") artistid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -930,7 +930,7 @@ class KodiDBMethods(object): albumid = self.cursor.fetchone()[0] except TypeError: # Create the album - self.cursor.execute("select coalesce(max(idAlbum),0) from album") + self.cursor.execute("SELECT COALESCE(MAX(idAlbum),-1) FROM album") albumid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -967,7 +967,7 @@ class KodiDBMethods(object): genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - self.cursor.execute("select coalesce(max(idGenre),0) from genre") + self.cursor.execute("SELECT COALESCE(MAX(idGenre),-1) FROM genre") genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" self.cursor.execute(query, (genreid, genre)) @@ -998,7 +998,7 @@ class KodiDBMethods(object): genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - self.cursor.execute("select coalesce(max(idGenre),0) from genre") + self.cursor.execute("SELECT COALESCE(MAX(idGenre),-1) FROM genre") genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" self.cursor.execute(query, (genreid, genre)) @@ -1047,7 +1047,7 @@ class KodiDBMethods(object): uniqueid = self.cursor.fetchone()[0] except TypeError: self.cursor.execute( - 'SELECT COALESCE(MAX(uniqueid_id),0) FROM uniqueid') + 'SELECT COALESCE(MAX(uniqueid_id),-1) FROM uniqueid') uniqueid = self.cursor.fetchone()[0] + 1 return uniqueid @@ -1078,7 +1078,7 @@ class KodiDBMethods(object): try: ratingid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute('SELECT COALESCE(MAX(rating_id),0) FROM rating') + self.cursor.execute('SELECT COALESCE(MAX(rating_id),-1) FROM rating') ratingid = self.cursor.fetchone()[0] + 1 return ratingid From b4716ba5115728af745e5654ff9509d272b4da99 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 3 Mar 2018 14:40:12 +0100 Subject: [PATCH 409/509] Artwork overhaul part 1 --- resources/lib/PlexAPI.py | 105 ++++++++++++++++-------------- resources/lib/itemtypes.py | 30 ++++----- resources/lib/kodidb_functions.py | 19 ++++++ resources/lib/variables.py | 15 +++++ 4 files changed, 104 insertions(+), 65 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b9d7e6a8..1710a093 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -44,6 +44,7 @@ from utils import window, settings, language as lang, try_decode, try_encode, \ unix_date_to_kodi, exists_dir, slugify, dialog, escape_html import PlexFunctions as PF import plexdb_functions as plexdb +import kodidb_functions as kodidb import variables as v import state @@ -732,51 +733,66 @@ class API(object): 'subtitle': subtitlelanguages } - def _one_artwork(self, entry): - if entry not in self.item.attrib: - return '' - artwork = self.item.attrib[entry] - if artwork.startswith('http'): - pass - else: + def _one_artwork(self, art_kind): + artwork = self.item.get(art_kind) + if artwork and not artwork.startswith('http'): artwork = self.attach_plex_token_to_url( '%s/photo/:/transcode?width=4000&height=4000&' 'minSize=1&upscale=0&url=%s' % (self.server, artwork)) return artwork - def artwork(self, parent_info=False): + def artwork(self, kodi_id=None, kodi_type=None): """ - Gets the URLs to the Plex artwork, or empty string if not found. - parent_info=True will check for parent's artwork if None is found + Gets the URLs to the Plex artwork. Dict keys will be missing if there + is no corresponding artwork. + Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB + (thus potentially more artwork, e.g. clearart, discart) - Output: + Output ('max' version) { - 'Primary' - 'Art' - 'Banner' - 'Logo' - 'Thumb' - 'Disc' - 'Backdrop' : LIST with the first entry xml key "art" + 'thumb' + 'poster' + 'banner' + 'clearart' + 'clearlogo' + 'landscape' + 'icon' + 'fanart' } """ - allartworks = { - 'Primary': self._one_artwork('thumb'), - 'Art': "", - 'Banner': self._one_artwork('banner'), - 'Logo': "", - 'Thumb': self._one_artwork('grandparentThumb'), - 'Disc': "", - 'Backdrop': [self._one_artwork('art')] - } - # Process parent items if the main item is missing artwork - if parent_info: - # Process parent backdrops - if not allartworks['Backdrop']: - allartworks['Backdrop'].append(self._one_artwork('parentArt')) - if not allartworks['Primary']: - allartworks['Primary'] = self._one_artwork('parentThumb') - return allartworks + if kodi_id: + # in Kodi database, potentially with additional e.g. clearart + if self.plex_type() in v.PLEX_VIDEOTYPES: + with kodidb.GetKodiDB('video') as kodi_db: + return kodi_db.get_art(kodi_id, kodi_type) + else: + with kodidb.GetKodiDB('music') as kodi_db: + return kodi_db.get_art(kodi_id, kodi_type) + + # Grab artwork from Plex + artworks = {} + for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems(): + art = self._one_artwork(plex_artwork) + if art: + artworks[kodi_artwork] = art + if self.plex_type() in (v.PLEX_TYPE_EPISODE, + v.PLEX_TYPE_SONG, + v.PLEX_TYPE_ALBUM): + # Process parent item's poster + art = self._one_artwork('grandparentThumb') + if art: + artworks['tvshow.poster'] = art + # Get parent item artwork if the main item is missing artwork + if 'fanart1' not in artworks: + art = self._one_artwork('parentArt') + if art: + artworks['fanart1'] = art + if 'poster' not in artworks: + art = self._one_artwork('parentThumb') + if art: + artworks['poster'] = art + LOG.debug('artworks: %s', artworks) + return artworks def fanart_artwork(self, allartworks): """ @@ -1339,7 +1355,9 @@ class API(object): append_show_title, append_sxxexx) self.add_video_streams(listitem) - self.set_listitem_artwork(listitem) + artwork = self.artwork() + LOG.debug('artwork: %s', artwork) + listitem.setArt(artwork) return listitem def _create_photo_listitem(self, listitem=None): @@ -1545,19 +1563,8 @@ class API(object): """ Set all artwork to the listitem """ - allartwork = self.artwork(parent_info=True) - arttypes = { - 'poster': "Primary", - 'tvshow.poster': "Thumb", - 'clearart': "Primary", - 'tvshow.clearart': "Primary", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Backdrop", - "banner": "Banner" - } + allartwork = self.artwork() + listitem.setArt(self.artwork()) for arttype in arttypes: art = arttypes[arttype] if art == "Backdrop": diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index c53fb002..d73bd5f2 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1306,16 +1306,15 @@ class Music(Items): bio = api.plot() # Associate artwork - artworks = api.artwork(parent_info=True) - thumb = artworks['Primary'] - backdrops = artworks['Backdrop'] # List - - if thumb: - thumb = "%s" % thumb - if backdrops: - fanart = "%s" % backdrops[0] + artworks = api.artwork() + if 'poster' in artworks: + thumb = "%s" % artworks['poster'] else: - fanart = "" + thumb = None + if 'fanart1' in artworks: + fanart = "%s" % artworks['fanart1'] + else: + fanart = None # UPDATE THE ARTIST ##### if update_item: @@ -1412,10 +1411,11 @@ class Music(Items): self.compilation = 1 break # Associate artwork - artworks = api.artwork(parent_info=True) - thumb = artworks['Primary'] - if thumb: - thumb = "%s" % thumb + artworks = api.artwork() + if 'poster' in artworks: + thumb = "%s" % artworks['poster'] + else: + thumb = None # UPDATE THE ALBUM ##### if update_item: @@ -1847,9 +1847,7 @@ class Music(Items): # Add genres if genres: self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG) - # Update artwork - allart = api.artwork(parent_info=True) - artwork.addArtwork(allart, songid, v.KODI_TYPE_SONG, kodicursor) + artwork.addArtwork(api.artwork(), songid, v.KODI_TYPE_SONG, kodicursor) if item.get('parentKey') is None: # Update album artwork artwork.addArtwork(allart, albumid, v.KODI_TYPE_ALBUM, kodicursor) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 8f98d442..cb535e83 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -461,6 +461,25 @@ class KodiDBMethods(object): self.cursor) return actor_id + def get_art(self, kodi_id, kodi_type): + """ + Returns a dict of all available artwork with unicode urls/paths: + { + 'thumb' + 'poster' + 'banner' + 'fanart' + 'clearart' + 'clearlogo' + 'landscape' + 'icon' + } + Missing fanart will not appear in the dict. + """ + query = 'SELECT type, url FROM art WHERE media_id=? AND media_type=?' + self.cursor.execute(query, (kodi_id, kodi_type)) + return dict(self.cursor.fetchall()) + def existingArt(self, kodiId, mediaType, refresh=False): """ For kodiId, returns an artwork dict with already existing art from diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 1674e67e..e1ed163e 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -180,6 +180,14 @@ KODI_VIDEOTYPES = ( KODI_TYPE_SET ) +PLEX_VIDEOTYPES = ( + PLEX_TYPE_MOVIE, + PLEX_TYPE_CLIP, + PLEX_TYPE_EPISODE, + PLEX_TYPE_SEASON, + PLEX_TYPE_SHOW +) + KODI_AUDIOTYPES = ( KODI_TYPE_SONG, KODI_TYPE_ALBUM, @@ -310,6 +318,13 @@ PLEX_TYPE_FROM_WEBSOCKET = { } +KODI_TO_PLEX_ARTWORK = { + 'poster': 'thumb', + 'banner': 'banner', + 'fanart1': 'art' +} + + # extensions from: # http://kodi.wiki/view/Features_and_supported_codecs#Format_support (RAW image # formats, BMP, JPEG, GIF, PNG, TIFF, MNG, ICO, PCX and Targa/TGA) From 8272a67b5fb3fdc28c0989823fc9f50e4c85ef29 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 13:39:18 +0100 Subject: [PATCH 410/509] Artwork overhaul part 2 --- resources/lib/PlexAPI.py | 158 ++++++++-------------- resources/lib/artwork.py | 211 ++++++++++-------------------- resources/lib/itemtypes.py | 67 ++++++---- resources/lib/kodidb_functions.py | 56 +------- resources/lib/variables.py | 29 +++- 5 files changed, 196 insertions(+), 325 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 1710a093..4be62b40 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -54,19 +54,6 @@ LOG = getLogger("PLEX." + __name__) REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') -# we need to use a little mapping between fanart.tv arttypes and kodi -# artttypes -FANART_TV_TYPES = [ - ("logo", "Logo"), - ("musiclogo", "clearlogo"), - ("disc", "Disc"), - ("clearart", "Art"), - ("banner", "Banner"), - ("clearlogo", "Logo"), - ("background", "fanart"), - ("showbackground", "fanart"), - ("characterart", "characterart") -] ############################################################################### @@ -755,10 +742,9 @@ class API(object): 'banner' 'clearart' 'clearlogo' - 'landscape' - 'icon' 'fanart' } + 'landscape' and 'icon' might be implemented later """ if kodi_id: # in Kodi database, potentially with additional e.g. clearart @@ -783,7 +769,7 @@ class API(object): if art: artworks['tvshow.poster'] = art # Get parent item artwork if the main item is missing artwork - if 'fanart1' not in artworks: + if 'fanart' not in artworks: art = self._one_artwork('parentArt') if art: artworks['fanart1'] = art @@ -791,39 +777,31 @@ class API(object): art = self._one_artwork('parentThumb') if art: artworks['poster'] = art - LOG.debug('artworks: %s', artworks) return artworks - def fanart_artwork(self, allartworks): + def fanart_artwork(self, artworks): """ Downloads additional fanart from third party sources (well, link to fanart only). - - allartworks = { - 'Primary': "", - 'Art': "", - 'Banner': "", - 'Logo': "", - 'Thumb': "", - 'Disc': "", - 'Backdrop': [] - } """ external_id = self.retrieve_external_item_id() if external_id is not None: - allartworks = self.lookup_fanart_tv(external_id, allartworks) - return allartworks + artworks = self.lookup_fanart_tv(external_id[0], artworks) + LOG.debug('fanart artworks: %s', artworks) + return artworks def retrieve_external_item_id(self, collection=False): """ - Returns the item's IMDB id for movies or tvdb id for TV shows + Returns the set + media_id [unicode]: the item's IMDB id for movies or tvdb id for + TV shows + poster [unicode]: path to the item's poster artwork + background [unicode]: path to the item's background artwork + + The last two might be None if not found. Generally None is returned + if unsuccessful. - If not found in item's Plex metadata, check themovidedb.org - - collection=True will try to return the three-tuple: - collection ID, poster-path, background-path - - None is returned if unsuccessful + If not found in item's Plex metadata, check themovidedb.org. """ item = self.item.attrib media_type = item.get('type') @@ -836,7 +814,7 @@ class API(object): elif media_type == v.PLEX_TYPE_SHOW: media_id = self.provider('tvdb') if media_id is not None: - return media_id + return media_id, None, None LOG.info('Plex did not provide ID for IMDB or TVDB. Start ' 'lookup process') else: @@ -935,9 +913,8 @@ class API(object): media_type = entry.get("media_type") name = entry.get("name", entry.get("title")) # lookup external tmdb_id and perform artwork lookup on fanart.tv - parameters = { - 'api_key': api_key - } + parameters = {'api_key': api_key} + media_id, poster, background = None, None, None for language in [v.KODILANGUAGE, "en"]: parameters['language'] = language if media_type == "movie": @@ -977,7 +954,7 @@ class API(object): try: data.get('poster_path') except AttributeError: - LOG.info('Could not find TheMovieDB poster paths for %s in' + LOG.info('Could not find TheMovieDB poster paths for %s in ' 'the language %s', title, language) continue else: @@ -985,23 +962,21 @@ class API(object): data.get('poster_path')) background = ('https://image.tmdb.org/t/p/original%s' % data.get('backdrop_path')) - media_id = media_id, poster, background break - return media_id + return media_id, poster, background - def lookup_fanart_tv(self, media_id, allartworks, set_info=False): + def lookup_fanart_tv(self, media_id, artworks, set_info=False): """ perform artwork lookup on fanart.tv media_id: IMDB id for movies, tvdb id for TV shows """ - item = self.item.attrib api_key = settings('FanArtTVAPIKey') - typus = item.get('type') - if typus == 'show': + typus = self.plex_type() + if typus == v.PLEX_TYPE_SHOW: typus = 'tv' - if typus == "movie": + if typus == v.PLEX_TYPE_MOVIE: url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \ % (media_id, api_key) elif typus == 'tv': @@ -1009,24 +984,20 @@ class API(object): % (media_id, api_key) else: # Not supported artwork - return allartworks - data = DU().downloadUrl(url, - authenticate=False, - timeout=15) + return artworks + data = DU().downloadUrl(url, authenticate=False, timeout=15) try: data.get('test') except AttributeError: LOG.error('Could not download data from FanartTV') - return allartworks + return artworks - fanart_tv_types = list(FANART_TV_TYPES) + fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE) - if typus == "artist": + if typus == v.PLEX_TYPE_ARTIST: fanart_tv_types.append(("thumb", "folder")) else: - fanart_tv_types.append(("thumb", "Thumb")) - if set_info: - fanart_tv_types.append(("poster", "Primary")) + fanart_tv_types.append(("thumb", "thumb")) prefixes = ( "hd" + typus, @@ -1034,32 +1005,31 @@ class API(object): typus, "", ) - for fanarttype in fanart_tv_types: + for fanart_tv_type, kodi_type in fanart_tv_types: # Skip the ones we already have - if allartworks.get(fanarttype[1]): + if kodi_type in artworks: continue for prefix in prefixes: - fanarttvimage = prefix + fanarttype[0] + fanarttvimage = prefix + fanart_tv_type if fanarttvimage not in data: continue # select image in preferred language for entry in data[fanarttvimage]: if entry.get("lang") == v.KODILANGUAGE: - allartworks[fanarttype[1]] = \ + artworks[kodi_type] = \ entry.get("url", "").replace(' ', '%20') break # just grab the first english OR undefinded one as fallback # (so we're actually grabbing the more popular one) - if not allartworks.get(fanarttype[1]): + if kodi_type not in artworks: for entry in data[fanarttvimage]: if entry.get("lang") in ("en", "00"): - allartworks[fanarttype[1]] = \ + artworks[kodi_type] = \ entry.get("url", "").replace(' ', '%20') break # grab extrafanarts in list - maxfanarts = 10 - fanartcount = 0 + fanartcount = 1 if 'fanart' in artworks else '' for prefix in prefixes: fanarttvimage = prefix + 'background' if fanarttvimage not in data: @@ -1067,60 +1037,38 @@ class API(object): for entry in data[fanarttvimage]: if entry.get("url") is None: continue - if fanartcount > maxfanarts: + artworks['fanart%s' % fanartcount] = \ + entry['url'].replace(' ', '%20') + try: + fanartcount += 1 + except TypeError: + fanartcount = 1 + if fanartcount >= v.MAX_BACKGROUND_COUNT: break - allartworks['Backdrop'].append( - entry['url'].replace(' ', '%20')) - fanartcount += 1 - return allartworks + return artworks def set_artwork(self): """ Gets the URLs to the Plex artwork, or empty string if not found. - parentInfo=True will check for parent's artwork if None is found - Only call on movies - - Output: - { - 'Primary' - 'Art' - 'Banner' - 'Logo' - 'Thumb' - 'Disc' - 'Backdrop' : LIST with the first entry xml key "art" - } """ - allartworks = { - 'Primary': "", - 'Art': "", - 'Banner': "", - 'Logo': "", - 'Thumb': "", - 'Disc': "", - 'Backdrop': [] - } - + artworks = {} # Plex does not get much artwork - go ahead and get the rest from # fanart tv only for movie or tv show external_id = self.retrieve_external_item_id(collection=True) if external_id is not None: - try: - external_id, poster, background = external_id - except TypeError: - poster, background = None, None + external_id, poster, background = external_id if poster is not None: - allartworks['Primary'] = poster + artworks['poster'] = poster if background is not None: - allartworks['Backdrop'].append(background) - allartworks = self.lookup_fanart_tv(external_id, - allartworks, - set_info=True) + artworks['fanart'] = background + artworks = self.lookup_fanart_tv(external_id, + artworks, + set_info=True) else: LOG.info('Did not find a set/collection ID on TheMovieDB using %s.' ' Artwork will be missing.', self.titles()[0]) - return allartworks + return artworks def should_stream(self): """ diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 8c3ed719..1c39b806 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -21,9 +21,8 @@ LOG = getLogger("PLEX." + __name__) # Disable annoying requests warnings requests.packages.urllib3.disable_warnings() -############################################################################### - ARTWORK_QUEUE = Queue() +############################################################################### def double_urlencode(text): @@ -88,26 +87,25 @@ class Image_Cache_Thread(Thread): # Server thinks its a DOS attack, ('error 10053') # Wait before trying again if sleeptime > 5: - LOG.error('Repeatedly got ConnectionError for url %s' - % double_urldecode(url)) + LOG.error('Repeatedly got ConnectionError for url %s', + double_urldecode(url)) break LOG.debug('Were trying too hard to download art, server ' 'over-loaded. Sleep %s seconds before trying ' - 'again to download %s' - % (2**sleeptime, double_urldecode(url))) + 'again to download %s', + 2**sleeptime, double_urldecode(url)) sleep((2**sleeptime)*1000) sleeptime += 1 continue except Exception as e: - LOG.error('Unknown exception for url %s: %s' - % (double_urldecode(url), e)) + LOG.error('Unknown exception for url %s: %s'. + double_urldecode(url), e) import traceback - LOG.error("Traceback:\n%s" % traceback.format_exc()) + LOG.error("Traceback:\n%s", traceback.format_exc()) break # We did not even get a timeout break queue.task_done() - LOG.debug('Cached art: %s' % double_urldecode(url)) # Sleep for a bit to reduce CPU strain sleep(sleep_between) LOG.info("---===### Stopped Image_Cache_Thread ###===---") @@ -135,7 +133,7 @@ class Artwork(): path = try_decode(translatePath("special://thumbnails/")) if exists_dir(path): rmtree(path, ignore_errors=True) - self.restoreCacheDirectories() + self.restore_cache_directories() # remove all existing data from texture DB connection = kodi_sql('texture') @@ -158,167 +156,91 @@ class Artwork(): cursor.execute(query, ('actor', )) result = cursor.fetchall() total = len(result) - LOG.info("Image cache sync about to process %s video images" % total) + LOG.info("Image cache sync about to process %s video images", total) connection.close() for url in result: - self.cacheTexture(url[0]) + self.cache_texture(url[0]) # Cache all entries in music DB connection = kodi_sql('music') cursor = connection.cursor() cursor.execute("SELECT url FROM art") result = cursor.fetchall() total = len(result) - LOG.info("Image cache sync about to process %s music images" % total) + LOG.info("Image cache sync about to process %s music images", total) connection.close() for url in result: - self.cacheTexture(url[0]) + self.cache_texture(url[0]) - def cacheTexture(self, url): - # Cache a single image url to the texture cache + def cache_texture(self, url): + ''' + Cache a single image url to the texture cache + ''' if url and self.enableTextureCache: self.queue.put(double_urlencode(try_encode(url))) - def addArtwork(self, artwork, kodiId, mediaType, cursor): - # Kodi conversion table - kodiart = { - 'Primary': ["thumb", "poster"], - 'Banner': "banner", - 'Logo': "clearlogo", - 'Art': "clearart", - 'Thumb': "landscape", - 'Disc': "discart", - 'Backdrop': "fanart", - 'BoxRear': "poster" - } + def modify_artwork(self, artworks, kodi_id, kodi_type, cursor): + """ + Pass in an artworks dict (see PlexAPI) to set an items artwork. + """ + for kodi_art, url in artworks.iteritems(): + self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor) - # Artwork is a dictionary - for art in artwork: - if art == "Backdrop": - # Backdrop entry is a list - # Process extra fanart for artwork downloader (fanart, fanart1, - # fanart2...) - backdrops = artwork[art] - backdropsNumber = len(backdrops) - - query = ' '.join(( - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodiId, mediaType, "fanart%",)) - rows = cursor.fetchall() - - if len(rows) > backdropsNumber: - # More backdrops in database. Delete extra fanart. - query = ' '.join(( - "DELETE FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - cursor.execute(query, (kodiId, mediaType, "fanart_",)) - - # Process backdrops and extra fanart - index = "" - for backdrop in backdrops: - self.addOrUpdateArt( - imageUrl=backdrop, - kodiId=kodiId, - mediaType=mediaType, - imageType="%s%s" % ("fanart", index), - cursor=cursor) - - if backdropsNumber > 1: - try: # Will only fail on the first try, str to int. - index += 1 - except TypeError: - index = 1 - - elif art == "Primary": - # Primary art is processed as thumb and poster for Kodi. - for artType in kodiart[art]: - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=artType, - cursor=cursor) - - elif kodiart.get(art): - # Process the rest artwork type that Kodi can use - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=kodiart[art], - cursor=cursor) - - def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): - if not imageUrl: - # Possible that the imageurl is an empty string - return - - query = ' '.join(( - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - cursor.execute(query, (kodiId, mediaType, imageType,)) + def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor): + """ + Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the + Kodi art table for item kodi_id/kodi_type. Will also cache everything + except actor portraits. + """ + query = ''' + SELECT url FROM art + WHERE media_id = ? AND media_type = ? AND type = ? + LIMIT 1 + ''' + cursor.execute(query, (kodi_id, kodi_type, kodi_art,)) try: # Update the artwork - url = cursor.fetchone()[0] + old_url = cursor.fetchone()[0] except TypeError: # Add the artwork - LOG.debug("Adding Art Link for kodiId: %s (%s)" - % (kodiId, imageUrl)) - query = ( - ''' + LOG.debug('Adding Art Link for %s kodi_id %s, kodi_type %s: %s', + kodi_art, kodi_id, kodi_type, url) + query = ''' INSERT INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?) - ''' - ) - cursor.execute(query, (kodiId, mediaType, imageType, imageUrl)) + ''' + cursor.execute(query, (kodi_id, kodi_type, kodi_art, url)) else: - if url == imageUrl: + if url == old_url: # Only cache artwork if it changed return - # Only for the main backdrop, poster - if (window('plex_initialScan') != "true" and - imageType in ("fanart", "poster")): - # Delete current entry before updating with the new one - self.deleteCachedArtwork(url) - LOG.debug("Updating Art url for %s kodiId %s %s -> (%s)" - % (imageType, kodiId, url, imageUrl)) - query = ' '.join(( - "UPDATE art", - "SET url = ?", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) - + self.delete_cached_artwork(old_url) + LOG.debug("Updating Art url for %s kodi_id %s, kodi_type %s to %s", + kodi_art, kodi_id, kodi_type, url) + query = ''' + UPDATE art SET url = ? + WHERE media_id = ? AND media_type = ? AND type = ? + ''' + cursor.execute(query, (url, kodi_id, kodi_type, kodi_art)) # Cache fanart and poster in Kodi texture cache - if mediaType != 'actor': - self.cacheTexture(imageUrl) + if kodi_type != 'actor': + self.cache_texture(url) - def deleteArtwork(self, kodiId, mediaType, cursor): + def delete_artwork(self, kodiId, mediaType, cursor): query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?' cursor.execute(query, (kodiId, mediaType,)) for row in cursor.fetchall(): - self.deleteCachedArtwork(row[0]) + self.delete_cached_artwork(row[0]) - def deleteCachedArtwork(self, url): - # Only necessary to remove and apply a new backdrop or poster + @staticmethod + def delete_cached_artwork(url): + """ + Deleted the cached artwork with path url (if it exists) + """ connection = kodi_sql('texture') cursor = connection.cursor() try: - cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", + cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1", (url,)) cachedurl = cursor.fetchone()[0] except TypeError: @@ -327,7 +249,7 @@ class Artwork(): else: # Delete thumbnail as well as the entry path = translatePath("special://thumbnails/%s" % cachedurl) - LOG.debug("Deleting cached thumbnail: %s" % path) + LOG.debug("Deleting cached thumbnail: %s", path) if exists(path): rmtree(try_decode(path), ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) @@ -336,8 +258,11 @@ class Artwork(): connection.close() @staticmethod - def restoreCacheDirectories(): + def restore_cache_directories(): LOG.info("Restoring cache directories...") - paths = ("","0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","Video","plex") - for p in paths: - makedirs(try_decode(translatePath("special://thumbnails/%s" % p))) + paths = ("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "a", "b", "c", "d", "e", "f", + "Video", "plex") + for path in paths: + makedirs(try_decode(translatePath("special://thumbnails/%s" + % path))) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index d73bd5f2..5232018b 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -82,11 +82,11 @@ class Items(object): allartworks = None else: with kodidb.GetKodiDB('video') as kodi_db: - allartworks = kodi_db.existingArt(kodi_id, kodi_type) + allartworks = kodi_db.get_art(kodi_id, kodi_type) # Check if we even need to get additional art needsupdate = False - for key, value in allartworks.iteritems(): - if not value and not key == 'BoxRear': + for key in v.ALL_KODI_ARTWORK: + if key not in allartworks: needsupdate = True break if needsupdate is False: @@ -107,19 +107,19 @@ class Items(object): api = API(xml[0]) if allartworks is None: allartworks = api.artwork() - self.artwork.addArtwork(api.fanart_artwork(allartworks), - kodi_id, - kodi_type, - self.kodicursor) + self.artwork.modify_artwork(api.fanart_artwork(allartworks), + kodi_id, + kodi_type, + self.kodicursor) # Also get artwork for collections/movie sets if kodi_type == v.KODI_TYPE_MOVIE: for setname in api.collection_list(): LOG.debug('Getting artwork for movie set %s', setname) setid = self.kodi_db.createBoxset(setname) - self.artwork.addArtwork(api.set_artwork(), - setid, - v.KODI_TYPE_SET, - self.kodicursor) + self.artwork.modify_artwork(api.set_artwork(), + setid, + v.KODI_TYPE_SET, + self.kodicursor) self.kodi_db.assignBoxset(setid, kodi_id) return True @@ -445,7 +445,10 @@ class Movies(Items): # Process genres self.kodi_db.modify_genres(movieid, v.KODI_TYPE_MOVIE, genres) # Process artwork - artwork.addArtwork(api.artwork(), movieid, "movie", kodicursor) + artwork.modify_artwork(api.artwork(), + movieid, + v.KODI_TYPE_MOVIE, + kodicursor) # Process stream details self.kodi_db.modify_streams(fileid, api.mediastreams(), runtime) # Process studios @@ -482,7 +485,7 @@ class Movies(Items): # Remove the plex reference plex_db.removeItem(itemid) # Remove artwork - artwork.deleteArtwork(kodi_id, kodi_type, kodicursor) + artwork.delete_artwork(kodi_id, kodi_type, kodicursor) if kodi_type == v.KODI_TYPE_MOVIE: set_id = self.kodi_db.get_set_id(kodi_id) @@ -743,7 +746,10 @@ class TVShows(Items): self.kodi_db.modify_people(showid, v.KODI_TYPE_SHOW, api.people_list()) self.kodi_db.modify_genres(showid, v.KODI_TYPE_SHOW, genres) - artwork.addArtwork(api.artwork(), showid, v.KODI_TYPE_SHOW, kodicursor) + artwork.modify_artwork(api.artwork(), + showid, + v.KODI_TYPE_SHOW, + kodicursor) # Process studios self.kodi_db.modify_studios(showid, v.KODI_TYPE_SHOW, studios) # Process tags: view, PMS collection tags @@ -784,7 +790,10 @@ class TVShows(Items): # Process artwork allartworks = api.artwork() - artwork.addArtwork(allartworks, seasonid, "season", kodicursor) + artwork.modify_artwork(allartworks, + seasonid, + v.KODI_TYPE_SEASON, + kodicursor) if update_item: # Update a reference: checksum in plex table @@ -1095,8 +1104,8 @@ class TVShows(Items): if poster: poster = api.attach_plex_token_to_url( "%s%s" % (self.server, poster)) - artwork.addOrUpdateArt( - poster, episodeid, "episode", "thumb", kodicursor) + artwork.modify_art( + poster, episodeid, v.KODI_TYPE_EPISODE, "thumb", kodicursor) # Process stream details streams = api.mediastreams() @@ -1223,7 +1232,7 @@ class TVShows(Items): self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW) - self.artwork.deleteArtwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) + self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_SHOW) @@ -1235,7 +1244,7 @@ class TVShows(Items): Remove a season, and only a season, not the show or episodes """ kodicursor = self.kodicursor - self.artwork.deleteArtwork(kodi_id, "season", kodicursor) + self.artwork.delete_artwork(kodi_id, "season", kodicursor) kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,)) LOG.info("Removed season: %s.", kodi_id) @@ -1248,7 +1257,7 @@ class TVShows(Items): self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.modify_streams(file_id) self.kodi_db.delete_playstate(file_id) - self.artwork.deleteArtwork(kodi_id, "episode", kodicursor) + self.artwork.delete_artwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) kodicursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) @@ -1359,7 +1368,10 @@ class Music(Items): dateadded, artistid)) # Update artwork - artwork.addArtwork(artworks, artistid, v.KODI_TYPE_ARTIST, kodicursor) + artwork.modify_artwork(artworks, + artistid, + v.KODI_TYPE_ARTIST, + kodicursor) @catch_exceptions(warnuser=True) def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None, @@ -1553,7 +1565,7 @@ class Music(Items): # Add genres self.kodi_db.addMusicGenres(albumid, self.genres, v.KODI_TYPE_ALBUM) # Update artwork - artwork.addArtwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) + artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) # Add all children - all tracks if scan_children: for child in children: @@ -1847,10 +1859,11 @@ class Music(Items): # Add genres if genres: self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG) - artwork.addArtwork(api.artwork(), songid, v.KODI_TYPE_SONG, kodicursor) + artworks = api.artwork() + artwork.modify_artwork(artworks, songid, v.KODI_TYPE_SONG, kodicursor) if item.get('parentKey') is None: # Update album artwork - artwork.addArtwork(allart, albumid, v.KODI_TYPE_ALBUM, kodicursor) + artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) def remove(self, itemid): """ @@ -1937,7 +1950,7 @@ class Music(Items): """ Remove song, and only the song """ - self.artwork.deleteArtwork(kodiid, v.KODI_TYPE_SONG, self.kodicursor) + self.artwork.delete_artwork(kodiid, v.KODI_TYPE_SONG, self.kodicursor) self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) @@ -1945,7 +1958,7 @@ class Music(Items): """ Remove an album, and only the album """ - self.artwork.deleteArtwork(kodiid, v.KODI_TYPE_ALBUM, self.kodicursor) + self.artwork.delete_artwork(kodiid, v.KODI_TYPE_ALBUM, self.kodicursor) self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,)) @@ -1953,7 +1966,7 @@ class Music(Items): """ Remove an artist, and only the artist """ - self.artwork.deleteArtwork(kodiid, + self.artwork.delete_artwork(kodiid, v.KODI_TYPE_ARTIST, self.kodicursor) self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index cb535e83..34fbff2a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -406,7 +406,7 @@ class KodiDBMethods(object): self.cursor.execute(query_actor_delete, (person[0],)) if kind == 'actor': # Delete any associated artwork - self.artwork.deleteArtwork(person[0], 'actor', self.cursor) + self.artwork.delete_artwork(person[0], 'actor', self.cursor) # Save new people to Kodi DB by iterating over the remaining entries if kind == 'actor': query = 'INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)' @@ -454,11 +454,11 @@ class KodiDBMethods(object): 'VALUES (?, ?)', (actor_id, name)) if art_url: - self.artwork.addOrUpdateArt(art_url, - actor_id, - 'actor', - "thumb", - self.cursor) + self.artwork.modify_art(art_url, + actor_id, + 'actor', + 'thumb', + self.cursor) return actor_id def get_art(self, kodi_id, kodi_type): @@ -468,11 +468,11 @@ class KodiDBMethods(object): 'thumb' 'poster' 'banner' - 'fanart' 'clearart' 'clearlogo' 'landscape' 'icon' + 'fanart' and also potentially more fanart 'fanart1', 2, 3, ... } Missing fanart will not appear in the dict. """ @@ -480,48 +480,6 @@ class KodiDBMethods(object): self.cursor.execute(query, (kodi_id, kodi_type)) return dict(self.cursor.fetchall()) - def existingArt(self, kodiId, mediaType, refresh=False): - """ - For kodiId, returns an artwork dict with already existing art from - the Kodi db - """ - # Only get EITHER poster OR thumb (should have same URL) - kodiToPKC = { - 'banner': 'Banner', - 'clearart': 'Art', - 'clearlogo': 'Logo', - 'discart': 'Disc', - 'landscape': 'Thumb', - 'thumb': 'Primary' - } - # BoxRear yet unused - result = {'BoxRear': ''} - for art in kodiToPKC: - query = ' '.join(( - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type = ?" - )) - self.cursor.execute(query, (kodiId, mediaType, art,)) - try: - url = self.cursor.fetchone()[0] - except TypeError: - url = "" - result[kodiToPKC[art]] = url - # There may be several fanart URLs saved - query = ' '.join(( - "SELECT url", - "FROM art", - "WHERE media_id = ?", - "AND media_type = ?", - "AND type LIKE ?" - )) - data = self.cursor.execute(query, (kodiId, mediaType, "fanart%",)) - result['Backdrop'] = [d[0] for d in data] - return result - def modify_streams(self, fileid, streamdetails=None, runtime=None): """ Leave streamdetails and runtime empty to delete all stream entries for diff --git a/resources/lib/variables.py b/resources/lib/variables.py index e1ed163e..6f440f92 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -321,9 +321,36 @@ PLEX_TYPE_FROM_WEBSOCKET = { KODI_TO_PLEX_ARTWORK = { 'poster': 'thumb', 'banner': 'banner', - 'fanart1': 'art' + 'fanart': 'art' } +# Might be implemented in the future: 'icon', 'landscape' (16:9) +ALL_KODI_ARTWORK = ( + 'thumb', + 'poster', + 'banner', + 'clearart', + 'clearlogo', + 'fanart', + 'discart' +) + +# we need to use a little mapping between fanart.tv arttypes and kodi artttypes +FANART_TV_TO_KODI_TYPE = [ + ('poster', 'poster'), + ('logo', 'clearlogo'), + ('musiclogo', 'clearlogo'), + ('disc', 'discart'), + ('clearart', 'clearart'), + ('banner', 'banner'), + ('clearlogo', 'clearlogo'), + ('background', 'fanart'), + ('showbackground', 'fanart'), + ('characterart', 'characterart') +] +# How many different backgrounds do we want to load from fanart.tv? +MAX_BACKGROUND_COUNT = 10 + # extensions from: # http://kodi.wiki/view/Features_and_supported_codecs#Format_support (RAW image From 275283616ee6986114aa2900b2b3317d04cb26a0 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 13:39:40 +0100 Subject: [PATCH 411/509] Revert "Start id numbering with 0, not 1" This reverts commit 22ddd28f0be5d985b84c67129cd7572cf2408008. --- resources/lib/itemtypes.py | 11 +++++----- resources/lib/kodidb_functions.py | 34 +++++++++++++++---------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 5232018b..9e40dd08 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -207,7 +207,7 @@ class Movies(Items): except TypeError: # movieid update_item = False - kodicursor.execute("SELECT COALESCE(MAX(idMovie),-1) FROM movie") + kodicursor.execute("select coalesce(max(idMovie),0) from movie") movieid = kodicursor.fetchone()[0] + 1 else: @@ -547,7 +547,7 @@ class TVShows(Items): pathid = plex_dbitem[2] except TypeError: update_item = False - kodicursor.execute("SELECT COALESCE(MAX(idShow),-1) from tvshow") + kodicursor.execute("select coalesce(max(idShow),0) from tvshow") showid = kodicursor.fetchone()[0] + 1 else: @@ -834,8 +834,7 @@ class TVShows(Items): except TypeError: update_item = False # episodeid - query = 'SELECT COALESCE(MAX(idEpisode),-1) FROM episode' - kodicursor.execute(query) + kodicursor.execute("select coalesce(max(idEpisode),0) from episode") episodeid = kodicursor.fetchone()[0] + 1 else: # Verification the item is still in Kodi @@ -1594,7 +1593,7 @@ class Music(Items): except TypeError: # Songid not found update_item = False - kodicursor.execute("SELECT COALESCE(MAX(idSong),-1) FROM song") + kodicursor.execute("select coalesce(max(idSong),0) from song") songid = kodicursor.fetchone()[0] + 1 # The song details ##### @@ -1747,7 +1746,7 @@ class Music(Items): # No album found, create a single's album LOG.info("Failed to add album. Creating singles.") kodicursor.execute( - "SELECT COALESCE(MAX(idAlbum),-1) FROM album") + "select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 if v.KODIVERSION >= 16: # Kodi Jarvis diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 34fbff2a..af9f89f7 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -62,7 +62,7 @@ class KodiDBMethods(object): """ path_id = self.getPath('plugin://%s.movies/' % v.ADDON_ID) if path_id is None: - self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") + self.cursor.execute("select coalesce(max(idPath),0) from path") path_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO path(idPath, @@ -82,7 +82,7 @@ class KodiDBMethods(object): # And TV shows path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID) if path_id is None: - self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") + self.cursor.execute("select coalesce(max(idPath),0) from path") path_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO path(idPath, @@ -113,7 +113,7 @@ class KodiDBMethods(object): parentpath = "%s/" % dirname(dirname(path)) pathid = self.getPath(parentpath) if pathid is None: - self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") + self.cursor.execute("select coalesce(max(idPath),0) from path") pathid = self.cursor.fetchone()[0] + 1 query = ' '.join(( "INSERT INTO path(idPath, strPath)", @@ -143,7 +143,7 @@ class KodiDBMethods(object): try: pathid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("SELECT COALESCE(MAX(idPath),-1) FROM path") + self.cursor.execute("select coalesce(max(idPath),0) from path") pathid = self.cursor.fetchone()[0] + 1 if strHash is None: query = ( @@ -197,7 +197,7 @@ class KodiDBMethods(object): try: fileid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("SELECT COALESCE(MAX(idFile),-1) FROM files") + self.cursor.execute("select coalesce(max(idFile),0) from files") fileid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -684,7 +684,7 @@ class KodiDBMethods(object): # Set the resume bookmark if resume_seconds: self.cursor.execute( - 'SELECT COALESCE(MAX(idBookmark),-1) FROM bookmark') + 'select coalesce(max(idBookmark),0) from bookmark') bookmark_id = self.cursor.fetchone()[0] + 1 query = ''' INSERT INTO bookmark( @@ -721,7 +721,7 @@ class KodiDBMethods(object): tag_id = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("SELECT COALESCE(MAX(tag_id),-1) FROM tag") + self.cursor.execute("select coalesce(max(tag_id),0) from tag") tag_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO tag(tag_id, name) values(?, ?)" @@ -772,7 +772,7 @@ class KodiDBMethods(object): setid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("SELECT COALESCE(MAX(idSet),-1) FROM sets") + self.cursor.execute("select coalesce(max(idSet),0) from sets") setid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO sets(idSet, strSet) values(?, ?)" @@ -836,9 +836,9 @@ class KodiDBMethods(object): try: seasonid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("SELECT COALESCE(MAX(idSeason),-1) FROM seasons") + self.cursor.execute("select coalesce(max(idSeason),0) from seasons") seasonid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO seasons(idSeason, idShow, season) VALUES(?, ?, ?)" + query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" self.cursor.execute(query, (seasonid, showid, seasonnumber)) return seasonid @@ -874,10 +874,10 @@ class KodiDBMethods(object): # [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing if v.KODIVERSION >= 17: self.cursor.execute( - "SELECT COALESCE(MAX(idArtist),1) FROM artist") + "select coalesce(max(idArtist),1) from artist") else: self.cursor.execute( - "SELECT COALESCE(MAX(idArtist),-1) FROM artist") + "select coalesce(max(idArtist),0) from artist") artistid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -907,7 +907,7 @@ class KodiDBMethods(object): albumid = self.cursor.fetchone()[0] except TypeError: # Create the album - self.cursor.execute("SELECT COALESCE(MAX(idAlbum),-1) FROM album") + self.cursor.execute("select coalesce(max(idAlbum),0) from album") albumid = self.cursor.fetchone()[0] + 1 query = ( ''' @@ -944,7 +944,7 @@ class KodiDBMethods(object): genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - self.cursor.execute("SELECT COALESCE(MAX(idGenre),-1) FROM genre") + self.cursor.execute("select coalesce(max(idGenre),0) from genre") genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" self.cursor.execute(query, (genreid, genre)) @@ -975,7 +975,7 @@ class KodiDBMethods(object): genreid = self.cursor.fetchone()[0] except TypeError: # Create the genre - self.cursor.execute("SELECT COALESCE(MAX(idGenre),-1) FROM genre") + self.cursor.execute("select coalesce(max(idGenre),0) from genre") genreid = self.cursor.fetchone()[0] + 1 query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" self.cursor.execute(query, (genreid, genre)) @@ -1024,7 +1024,7 @@ class KodiDBMethods(object): uniqueid = self.cursor.fetchone()[0] except TypeError: self.cursor.execute( - 'SELECT COALESCE(MAX(uniqueid_id),-1) FROM uniqueid') + 'SELECT COALESCE(MAX(uniqueid_id),0) FROM uniqueid') uniqueid = self.cursor.fetchone()[0] + 1 return uniqueid @@ -1055,7 +1055,7 @@ class KodiDBMethods(object): try: ratingid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute('SELECT COALESCE(MAX(rating_id),-1) FROM rating') + self.cursor.execute('SELECT COALESCE(MAX(rating_id),0) FROM rating') ratingid = self.cursor.fetchone()[0] + 1 return ratingid From cb8a3abdd871777658d78e0949d74c6cdc3aab74 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 13:52:44 +0100 Subject: [PATCH 412/509] Remove obsolete code --- resources/lib/PlexAPI.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 4be62b40..337cad5b 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -764,10 +764,6 @@ class API(object): if self.plex_type() in (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SONG, v.PLEX_TYPE_ALBUM): - # Process parent item's poster - art = self._one_artwork('grandparentThumb') - if art: - artworks['tvshow.poster'] = art # Get parent item artwork if the main item is missing artwork if 'fanart' not in artworks: art = self._one_artwork('parentArt') From 80b810c7e0871f87836709fa81ce492f67439f82 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 14:12:43 +0100 Subject: [PATCH 413/509] Update method description --- resources/lib/kodidb_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index af9f89f7..e31a6be0 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -470,11 +470,11 @@ class KodiDBMethods(object): 'banner' 'clearart' 'clearlogo' - 'landscape' - 'icon' - 'fanart' and also potentially more fanart 'fanart1', 2, 3, ... + 'discart' + 'fanart' and also potentially more fanart 'fanart1', 'fanart2', } - Missing fanart will not appear in the dict. + Missing fanart will not appear in the dict. 'landscape' and 'icon' + might be implemented in the future. """ query = 'SELECT type, url FROM art WHERE media_id=? AND media_type=?' self.cursor.execute(query, (kodi_id, kodi_type)) From 48cc6e3471d594dceaf95166530599ad849c09d4 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 14:22:39 +0100 Subject: [PATCH 414/509] Fix music artwork not appearing --- resources/lib/PlexAPI.py | 7 +++++++ resources/lib/itemtypes.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 337cad5b..44512ee1 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -773,6 +773,13 @@ class API(object): art = self._one_artwork('parentThumb') if art: artworks['poster'] = art + if self.plex_type() in (v.PLEX_TYPE_SONG, + v.PLEX_TYPE_ALBUM, + v.PLEX_TYPE_ARTIST): + # need to set poster also as thumb + art = self._one_artwork('thumb') + if art: + artworks['thumb'] = art return artworks def fanart_artwork(self, artworks): diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9e40dd08..c7e77a45 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1319,8 +1319,8 @@ class Music(Items): thumb = "%s" % artworks['poster'] else: thumb = None - if 'fanart1' in artworks: - fanart = "%s" % artworks['fanart1'] + if 'fanart' in artworks: + fanart = "%s" % artworks['fanart'] else: fanart = None From 60c122523b594e1a99d2a61702421b7eea90cfbb Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 15:29:45 +0100 Subject: [PATCH 415/509] Fix episode information not working --- resources/lib/itemtypes.py | 13 ++--- resources/lib/kodidb_functions.py | 81 +++++++++++++++---------------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index c7e77a45..410066e5 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -299,7 +299,7 @@ class Movies(Items): # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.addPath(path) + pathid = self.kodi_db.add_video_path(path) fileid = self.kodi_db.addFile(filename, pathid) # UPDATE THE MOVIE ##### @@ -610,10 +610,10 @@ class TVShows(Items): path = "%s%s/" % (toplevelpath, itemid) # Add top path - toppathid = self.kodi_db.addPath(toplevelpath) + toppathid = self.kodi_db.add_video_path(toplevelpath) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.addPath(path) + pathid = self.kodi_db.add_video_path(path) # UPDATE THE TVSHOW ##### if update_item: LOG.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) @@ -924,7 +924,7 @@ class TVShows(Items): if do_indirect: # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! - path = 'plugin://%s.tvshows/' % v.ADDON_ID + path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) params = { 'plex_id': itemid, 'plex_type': v.PLEX_TYPE_EPISODE, @@ -932,10 +932,11 @@ class TVShows(Items): } filename = "%s?%s" % (path, urlencode(params)) playurl = filename + parent_path_id = self.kodi_db.getParentPathId(path) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.addPath(path) + pathid = self.kodi_db.add_video_path(path) fileid = self.kodi_db.addFile(filename, pathid) # UPDATE THE EPISODE ##### @@ -1700,7 +1701,7 @@ class Music(Items): LOG.info("ADD song itemid: %s - Title: %s", itemid, title) # Add path - pathid = self.kodi_db.addPath(path, strHash="123") + pathid = self.kodi_db.add_music_path(path, strHash="123") try: # Get the album diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index e31a6be0..8dd36633 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -6,7 +6,7 @@ from ntpath import dirname from sqlite3 import IntegrityError import artwork -from utils import kodi_sql, try_decode +from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi import variables as v ############################################################################### @@ -113,59 +113,54 @@ class KodiDBMethods(object): parentpath = "%s/" % dirname(dirname(path)) pathid = self.getPath(parentpath) if pathid is None: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 - query = ' '.join(( - "INSERT INTO path(idPath, strPath)", - "VALUES (?, ?)" - )) - self.cursor.execute(query, (pathid, parentpath)) - parentPathid = self.getParentPathId(parentpath) - query = ' '.join(( - "UPDATE path", - "SET idParentPath = ?", - "WHERE idPath = ?" - )) - self.cursor.execute(query, (parentPathid, pathid)) + datetime = unix_date_to_kodi(unix_timestamp()) + query = ''' + INSERT INTO path(idPath, strPath, dateAdded) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (pathid, parentpath, datetime)) + parent_path_id = self.getParentPathId(parentpath) + query = 'UPDATE path SET idParentPath = ? WHERE idPath = ?' + self.cursor.execute(query, (parent_path_id, pathid)) return pathid - def addPath(self, path, strHash=None): + def add_video_path(self, path): # SQL won't return existing paths otherwise if path is None: - path = "" - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) + path = '' + query = 'SELECT idPath FROM path WHERE strPath = ?' self.cursor.execute(query, (path,)) try: pathid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idPath),0) from path") + self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 - if strHash is None: - query = ( - ''' - INSERT INTO path( - idPath, strPath) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (pathid, path)) - else: - query = ( - ''' - INSERT INTO path( - idPath, strPath, strHash) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (pathid, path, strHash)) + datetime = unix_date_to_kodi(unix_timestamp()) + query = ''' + INSERT INTO path(idPath, strPath, dateAdded) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (pathid, path, datetime)) + return pathid + def add_music_path(self, path, strHash=None): + # SQL won't return existing paths otherwise + if path is None: + path = '' + query = 'SELECT idPath FROM path WHERE strPath = ?' + self.cursor.execute(query, (path,)) + try: + pathid = self.cursor.fetchone()[0] + except TypeError: + self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") + pathid = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO path(idPath, strPath, strHash) + VALUES (?, ?, ?) + ''' + self.cursor.execute(query, (pathid, path, strHash)) return pathid def getPath(self, path): From e9abce7d125342b805b42d38cb9bda6b6ce32ccd Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 18:02:55 +0100 Subject: [PATCH 416/509] Remove obsolete code --- resources/lib/PlexAPI.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 44512ee1..343c19c2 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -286,18 +286,14 @@ class API(object): cast = [] producer = [] for child in self.item: - try: - if child.tag == 'Director': - director.append(child.attrib['tag']) - elif child.tag == 'Writer': - writer.append(child.attrib['tag']) - elif child.tag == 'Role': - cast.append(child.attrib['tag']) - elif child.tag == 'Producer': - producer.append(child.attrib['tag']) - except KeyError: - LOG.warn('Malformed PMS answer for getPeople: %s: %s', - child.tag, child.attrib) + if child.tag == 'Director': + director.append(child.attrib['tag']) + elif child.tag == 'Writer': + writer.append(child.attrib['tag']) + elif child.tag == 'Role': + cast.append(child.attrib['tag']) + elif child.tag == 'Producer': + producer.append(child.attrib['tag']) return { 'Director': director, 'Writer': writer, From 377f721f1d5a057fe2098b12ebf3635a5276a64a Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 18:32:29 +0100 Subject: [PATCH 417/509] Fix art and show info not showing for addon paths --- resources/lib/PlexAPI.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 343c19c2..030aff5f 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -277,7 +277,7 @@ class API(object): { 'Director': list, 'Writer': list, - 'Cast': list, + 'Cast': list of tuples (, ), might be '' 'Producer': list } """ @@ -291,7 +291,7 @@ class API(object): elif child.tag == 'Writer': writer.append(child.attrib['tag']) elif child.tag == 'Role': - cast.append(child.attrib['tag']) + cast.append((child.attrib['tag'], child.get('role', ''))) elif child.tag == 'Producer': producer.append(child.attrib['tag']) return { @@ -1355,23 +1355,24 @@ class API(object): people = self.people() userdata = self.userdata() metadata = { - 'genre': self.list_to_string(self.genre_list()), + 'genre': self.genre_list(), + 'country': self.country_list(), 'year': self.year(), 'rating': self.audience_rating(), 'playcount': userdata['PlayCount'], 'cast': people['Cast'], - 'director': self.list_to_string(people.get('Director')), + 'director': people['Director'], 'plot': self.plot(), 'sorttitle': sorttitle, 'duration': userdata['Runtime'], - 'studio': self.list_to_string(self.music_studio_list()), + 'studio': self.music_studio_list(), 'tagline': self.tagline(), - 'writer': self.list_to_string(people.get('Writer')), + 'writer': people.get('Writer'), 'premiered': self.premiere_date(), 'dateadded': self.date_created(), 'lastplayed': userdata['LastPlayedDate'], 'mpaa': self.content_rating(), - 'aired': self.premiere_date() + 'aired': self.premiere_date(), } # Do NOT set resumetime - otherwise Kodi always resumes at that time # even if the user chose to start element from the beginning @@ -1379,38 +1380,37 @@ class API(object): listitem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: + metadata['mediatype'] = 'episode' _, show, season, episode = self.episode_data() season = -1 if season is None else int(season) episode = -1 if episode is None else int(episode) metadata['episode'] = episode + metadata['sortepisode'] = episode metadata['season'] = season + metadata['sortseason'] = season metadata['tvshowtitle'] = show if season and episode: - listitem.setProperty('episodeno', - "s%.2de%.2d" % (season, episode)) if append_sxxexx is True: title = "S%.2dE%.2d - %s" % (season, episode, title) - listitem.setArt({'icon': 'DefaultTVShows.png'}) if append_show_title is True: title = "%s - %s " % (show, title) if append_show_title or append_sxxexx: listitem.setLabel(title) elif typus == v.PLEX_TYPE_MOVIE: - listitem.setArt({'icon': 'DefaultMovies.png'}) + metadata['mediatype'] = 'movie' else: # E.g. clips, trailers, ... - listitem.setArt({'icon': 'DefaultVideo.png'}) + pass plex_id = self.plex_id() listitem.setProperty('plexid', plex_id) with plexdb.Get_Plex_DB() as plex_db: - try: - listitem.setProperty('dbid', - str(plex_db.getItem_byId(plex_id)[0])) - except TypeError: - pass - # Expensive operation + kodi_id = plex_db.getItem_byId(plex_id) + if kodi_id: + kodi_id = kodi_id[0] + metadata['dbid'] = kodi_id metadata['title'] = title + # Expensive operation listitem.setInfo('video', infoLabels=metadata) try: # Add context menu entry for information screen From 5af541200916d2c74164f1070939842bd6d78142 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 4 Mar 2018 18:59:18 +0100 Subject: [PATCH 418/509] Version bump --- README.md | 2 +- addon.xml | 13 +++++++++++-- changelog.txt | 9 +++++++++ resources/lib/variables.py | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 510bbd1d..952d12b8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.4-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index b702fefd..82f4ab67 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,16 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.4 (beta only): + version 2.0.5 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix art and show info not showing for addon paths +- Fix episode information not working +- Big Kodi DB overhaul - ensure video metadata updates/deletes correctly +- Artwork code overhaul +- Greatly speed up switch of PMS +- And a lot of other stuff + +version 2.0.4 (beta only): - WARNING: You will need to reset the Kodi database! - Many improvements to the Kodi database handling which should get rid of some weird bugs - Many improvements to playback startup diff --git a/changelog.txt b/changelog.txt index 6f79e610..4401f66c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +version 2.0.5 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix art and show info not showing for addon paths +- Fix episode information not working +- Big Kodi DB overhaul - ensure video metadata updates/deletes correctly +- Artwork code overhaul +- Greatly speed up switch of PMS +- And a lot of other stuff + version 2.0.4 (beta only): - WARNING: You will need to reset the Kodi database! - Many improvements to the Kodi database handling which should get rid of some weird bugs diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 6f440f92..d6ed6e3a 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -75,7 +75,7 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort')) PKC_MACHINE_IDENTIFIER = None # Minimal PKC version needed for the Kodi database - otherwise need to recreate -MIN_DB_VERSION = '2.0.4' +MIN_DB_VERSION = '2.0.5' # Database paths _DB_VIDEO_VERSION = { From 62e973dbe28997ce1c98e3046414f91aba5333ca Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Mar 2018 18:23:56 +0100 Subject: [PATCH 419/509] Fixes to add-on paths playback startup --- resources/lib/kodimonitor.py | 21 ++++++++++++--------- resources/lib/player.py | 4 ---- resources/lib/playlist_func.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 7cf72e64..289dbe22 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -209,17 +209,15 @@ class KodiMonitor(Monitor): } Will NOT be called if playback initiated by Kodi widgets """ - kodi_item = js.get_item(data['playlistid']) - if (state.RESUMABLE is True and not kodi_item['file'] and - data['position'] == 0 and - data['item'].get('title') is not None and - getCondVisibility('Window.IsVisible(MyVideoNav.xml)')): + old = state.OLD_PLAYER_STATES[data['playlistid']] + if (not state.DIRECT_PATHS and data['position'] == 0 and + not PQ.PLAYQUEUES[data['playlistid']].items and + data['item']['type'] == old['kodi_type'] and + data['item']['id'] == old['kodi_id']): # Hack we need for RESUMABLE items because Kodi lost the path of the # last played item that is now being replayed (see playback.py's - # Player().play()) - # Also see playqueue.py _compare_playqueues() + # Player().play()) Also see playqueue.py _compare_playqueues() LOG.info('Detected re-start of playback of last item') - old = state.OLD_PLAYER_STATES[data['playlistid']] kwargs = { 'plex_id': old['plex_id'], 'plex_type': old['plex_type'], @@ -240,6 +238,7 @@ class KodiMonitor(Monitor): """ pass + @LOCKER.lockthis def _playlist_onclear(self, data): """ Called if a Kodi playlist is cleared. Example data dict: @@ -247,7 +246,11 @@ class KodiMonitor(Monitor): u'playlistid': 1, } """ - pass + playqueue = PQ.PLAYQUEUES[data['playlistid']] + if not playqueue.is_pkc_clear(): + playqueue.clear(kodi=False) + else: + LOG.debug('Detected PKC clear - ignoring') def _get_ids(self, json_item): """ diff --git a/resources/lib/player.py b/resources/lib/player.py index 64710f66..74c38411 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -37,10 +37,6 @@ def playback_cleanup(): DU().downloadUrl( '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) - # Kodi will not clear the playqueue (because there is not really any) - # if there is only 1 item in it - if len(PQ.PLAYQUEUES[playerid].items) == 1: - PQ.PLAYQUEUES[playerid].clear() # Reset the player's status status = dict(state.PLAYSTATE) # As all playback has halted, reset the players that have been active diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index ac73cbd9..17cbd5ae 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -48,6 +48,8 @@ class PlaylistObjectBaseclase(object): self.plex_transient_token = None # Need a hack for detecting swaps of elements self.old_kodi_pl = [] + # Workaround to avoid endless loops of detecting PL clears + self._clear_list = [] def __repr__(self): """ @@ -67,6 +69,18 @@ class PlaylistObjectBaseclase(object): answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) return answ + '\'items\': %s}}' % self.items + def is_pkc_clear(self): + """ + Returns True if PKC has cleared the Kodi playqueue just recently. + Then this clear will be ignored from now on + """ + try: + self._clear_list.pop() + except IndexError: + return False + else: + return True + def clear(self, kodi=True): """ Resets the playlist object to an empty playlist. @@ -76,6 +90,7 @@ class PlaylistObjectBaseclase(object): # kodi monitor's on_clear method will only be called if there were some # items to begin with if kodi and self.kodi_pl.size() != 0: + self._clear_list.append(None) self.kodi_pl.clear() # Clear Kodi playlist object self.items = [] self.id = None From 2fb79b97f8d4dae17164d34b4306ea84ae353982 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Mar 2018 20:40:30 +0100 Subject: [PATCH 420/509] Fix UnicodeDecodeError for playqueue logging - Fixes #419 --- resources/lib/playlist_func.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 17cbd5ae..d302d18e 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Collection of functions associated with Kodi and Plex playlists and playqueues """ @@ -8,7 +9,7 @@ from re import compile as re_compile import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU -from utils import try_encode +from utils import try_decode, try_encode from PlexAPI import API from PlexFunctions import GetPlexMetadata from kodidb_functions import kodiid_from_filename @@ -53,21 +54,20 @@ class PlaylistObjectBaseclase(object): def __repr__(self): """ - Print the playlist, e.g. to log + Print the playlist, e.g. to log. Returns utf-8 encoded string """ - answ = '{\'%s\': {' % (self.__class__.__name__) + answ = u'{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id) # For some reason, can't use dir directly - answ += '\'id\': %s, ' % self.id for key in self.__dict__: if key in ('id', 'items', 'kodi_pl'): continue - if isinstance(getattr(self, key), (str, unicode)): + if isinstance(getattr(self, key), str): answ += '\'%s\': \'%s\', ' % (key, - try_encode(getattr(self, key))) + try_decode(getattr(self, key))) else: # e.g. int - answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) - return answ + '\'items\': %s}}' % self.items + answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) + return try_encode(answ + '\'items\': %s}}' % self.items) def is_pkc_clear(self): """ @@ -174,25 +174,27 @@ class Playlist_Item(object): def __repr__(self): """ - Print the playlist item, e.g. to log + Print the playlist item, e.g. to log. Returns utf-8 encoded string """ - answ = '{\'%s\': {' % (self.__class__.__name__) - answ += '\'id\': \'%s\', ' % self.id - answ += '\'plex_id\': \'%s\', ' % self.plex_id + answ = (u'{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', ' + % (self.__class__.__name__, self.id, self.plex_id)) for key in self.__dict__: if key in ('id', 'plex_id', 'xml'): continue - if isinstance(getattr(self, key), (str, unicode)): + if isinstance(getattr(self, key), str): + LOG.debug('key: %s, type: %s', key, type(key)) + LOG.debug('answ: %s, type: %s', answ, type(answ)) + LOG.debug('content: %s, type: %s', getattr(self, key), type(getattr(self, key))) answ += '\'%s\': \'%s\', ' % (key, - try_encode(getattr(self, key))) + try_decode(getattr(self, key))) else: # e.g. int - answ += '\'%s\': %s, ' % (key, str(getattr(self, key))) + answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) if self.xml is None: answ += '\'xml\': None}}' else: answ += '\'xml\': \'%s\'}}' % self.xml.tag - return answ + return try_encode(answ) def plex_stream_index(self, kodi_stream_index, stream_type): """ From 476e88dbccb44b529bfcd0600060c3e4c6c3f08c Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 6 Mar 2018 21:09:58 +0100 Subject: [PATCH 421/509] Version bump --- README.md | 2 +- addon.xml | 9 +++++++-- changelog.txt | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 952d12b8..80bf5ed8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.6-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 82f4ab67..db91c64b 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,12 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.5 (beta only): + version 2.0.6 (beta only): +- Addon paths playback was basically broken - hope it works again! +- Fixes to add-on paths playback startup +- Fix UnicodeDecodeError for playqueue logging + +version 2.0.5 (beta only): - WARNING: You will need to reset the Kodi database! - Fix art and show info not showing for addon paths - Fix episode information not working diff --git a/changelog.txt b/changelog.txt index 4401f66c..9b17aee4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +version 2.0.6 (beta only): +- Addon paths playback was basically broken - hope it works again! +- Fixes to add-on paths playback startup +- Fix UnicodeDecodeError for playqueue logging + version 2.0.5 (beta only): - WARNING: You will need to reset the Kodi database! - Fix art and show info not showing for addon paths From 433246207523fe5cae3c8b52676cda48edf0240e Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Mar 2018 07:48:14 +0100 Subject: [PATCH 422/509] Remove logging --- resources/lib/playlist_func.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index d302d18e..86de6752 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -182,9 +182,6 @@ class Playlist_Item(object): if key in ('id', 'plex_id', 'xml'): continue if isinstance(getattr(self, key), str): - LOG.debug('key: %s, type: %s', key, type(key)) - LOG.debug('answ: %s, type: %s', answ, type(answ)) - LOG.debug('content: %s, type: %s', getattr(self, key), type(getattr(self, key))) answ += '\'%s\': \'%s\', ' % (key, try_decode(getattr(self, key))) else: From a8ac23e74a4ffbe8b72453075c63fb27ad802eb9 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Mar 2018 07:52:13 +0100 Subject: [PATCH 423/509] Fix another UnicodeDecodeError for playlists - Should fix #419 --- resources/lib/playlist_func.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 86de6752..389d991f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -67,7 +67,7 @@ class PlaylistObjectBaseclase(object): else: # e.g. int answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) - return try_encode(answ + '\'items\': %s}}' % self.items) + return try_encode(answ + '\'items\': %s}}') % self.items def is_pkc_clear(self): """ @@ -206,13 +206,13 @@ class Playlist_Item(object): count = 0 # Kodi indexes differently than Plex for stream in self.xml[0][self.part]: - if (stream.attrib['streamType'] == stream_type and + if (stream.attrib['streamType'] == stream_type and 'key' in stream.attrib): if count == kodi_stream_index: return stream.attrib['id'] count += 1 for stream in self.xml[0][self.part]: - if (stream.attrib['streamType'] == stream_type and + if (stream.attrib['streamType'] == stream_type and 'key' not in stream.attrib): if count == kodi_stream_index: return stream.attrib['id'] From 11db94f84f21eb25a0e02551b2dfdd36d1cbdab7 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Mar 2018 08:40:18 +0100 Subject: [PATCH 424/509] Hardcode plugin-calls instead of using urlencode --- resources/lib/context_entry.py | 14 +++++--------- resources/lib/entrypoint.py | 17 ++++------------- resources/lib/itemtypes.py | 20 +++++++------------- resources/lib/playback.py | 12 ++++-------- 4 files changed, 20 insertions(+), 43 deletions(-) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 96ea0777..56d2f2b1 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -2,7 +2,7 @@ ############################################################################### from logging import getLogger -from xbmc import getInfoLabel, sleep, executebuiltin, getCondVisibility +from xbmc import getInfoLabel, sleep, executebuiltin from xbmcaddon import Addon import plexdb_functions as plexdb @@ -157,12 +157,8 @@ class ContextMenu(object): v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type]) playqueue.clear() state.CONTEXT_MENU_PLAY = True - params = { - 'mode': 'play', - 'plex_id': self.plex_id, - 'plex_type': self.plex_type - } - from urllib import urlencode - handle = ("plugin://plugin.video.plexkodiconnect/movies?%s" - % urlencode(params)) + handle = ('plugin://%s/?plex_id=%s&plex_type=%s&mode=play' + % (v.ADDON_TYPE[self.plex_type], + self.plex_id, + self.plex_type)) executebuiltin('RunPlugin(%s)' % handle) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 6bb54d2d..c2741eee 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -562,13 +562,8 @@ def getOnDeck(viewid, mediatype, tagname, limit): if directpaths: url = api.file_path() else: - params = { - 'mode': "play", - 'plex_id': api.plex_id(), - 'plex_type': api.plex_type() - } - url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \ - % urlencode(params) + url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play' + % (v.ADDON_ID, api.plex_id(), api.plex_type())) xbmcplugin.addDirectoryItem( handle=HANDLE, url=url, @@ -836,12 +831,8 @@ def __build_item(xml_element): elif api.plex_type() == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: - params = { - 'mode': 'play', - 'plex_id': api.plex_id(), - 'plex_type': api.plex_type(), - } - url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params)) + url = 'plugin://%s/?plex_id=%s&plex_type=%s&mode=play' \ + % (v.ADDON_TYPE[api.plex_type()], api.plex_id(), api.plex_type()) xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=listitem) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 410066e5..6b465bc7 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### from logging import getLogger -from urllib import urlencode from ntpath import dirname from datetime import datetime @@ -18,6 +17,9 @@ import state LOG = getLogger("PLEX." + __name__) +# Note: always use same order of URL arguments, NOT urlencode: +# plex_id=&plex_type=&mode=play + ############################################################################### @@ -284,12 +286,8 @@ class Movies(Items): if do_indirect: # Set plugin path and media flags using real filename path = 'plugin://%s.movies/' % v.ADDON_ID - params = { - 'mode': 'play', - 'plex_id': itemid, - 'plex_type': v.PLEX_TYPE_MOVIE - } - filename = "%s?%s" % (path, urlencode(params)) + filename = ('%s?plex_id=%s&plex_type=%s&mode=play' + % (path, itemid, v.PLEX_TYPE_MOVIE)) playurl = filename # movie table: @@ -925,12 +923,8 @@ class TVShows(Items): # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) - params = { - 'plex_id': itemid, - 'plex_type': v.PLEX_TYPE_EPISODE, - 'mode': 'play' - } - filename = "%s?%s" % (path, urlencode(params)) + filename = ('%s?plex_id=%s&plex_type=%s&mode=play' + % (path, itemid, v.PLEX_TYPE_EPISODE)) playurl = filename parent_path_id = self.kodi_db.getParentPathId(path) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 7c481ad5..a0723a9d 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -3,7 +3,6 @@ Used to kick off Kodi playback """ from logging import getLogger from threading import Thread -from urllib import urlencode from xbmc import Player, sleep @@ -205,13 +204,10 @@ def _prep_playlist_stack(xml): api.set_part_number(part) if kodi_id is None: # Need to redirect again to PKC to conclude playback - params = { - 'mode': 'play', - 'plex_id': api.plex_id(), - 'plex_type': api.plex_type() - } - path = ('plugin://%s/?%s' - % (v.ADDON_TYPE[api.plex_type()], urlencode(params))) + path = ('plugin://%s/?plex_id=%s&plex_type=%s&mode=play' + % (v.ADDON_TYPE[api.plex_type()], + api.plex_id(), + api.plex_type())) listitem = api.create_listitem() listitem.setPath(try_encode(path)) else: From 79dba00f275e32c49ef775fbdb0459a32dc56798 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Mar 2018 08:52:05 +0100 Subject: [PATCH 425/509] Fix Kodi 18 log warnings by declaring all settings variables - Fixes #414 --- resources/settings.xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/settings.xml b/resources/settings.xml index 2470f1b4..0ef67089 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -38,10 +38,21 @@ - + + + + + + + + + + + + From fe01405d3e2ad4003e733caa16ada35cb4c2736b Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 7 Mar 2018 08:54:14 +0100 Subject: [PATCH 426/509] Version bump --- README.md | 2 +- addon.xml | 9 +++++++-- changelog.txt | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 80bf5ed8..65857b7d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.6-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.7-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index db91c64b..3fd427dd 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,12 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.6 (beta only): + version 2.0.7 (beta only): +- Fix another UnicodeDecodeError for playlists +- Hardcode plugin-calls instead of using urlencode +- Fix Kodi 18 log warnings by declaring all settings variables + +version 2.0.6 (beta only): - Addon paths playback was basically broken - hope it works again! - Fixes to add-on paths playback startup - Fix UnicodeDecodeError for playqueue logging diff --git a/changelog.txt b/changelog.txt index 9b17aee4..3deff21f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +version 2.0.7 (beta only): +- Fix another UnicodeDecodeError for playlists +- Hardcode plugin-calls instead of using urlencode +- Fix Kodi 18 log warnings by declaring all settings variables + version 2.0.6 (beta only): - Addon paths playback was basically broken - hope it works again! - Fixes to add-on paths playback startup From 2cd00f21b7628243732600fa91a67d296b7958ee Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 12:24:57 +0100 Subject: [PATCH 427/509] Improve playback startup resiliance - Fixes #426 --- resources/lib/playback.py | 40 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index a0723a9d..dd233d95 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -54,7 +54,7 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): LOG.error('Not yet authenticated for PMS, abort starting playback') # "Unauthorized for PMS" dialog('notification', lang(29999), lang(30017)) - _ensure_resolve() + _ensure_resolve(abort=True) return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) @@ -64,8 +64,12 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): LOG.debug('playQueue position: %s for %s', pos, playqueue) # Have we already initiated playback? try: - playqueue.items[pos] + item = playqueue.items[pos] except IndexError: + initiate = True + else: + initiate = True if item.plex_id != plex_id else False + if initiate: _playback_init(plex_id, plex_type, playqueue, pos) else: # kick off playback on second pass @@ -84,7 +88,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos): LOG.error('Could not get a PMS xml for plex id %s', plex_id) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') - _ensure_resolve() + _ensure_resolve(abort=True) return if playqueue.kodi_pl.size() > 1: # Special case - we already got a filled Kodi playqueue @@ -92,7 +96,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos): _init_existing_kodi_playlist(playqueue) except PL.PlaylistError: LOG.error('Aborting playback_init for longer Kodi playlist') - _ensure_resolve() + _ensure_resolve(abort=True) return # Now we need to use setResolvedUrl for the item at position pos _conclude_playback(playqueue, pos) @@ -101,9 +105,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos): # playqueues # Fail the item we're trying to play now so we can restart the player _ensure_resolve() - api = API(xml[0]) trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and + if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUME_PLAYBACK and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" @@ -124,7 +127,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos): plex_id, xml.attrib.get('librarySectionUUID')) # "Play error" dialog('notification', lang(29999), lang(30128), icon='{error}') - _ensure_resolve() + _ensure_resolve(abort=True) return # Should already be empty, but just in case PL.get_playlist_details_from_xml(playqueue, xml) @@ -135,11 +138,13 @@ def _playback_init(plex_id, plex_type, playqueue, pos): # Reset some playback variables state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False + # Do NOT set offset, because Kodi player will return here to resolveURL # New thread to release this one sooner (e.g. harddisk spinning up) - thread = Thread(target=Player().play, - args=(playqueue.kodi_pl, )) + thread = Thread(target=threaded_playback, + args=(playqueue.kodi_pl, pos, None)) thread.setDaemon(True) - LOG.info('Done initializing PKC playback, starting Kodi player') + LOG.info('Done initializing playback, starting Kodi player at pos %s', + pos) # By design, PKC will start Kodi playback using Player().play(). Kodi # caches paths like our plugin://pkc. If we use Player().play() between # 2 consecutive startups of exactly the same Kodi library item, Kodi's @@ -149,7 +154,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos): thread.start() -def _ensure_resolve(): +def _ensure_resolve(abort=False): """ Will check whether RESOLVE=True and if so, fail Kodi playback startup with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some @@ -158,11 +163,17 @@ def _ensure_resolve(): This way we're making sure that other Python instances (calling default.py) will be destroyed. """ - if RESOLVE is True: + if RESOLVE: state.PKC_CAUSED_STOP = True result = Playback_Successful() result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) + if abort: + # Reset some playback variables + state.CONTEXT_MENU_PLAY = False + state.FORCE_TRANSCODE = False + state.RESUMABLE = False + state.RESUME_PLAYBACK = False def _init_existing_kodi_playlist(playqueue): @@ -291,6 +302,7 @@ def _conclude_playback(playqueue, pos): listitem.setSubtitles(api.cache_external_subs()) elif item.playmethod == 'Transcode': playutils.audio_subtitle_prefs(listitem) + if state.RESUME_PLAYBACK is True: state.RESUME_PLAYBACK = False if (item.offset is None and @@ -335,7 +347,7 @@ def process_indirect(key, offset, resolve=True): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download PMS metadata') - _ensure_resolve() + _ensure_resolve(abort=True) return if offset != '0': offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) @@ -358,7 +370,7 @@ def process_indirect(key, offset, resolve=True): xml[0].attrib except (TypeError, IndexError, AttributeError): LOG.error('Could not download last xml for playurl') - _ensure_resolve() + _ensure_resolve(abort=True) return playurl = xml[0].attrib['key'] item.file = playurl From 344e4337e1ea700cb5676160b61289076a26a310 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 12:45:50 +0100 Subject: [PATCH 428/509] Fix settings not being picked up correctly --- resources/lib/librarysync.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index f012e59b..16517dd4 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1477,11 +1477,9 @@ class LibrarySync(Thread): stopped = self.stopped suspended = self.suspended installSyncDone = self.installSyncDone - background_sync = state.BACKGROUND_SYNC fullSync = self.fullSync processMessage = self.processMessage processItems = self.processItems - FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL lastSync = 0 lastTimeSync = 0 lastProcessing = 0 @@ -1599,7 +1597,7 @@ class LibrarySync(Thread): now = unix_timestamp() # Standard syncs - don't force-show dialogs self.force_dialog = False - if (now - lastSync > FULL_SYNC_INTERVALL and + if (now - lastSync > state.FULL_SYNC_INTERVALL and not self.xbmcplayer.isPlaying()): lastSync = now log.info('Doing scheduled full library scan') @@ -1623,7 +1621,7 @@ class LibrarySync(Thread): self.syncPMStime() window('plex_dbScan', clear=True) state.DB_SCAN = False - elif background_sync: + elif state.BACKGROUND_SYNC: # Check back whether we should process something # Only do this once every while (otherwise, potentially # many screen refreshes lead to flickering) From d74c26fd4c46613394355c55081b5bb19a3cfa26 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 12:58:11 +0100 Subject: [PATCH 429/509] Fix disabling of background sync (websockets) - Partially fixes #425 --- resources/lib/initialsetup.py | 4 ++-- resources/lib/kodimonitor.py | 3 ++- resources/lib/librarysync.py | 2 +- resources/lib/state.py | 4 ++-- resources/lib/websocket_client.py | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index aa92173f..34544c47 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -60,8 +60,8 @@ def reload_pkc(): state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' state.ENABLE_MUSIC = settings('enableMusic') == 'true' - state.BACKGROUND_SYNC = settings( - 'enableBackgroundSync') == 'true' + state.BACKGROUND_SYNC_DISABLED = settings( + 'enableBackgroundSync') == 'false' state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 289dbe22..5a55a390 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -46,7 +46,6 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'enableBackgroundSync': 'BACKGROUND_SYNC', 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } @@ -111,6 +110,8 @@ class KodiMonitor(Monitor): plex_command('RUN_LIB_SCAN', 'views') # Special cases, overwrite all internal settings set_replace_paths() + state.BACKGROUND_SYNC_DISABLED = settings( + 'enableBackgroundSync') == 'false' state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( settings('backgroundsync_saftyMargin')) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 16517dd4..8bba4960 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1621,7 +1621,7 @@ class LibrarySync(Thread): self.syncPMStime() window('plex_dbScan', clear=True) state.DB_SCAN = False - elif state.BACKGROUND_SYNC: + elif not state.BACKGROUND_SYNC_DISABLED: # Check back whether we should process something # Only do this once every while (otherwise, potentially # many screen refreshes lead to flickering) diff --git a/resources/lib/state.py b/resources/lib/state.py index 31085897..4fbdc907 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -40,8 +40,8 @@ KODI_DB_CHECKED = False ENABLE_MUSIC = True # How often shall we sync? FULL_SYNC_INTERVALL = 0 -# Background Sync enabled at all? -BACKGROUND_SYNC = True +# Background Sync disabled? +BACKGROUND_SYNC_DISABLED = False # How long shall we wait with synching a new item to make sure Plex got all # metadata? BACKGROUNDSYNC_SAFTYMARGIN = 0 diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index cbc2f681..ff636c87 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -141,7 +141,8 @@ class WebSocket(Thread): LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) -@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', + 'BACKGROUND_SYNC_DISABLED']) class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion From 44073a3201a8da7978df8f061e4e25b371131574 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 14:51:00 +0100 Subject: [PATCH 430/509] Optimize DB path updates for TV shows --- resources/lib/PlexAPI.py | 2 +- resources/lib/itemtypes.py | 65 ++++++++++--------------------- resources/lib/kodidb_functions.py | 46 ++++++++++++---------- 3 files changed, 47 insertions(+), 66 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 030aff5f..f4f67cb2 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1442,7 +1442,7 @@ class API(object): omit_check : Will entirely omit validity check if True """ if path is None: - return None + return typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus] if state.REMAP_PATH is True: path = path.replace(getattr(state, 'remapSMB%sOrg' % typus), diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 6b465bc7..a092c73e 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -531,10 +531,8 @@ class TVShows(Items): plex_db = self.plex_db artwork = self.artwork api = API(item) - update_item = True itemid = api.plex_id() - if not itemid: LOG.error("Cannot parse XML data for TV show") return @@ -547,7 +545,6 @@ class TVShows(Items): update_item = False kodicursor.execute("select coalesce(max(idShow),0) from tvshow") showid = kodicursor.fetchone()[0] + 1 - else: # Verification the item is still in Kodi query = "SELECT * FROM tvshow WHERE idShow = ?" @@ -562,7 +559,6 @@ class TVShows(Items): # fileId information checksum = api.checksum() - # item details genres = api.genre_list() title, sorttitle = api.titles() @@ -581,37 +577,34 @@ class TVShows(Items): studio = None # GET THE FILE AND PATH ##### - do_indirect = not state.DIRECT_PATHS if state.DIRECT_PATHS: # Direct paths is set the Kodi way - playurl = api.tv_show_path() + playurl = api.validate_playurl(api.tv_show_path(), + api.plex_type(), + folder=True) if playurl is None: - # Something went wrong, trying to use non-direct paths - do_indirect = True + return + if "\\" in playurl: + # Local path + path = "%s\\" % playurl + toplevelpath = "%s\\" % dirname(dirname(path)) else: - playurl = api.validate_playurl(playurl, - api.plex_type(), - folder=True) - if playurl is None: - return False - if "\\" in playurl: - # Local path - path = "%s\\" % playurl - toplevelpath = "%s\\" % dirname(dirname(path)) - else: - # Network path - path = "%s/" % playurl - toplevelpath = "%s/" % dirname(dirname(path)) - if do_indirect: + # Network path + path = "%s/" % playurl + toplevelpath = "%s/" % dirname(dirname(path)) + toppathid = self.kodi_db.add_video_path( + toplevelpath, + content='tvshows', + scraper='metadata.local') + else: # Set plugin path toplevelpath = "plugin://%s.tvshows/" % v.ADDON_ID path = "%s%s/" % (toplevelpath, itemid) + toppathid = self.kodi_db.get_path(toplevelpath) - # Add top path - toppathid = self.kodi_db.add_video_path(toplevelpath) - # add/retrieve pathid and fileid - # if the path or file already exists, the calls return current value - pathid = self.kodi_db.add_video_path(path) + pathid = self.kodi_db.add_video_path(path, + date_added=api.date_created(), + id_parent_path=toppathid) # UPDATE THE TVSHOW ##### if update_item: LOG.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) @@ -671,16 +664,6 @@ class TVShows(Items): # OR ADD THE TVSHOW ##### else: LOG.info("ADD tvshow itemid: %s - Title: %s", itemid, title) - query = ''' - UPDATE path - SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ? - WHERE idPath = ? - ''' - kodicursor.execute(query, (toplevelpath, - "tvshows", - "metadata.local", - 1, - toppathid)) # Link the path query = "INSERT INTO tvshowlinkpath(idShow, idPath) values (?, ?)" kodicursor.execute(query, (showid, pathid)) @@ -733,14 +716,6 @@ class TVShows(Items): kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre, title, tvdb, mpaa, studio, sorttitle)) - # Update the path - query = ''' - UPDATE path - SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?, - idParentPath = ? - WHERE idPath = ? - ''' - kodicursor.execute(query, (path, None, None, 1, toppathid, pathid)) self.kodi_db.modify_people(showid, v.KODI_TYPE_SHOW, api.people_list()) self.kodi_db.modify_genres(showid, v.KODI_TYPE_SHOW, genres) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 8dd36633..de8fb655 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -60,7 +60,7 @@ class KodiDBMethods(object): For some reason, Kodi ignores this if done via itemtypes while e.g. adding or updating items. (addPath method does NOT work) """ - path_id = self.getPath('plugin://%s.movies/' % v.ADDON_ID) + path_id = self.get_path('plugin://%s.movies/' % v.ADDON_ID) if path_id is None: self.cursor.execute("select coalesce(max(idPath),0) from path") path_id = self.cursor.fetchone()[0] + 1 @@ -80,7 +80,7 @@ class KodiDBMethods(object): 1, 0)) # And TV shows - path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID) + path_id = self.get_path('plugin://%s.tvshows/' % v.ADDON_ID) if path_id is None: self.cursor.execute("select coalesce(max(idPath),0) from path") path_id = self.cursor.fetchone()[0] + 1 @@ -111,7 +111,7 @@ class KodiDBMethods(object): else: # Network path parentpath = "%s/" % dirname(dirname(path)) - pathid = self.getPath(parentpath) + pathid = self.get_path(parentpath) if pathid is None: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 @@ -126,8 +126,16 @@ class KodiDBMethods(object): self.cursor.execute(query, (parent_path_id, pathid)) return pathid - def add_video_path(self, path): - # SQL won't return existing paths otherwise + def add_video_path(self, path, date_added=None, id_parent_path=None, + content=None, scraper=None): + """ + Returns the idPath from the path table. Creates a new entry if path + [unicode] does not yet exist (using date_added [kodi date type], + id_parent_path [int], content ['tvshows', 'movies', None], scraper + [usually 'metadata.local']) + + WILL activate noUpdate for the path! + """ if path is None: path = '' query = 'SELECT idPath FROM path WHERE strPath = ?' @@ -137,12 +145,14 @@ class KodiDBMethods(object): except TypeError: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 - datetime = unix_date_to_kodi(unix_timestamp()) query = ''' - INSERT INTO path(idPath, strPath, dateAdded) - VALUES (?, ?, ?) + INSERT INTO path(idPath, strPath, dateAdded, idParentPath, + strContent, strScraper, noUpdate) + VALUES (?, ?, ?, ?, ?, ?, ?) ''' - self.cursor.execute(query, (pathid, path, datetime)) + self.cursor.execute(query, + (pathid, path, date_added, id_parent_path, + content, scraper, 1)) return pathid def add_music_path(self, path, strHash=None): @@ -163,20 +173,16 @@ class KodiDBMethods(object): self.cursor.execute(query, (pathid, path, strHash)) return pathid - def getPath(self, path): - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - self.cursor.execute(query, (path,)) + def get_path(self, path): + """ + Returns the idPath from the path table for path [unicode] or None + """ + self.cursor.execute('SELECT idPath FROM path WHERE strPath = ?', + (path,)) try: pathid = self.cursor.fetchone()[0] except TypeError: pathid = None - return pathid def addFile(self, filename, pathid): @@ -224,7 +230,7 @@ class KodiDBMethods(object): def removeFile(self, path, filename): - pathid = self.getPath(path) + pathid = self.get_path(path) if pathid is not None: query = ' '.join(( From 04f94f0828b64bd003cbe7dfb0e17d86d556c857 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 14:53:39 +0100 Subject: [PATCH 431/509] Use api method for parent id --- resources/lib/itemtypes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index a092c73e..34601af5 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -745,7 +745,7 @@ class TVShows(Items): artwork = self.artwork seasonnum = api.season_number() # Get parent tv show Plex id - plexshowid = item.attrib.get('parentRatingKey') + plexshowid = api.parent_plex_id() # Get Kodi showid plex_dbitem = plex_db.getItem_byId(plexshowid) try: @@ -1473,7 +1473,7 @@ class Music(Items): studio, albumid)) # Associate the parentid for plex reference - parent_id = item.attrib.get('parentRatingKey') + parent_id = api.parent_plex_id() if parent_id is not None: plex_dbartist = plex_db.getItem_byId(parent_id) try: @@ -1674,8 +1674,7 @@ class Music(Items): try: # Get the album - plex_dbalbum = plex_db.getItem_byId( - item.attrib.get('parentRatingKey')) + plex_dbalbum = plex_db.getItem_byId(api.parent_plex_id()) albumid = plex_dbalbum[0] except KeyError: # Verify if there's an album associated. @@ -1700,7 +1699,7 @@ class Music(Items): except TypeError: # No album found. Let's create it LOG.info("Album database entry missing.") - plex_album_id = item.attrib.get('parentRatingKey') + plex_album_id = api.parent_plex_id() album = GetPlexMetadata(plex_album_id) if album is None or album == 401: LOG.error('Could not download album, abort') From e642e309787048777e634b20614e3957b393231a Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 14:56:24 +0100 Subject: [PATCH 432/509] New api method grandparent title --- resources/lib/PlexAPI.py | 7 +++++++ resources/lib/itemtypes.py | 9 ++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index f4f67cb2..843d85ec 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -539,6 +539,13 @@ class API(object): """ return self.item.get('grandparentRatingKey') + def grandparent_title(self): + """ + Returns the title for the corresponding grandparent, e.g. a TV show + name for episodes, or None + """ + return self.item.get('grandparentTitle') + def episode_data(self): """ Call on a single episode. diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 34601af5..03c83a00 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1589,13 +1589,12 @@ class Music(Items): genre = None try: if self.compilation == 0: - artists = item.attrib.get('grandparentTitle') + artists = api.grandparent_title() else: artists = item.attrib.get('originalTitle') except AttributeError: # compilation not set - artists = item.attrib.get('originalTitle', - item.attrib.get('grandparentTitle')) + artists = item.attrib.get('originalTitle', api.grandparent_title()) tracknumber = int(item.attrib.get('index', 0)) disc = int(item.attrib.get('parentIndex', 1)) if disc == 1: @@ -1780,8 +1779,8 @@ class Music(Items): # Link song to artists artist_loop = [{ - 'Name': item.attrib.get('grandparentTitle'), - 'Id': item.attrib.get('grandparentRatingKey') + 'Name': api.grandparent_title(), + 'Id': api.grandparent_id() }] # for index, artist in enumerate(item['ArtistItems']): for index, artist in enumerate(artist_loop): From 54a231a67f9bbc411423247ee54d330c38634595 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 14:56:46 +0100 Subject: [PATCH 433/509] Remove comments --- resources/lib/itemtypes.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 03c83a00..f6d18702 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -847,21 +847,6 @@ class TVShows(Items): season = -1 if episode is None: episode = -1 - # if item.get('AbsoluteEpisodeNumber'): - # # Anime scenario - # season = 1 - # episode = item['AbsoluteEpisodeNumber'] - # else: - # season = -1 - - # # Specials ordering within season - # if item.get('AirsAfterSeasonNumber'): - # airs_before_season = item['AirsAfterSeasonNumber'] - # # Kodi default number for afterseason ordering - # airs_before_episode = 4096 - # else: - # airs_before_season = item.get('AirsBeforeSeasonNumber') - # airs_before_episode = item.get('AirsBeforeEpisodeNumber') airs_before_season = "-1" airs_before_episode = "-1" From f2fea1bcde7f32d1142d54bdc172fedf4ef16dfe Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 15:02:06 +0100 Subject: [PATCH 434/509] Optimize Kodi db method add_season --- resources/lib/itemtypes.py | 4 ++-- resources/lib/kodidb_functions.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f6d18702..fdda7342 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -755,7 +755,7 @@ class TVShows(Items): 'Skipping season for now.', plex_id) return - seasonid = self.kodi_db.addSeason(showid, seasonnum) + seasonid = self.kodi_db.add_season(showid, seasonnum) checksum = api.checksum() # Check whether Season already exists plex_dbitem = plex_db.getItem_byId(plex_id) @@ -857,7 +857,7 @@ class TVShows(Items): except TypeError: LOG.error("Parent tvshow now found, skip item") return False - seasonid = self.kodi_db.addSeason(showid, season) + seasonid = self.kodi_db.add_season(showid, season) # GET THE FILE AND PATH ##### do_indirect = not state.DIRECT_PATHS diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index de8fb655..f4d814dc 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -824,24 +824,23 @@ class KodiDBMethods(object): query = 'DELETE FROM sets WHERE idSet = ?' self.cursor.execute(query, (set_id,)) - def addSeason(self, showid, seasonnumber): - - query = ' '.join(( - - "SELECT idSeason", - "FROM seasons", - "WHERE idShow = ?", - "AND season = ?" - )) + def add_season(self, showid, seasonnumber): + """ + Adds a TV show season to the Kodi video DB or simply returns the ID, + if there already is an entry in the DB + """ + query = 'SELECT idSeason FROM seasons WHERE idShow = ? AND season = ?' self.cursor.execute(query, (showid, seasonnumber,)) try: seasonid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idSeason),0) from seasons") + self.cursor.execute("SELECT COALESCE(MAX(idSeason),0) FROM seasons") seasonid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" + query = ''' + INSERT INTO seasons(idSeason, idShow, season) + VALUES (?, ?, ?) + ''' self.cursor.execute(query, (seasonid, showid, seasonnumber)) - return seasonid def addArtist(self, name, musicbrainz): From 5a2d3f4238b923be7e6bf916ef9c304d3f5c2bb0 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 15:03:31 +0100 Subject: [PATCH 435/509] Optimize code --- resources/lib/itemtypes.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index fdda7342..1aaf08cf 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -754,20 +754,15 @@ class TVShows(Items): LOG.error('Could not find parent tv show for season %s. ' 'Skipping season for now.', plex_id) return - seasonid = self.kodi_db.add_season(showid, seasonnum) checksum = api.checksum() # Check whether Season already exists plex_dbitem = plex_db.getItem_byId(plex_id) update_item = False if plex_dbitem is None else True - - # Process artwork - allartworks = api.artwork() - artwork.modify_artwork(allartworks, + artwork.modify_artwork(api.artwork(), seasonid, v.KODI_TYPE_SEASON, kodicursor) - if update_item: # Update a reference: checksum in plex table plex_db.updateReference(plex_id, checksum) From 5882a6ef3beea9cbf7db0db7425c55275b977f36 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 15:44:08 +0100 Subject: [PATCH 436/509] Optimize code --- resources/lib/itemtypes.py | 69 +++++++++++-------------------- resources/lib/kodidb_functions.py | 39 +++++++---------- 2 files changed, 38 insertions(+), 70 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 1aaf08cf..e3dc0a52 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -298,7 +298,7 @@ class Movies(Items): # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value pathid = self.kodi_db.add_video_path(path) - fileid = self.kodi_db.addFile(filename, pathid) + fileid = self.kodi_db.add_file(filename, pathid) # UPDATE THE MOVIE ##### if update_item: @@ -785,10 +785,6 @@ class TVShows(Items): plex_db = self.plex_db artwork = self.artwork api = API(item) - - # If the item already exist in the local Kodi DB we'll perform a full - # item update - # If the item doesn't exist, we'll add it to the database update_item = True itemid = api.plex_id() if not itemid: @@ -802,18 +798,18 @@ class TVShows(Items): except TypeError: update_item = False # episodeid - kodicursor.execute("select coalesce(max(idEpisode),0) from episode") + kodicursor.execute('SELECT COALESCE(MAX(idEpisode),0) FROM episode') episodeid = kodicursor.fetchone()[0] + 1 else: # Verification the item is still in Kodi - query = "SELECT * FROM episode WHERE idEpisode = ?" + query = 'SELECT * FROM episode WHERE idEpisode = ?' kodicursor.execute(query, (episodeid,)) try: kodicursor.fetchone()[0] except TypeError: # item is not found, let's recreate it. update_item = False - LOG.info("episodeid: %s missing from Kodi, repairing entry.", + LOG.info('episodeid: %s missing from Kodi, repairing entry.', episodeid) # fileId information @@ -855,38 +851,33 @@ class TVShows(Items): seasonid = self.kodi_db.add_season(showid, season) # GET THE FILE AND PATH ##### - do_indirect = not state.DIRECT_PATHS - playurl = api.file_path(force_first_media=True) if state.DIRECT_PATHS: - # Direct paths is set the Kodi way + playurl = api.file_path(force_first_media=True) + playurl = api.validate_playurl(playurl, api.plex_type()) if playurl is None: - # Something went wrong, trying to use non-direct paths - do_indirect = True + return False + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] else: - playurl = api.validate_playurl(playurl, api.plex_type()) - if playurl is None: - return False - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: - # Network share - filename = playurl.rsplit("/", 1)[1] - path = playurl.replace(filename, "") - parent_path_id = self.kodi_db.getParentPathId(path) - if do_indirect: + # Network share + filename = playurl.rsplit("/", 1)[1] + path = playurl.replace(filename, "") + parent_path_id = self.kodi_db.parent_path_id(path) + else: # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) filename = ('%s?plex_id=%s&plex_type=%s&mode=play' % (path, itemid, v.PLEX_TYPE_EPISODE)) playurl = filename - parent_path_id = self.kodi_db.getParentPathId(path) + parent_path_id = self.kodi_db.parent_path_id(path) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.add_video_path(path) - fileid = self.kodi_db.addFile(filename, pathid) + pathid = self.kodi_db.add_video_path(path, + id_parent_path=parent_path_id) + fileid = self.kodi_db.add_file(filename, pathid) # UPDATE THE EPISODE ##### if update_item: @@ -1024,24 +1015,12 @@ class TVShows(Items): parent_id=seasonid, checksum=checksum, view_id=viewid) - - # Update the path for Direct Paths only - if not do_indirect: - query = ''' - UPDATE path - SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?, - idParentPath = ? - WHERE idPath = ? - ''' - kodicursor.execute(query, (path, None, None, 1, parent_path_id, - pathid)) # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) + query = ''' + UPDATE files + SET idPath = ?, strFilename = ?, dateAdded = ? + WHERE idFile = ? + ''' kodicursor.execute(query, (pathid, filename, dateadded, fileid)) # Process cast self.kodi_db.modify_people(episodeid, diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f4d814dc..d6953065 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -100,10 +100,10 @@ class KodiDBMethods(object): 1, 0)) - def getParentPathId(self, path): + def parent_path_id(self, path): """ - Video DB: Adds all subdirectories to SQL path while setting a "trail" - of parentPathId + Video DB: Adds all subdirectories to path table while setting a "trail" + of parent path ids """ if "\\" in path: # Local path @@ -121,9 +121,9 @@ class KodiDBMethods(object): VALUES (?, ?, ?) ''' self.cursor.execute(query, (pathid, parentpath, datetime)) - parent_path_id = self.getParentPathId(parentpath) + parent_id = self.parent_path_id(parentpath) query = 'UPDATE path SET idParentPath = ? WHERE idPath = ?' - self.cursor.execute(query, (parent_path_id, pathid)) + self.cursor.execute(query, (parent_id, pathid)) return pathid def add_video_path(self, path, date_added=None, id_parent_path=None, @@ -185,31 +185,20 @@ class KodiDBMethods(object): pathid = None return pathid - def addFile(self, filename, pathid): - - query = ' '.join(( - - "SELECT idFile", - "FROM files", - "WHERE strFilename = ?", - "AND idPath = ?" - )) - self.cursor.execute(query, (filename, pathid,)) + def add_file(self, filename, path_id): + """ + Adds the filename [unicode] to the table files if not already added + and returns the idFile. + """ + query = 'SELECT idFile FROM files WHERE strFilename = ? AND idPath = ?' + self.cursor.execute(query, (filename, path_id)) try: fileid = self.cursor.fetchone()[0] except TypeError: - self.cursor.execute("select coalesce(max(idFile),0) from files") + self.cursor.execute('SELECT COALESCE(MAX(idFile), 0) FROM files') fileid = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO files( - idFile, strFilename) - - VALUES (?, ?) - ''' - ) + query = 'INSERT INTO files(idFile, strFilename) VALUES (?, ?)' self.cursor.execute(query, (fileid, filename)) - return fileid def getFile(self, fileid): From 97dc1c1856bbaf73539c15552a27d03ec3c11173 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 10 Mar 2018 17:09:21 +0100 Subject: [PATCH 437/509] Ensure file id references get deleted --- resources/lib/itemtypes.py | 62 +++++++------------------------ resources/lib/kodidb_functions.py | 56 ++++++++++++++-------------- 2 files changed, 41 insertions(+), 77 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index e3dc0a52..9b2e2736 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -203,7 +203,7 @@ class Movies(Items): plex_dbitem = plex_db.getItem_byId(itemid) try: movieid = plex_dbitem[0] - fileid = plex_dbitem[1] + old_fileid = plex_dbitem[1] pathid = plex_dbitem[2] except TypeError: @@ -283,12 +283,16 @@ class Movies(Items): # Network share filename = playurl.rsplit("/", 1)[1] path = playurl.replace(filename, "") + pathid = self.kodi_db.add_video_path(path, + content='movies', + scraper='metadata.local') if do_indirect: # Set plugin path and media flags using real filename path = 'plugin://%s.movies/' % v.ADDON_ID filename = ('%s?plex_id=%s&plex_type=%s&mode=play' % (path, itemid, v.PLEX_TYPE_MOVIE)) playurl = filename + pathid = self.kodi_db.get_path(path) # movie table: # c22 - playurl @@ -297,12 +301,13 @@ class Movies(Items): # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.add_video_path(path) - fileid = self.kodi_db.add_file(filename, pathid) + fileid = self.kodi_db.add_file(filename, pathid, dateadded) # UPDATE THE MOVIE ##### if update_item: LOG.info("UPDATE movie itemid: %s - Title: %s", itemid, title) + if fileid != old_fileid: + self.kodi_db.remove_file(old_fileid) # Update the movie entry if v.KODIVERSION >= 17: # update new ratings Kodi 17 @@ -415,25 +420,6 @@ class Movies(Items): parent_id=None, checksum=checksum, view_id=viewid) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid)) - - # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - # Process countries self.kodi_db.modify_countries(movieid, v.KODI_TYPE_MOVIE, countries) # Process cast @@ -793,7 +779,7 @@ class TVShows(Items): plex_dbitem = plex_db.getItem_byId(itemid) try: episodeid = plex_dbitem[0] - fileid = plex_dbitem[1] + old_fileid = plex_dbitem[1] pathid = plex_dbitem[2] except TypeError: update_item = False @@ -877,11 +863,13 @@ class TVShows(Items): # if the path or file already exists, the calls return current value pathid = self.kodi_db.add_video_path(path, id_parent_path=parent_path_id) - fileid = self.kodi_db.add_file(filename, pathid) + fileid = self.kodi_db.add_file(filename, pathid, dateadded) # UPDATE THE EPISODE ##### if update_item: LOG.info("UPDATE episode itemid: %s", itemid) + if fileid != old_fileid: + self.kodi_db.remove_file(old_fileid) # Update the movie entry if v.KODIVERSION >= 17: # update new ratings Kodi 17 @@ -913,7 +901,7 @@ class TVShows(Items): premieredate, runtime, director, season, episode, title, airs_before_season, airs_before_episode, playurl, pathid, fileid, seasonid, userdata['UserRating'], episodeid)) - elif v.KODIVERSION == 16: + else: # Kodi Jarvis query = ''' UPDATE episode @@ -926,18 +914,6 @@ class TVShows(Items): premieredate, runtime, director, season, episode, title, airs_before_season, airs_before_episode, playurl, pathid, fileid, seasonid, episodeid)) - else: - query = ''' - UPDATE episode - SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, - c10 = ?, c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, - c18 = ?, c19 = ?, idFile = ? - WHERE idEpisode = ? - ''' - kodicursor.execute(query, (title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, - airs_before_season, airs_before_episode, playurl, pathid, - fileid, episodeid)) # Update parentid reference plex_db.updateParentId(itemid, seasonid) @@ -1015,18 +991,9 @@ class TVShows(Items): parent_id=seasonid, checksum=checksum, view_id=viewid) - # Update the file - query = ''' - UPDATE files - SET idPath = ?, strFilename = ?, dateAdded = ? - WHERE idFile = ? - ''' - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - # Process cast self.kodi_db.modify_people(episodeid, v.KODI_TYPE_EPISODE, api.people_list()) - # Process artwork # Wide "screenshot" of particular episode poster = item.attrib.get('thumb') if poster: @@ -1034,11 +1001,8 @@ class TVShows(Items): "%s%s" % (self.server, poster)) artwork.modify_art( poster, episodeid, v.KODI_TYPE_EPISODE, "thumb", kodicursor) - - # Process stream details streams = api.mediastreams() self.kodi_db.modify_streams(fileid, streams, runtime) - # Process playstates self.kodi_db.addPlaystate(fileid, resume, runtime, diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index d6953065..1bb14c3c 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -185,7 +185,7 @@ class KodiDBMethods(object): pathid = None return pathid - def add_file(self, filename, path_id): + def add_file(self, filename, path_id, date_added): """ Adds the filename [unicode] to the table files if not already added and returns the idFile. @@ -193,13 +193,30 @@ class KodiDBMethods(object): query = 'SELECT idFile FROM files WHERE strFilename = ? AND idPath = ?' self.cursor.execute(query, (filename, path_id)) try: - fileid = self.cursor.fetchone()[0] + file_id = self.cursor.fetchone()[0] except TypeError: self.cursor.execute('SELECT COALESCE(MAX(idFile), 0) FROM files') - fileid = self.cursor.fetchone()[0] + 1 - query = 'INSERT INTO files(idFile, strFilename) VALUES (?, ?)' - self.cursor.execute(query, (fileid, filename)) - return fileid + file_id = self.cursor.fetchone()[0] + 1 + query = ''' + INSERT INTO files(idFile, idPath, strFilename, dateAdded) + VALUES (?, ?, ?, ?) + ''' + self.cursor.execute(query, (file_id, path_id, filename, date_added)) + return file_id + + def remove_file(self, file_id): + """ + Removes the entry for file_id from the files table. Will also delete + entries from the associated tables: bookmark, settings, streamdetails + """ + self.cursor.execute('DELETE FROM files WHERE idFile = ?', + (file_id,)) + self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', + (file_id,)) + self.cursor.execute('DELETE FROM settings WHERE idFile = ?', + (file_id,)) + self.cursor.execute('DELETE FROM streamdetails WHERE idFile = ?', + (file_id,)) def getFile(self, fileid): @@ -217,19 +234,6 @@ class KodiDBMethods(object): return filename - def removeFile(self, path, filename): - - pathid = self.get_path(path) - - if pathid is not None: - query = ' '.join(( - - "DELETE FROM files", - "WHERE idPath = ?", - "AND strFilename = ?" - )) - self.cursor.execute(query, (pathid, filename,)) - def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, table, key): query = ''' @@ -656,21 +660,17 @@ class KodiDBMethods(object): """ self.cursor.execute("DELETE FROM bookmark") - def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, + def addPlaystate(self, file_id, resume_seconds, total_seconds, playcount, dateplayed): # Delete existing resume point - query = ''' - DELETE FROM bookmark - WHERE idFile = ? - ''' - self.cursor.execute(query, (fileid,)) + self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', (file_id,)) # Set watched count query = ''' UPDATE files SET playCount = ?, lastPlayed = ? WHERE idFile = ? ''' - self.cursor.execute(query, (playcount, dateplayed, fileid)) + self.cursor.execute(query, (playcount, dateplayed, file_id)) # Set the resume bookmark if resume_seconds: self.cursor.execute( @@ -683,11 +683,11 @@ class KodiDBMethods(object): VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (bookmark_id, - fileid, + file_id, resume_seconds, total_seconds, '', - "VideoPlayer", + 'VideoPlayer', '', 1)) From a7939f8b241966fd4b8883658c8e2cc9cacb6270 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 11:47:04 +0100 Subject: [PATCH 438/509] Also delete orphaned path entries in Kodi DB --- resources/lib/itemtypes.py | 2 ++ resources/lib/kodidb_functions.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 9b2e2736..73c727ac 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -307,6 +307,7 @@ class Movies(Items): if update_item: LOG.info("UPDATE movie itemid: %s - Title: %s", itemid, title) if fileid != old_fileid: + LOG.debug('Removing old file entry: %s', old_fileid) self.kodi_db.remove_file(old_fileid) # Update the movie entry if v.KODIVERSION >= 17: @@ -869,6 +870,7 @@ class TVShows(Items): if update_item: LOG.info("UPDATE episode itemid: %s", itemid) if fileid != old_fileid: + LOG.debug('Removing old file entry: %s', old_fileid) self.kodi_db.remove_file(old_fileid) # Update the movie entry if v.KODIVERSION >= 17: diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 1bb14c3c..6579a117 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -209,6 +209,9 @@ class KodiDBMethods(object): Removes the entry for file_id from the files table. Will also delete entries from the associated tables: bookmark, settings, streamdetails """ + self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1', + (file_id,)) + path_id = self.cursor.fetchone()[0] self.cursor.execute('DELETE FROM files WHERE idFile = ?', (file_id,)) self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', @@ -217,6 +220,14 @@ class KodiDBMethods(object): (file_id,)) self.cursor.execute('DELETE FROM streamdetails WHERE idFile = ?', (file_id,)) + self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', + (file_id,)) + # Delete orphaned path entry + self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? LIMIT 1', + (path_id,)) + if self.cursor.fetchone() is None: + self.cursor.execute('DELETE FROM path WHERE idPath = ?', + (path_id,)) def getFile(self, fileid): From 456ef5cb3472b155092196ba6bc8d4f522dec908 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 11:48:54 +0100 Subject: [PATCH 439/509] Remove obsolete method --- resources/lib/kodidb_functions.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 6579a117..8d1765ba 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -229,22 +229,6 @@ class KodiDBMethods(object): self.cursor.execute('DELETE FROM path WHERE idPath = ?', (path_id,)) - def getFile(self, fileid): - - query = ' '.join(( - - "SELECT strFilename", - "FROM files", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (fileid,)) - try: - filename = self.cursor.fetchone()[0] - except TypeError: - filename = "" - - return filename - def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, table, key): query = ''' From 9101f49895045dd5c27ff71edd37c077f7e4c35e Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 12:00:28 +0100 Subject: [PATCH 440/509] Cleanly remove all file references --- resources/lib/itemtypes.py | 8 ++------ resources/lib/kodidb_functions.py | 6 ------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 73c727ac..8dd0ae0a 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -479,13 +479,10 @@ class Movies(Items): self.kodi_db.modify_genres(kodi_id, kodi_type) self.kodi_db.modify_studios(kodi_id, kodi_type) self.kodi_db.modify_tags(kodi_id, kodi_type) - self.kodi_db.modify_streams(file_id) - self.kodi_db.delete_playstate(file_id) # Delete kodi movie and file + self.kodi_db.remove_file(file_id) kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", - (file_id,)) if set_id: self.kodi_db.delete_possibly_empty_set(set_id) if v.KODIVERSION >= 17: @@ -1149,8 +1146,7 @@ class TVShows(Items): """ kodicursor = self.kodicursor self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE) - self.kodi_db.modify_streams(file_id) - self.kodi_db.delete_playstate(file_id) + self.kodi_db.remove_file(file_id) self.artwork.delete_artwork(kodi_id, "episode", kodicursor) kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 8d1765ba..e2e2867a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -686,12 +686,6 @@ class KodiDBMethods(object): '', 1)) - def delete_playstate(self, file_id): - """ - Removes all playstates/bookmarks for the file with file_id - """ - self.cursor.execute('DELETE FROM bookmark where idFile = ?', (file_id,)) - def createTag(self, name): # This will create and return the tag_id query = ' '.join(( From 2144995a29393a7658dac0c822e32debd71ec103 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 12:08:27 +0100 Subject: [PATCH 441/509] Optimize code for deleting movies from Kodi DB --- resources/lib/itemtypes.py | 33 +++++++++++++++---------------- resources/lib/kodidb_functions.py | 16 +++++++-------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 8dd0ae0a..ca13750d 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -449,15 +449,12 @@ class Movies(Items): # Process playstates self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) - def remove(self, itemid): + def remove(self, plex_id): """ - Remove movieid, fileid, plex reference + Remove a movie with all references and all orphaned associated entries + from the Kodi DB """ - plex_db = self.plex_db - kodicursor = self.kodicursor - artwork = self.artwork - - plex_dbitem = plex_db.getItem_byId(itemid) + plex_dbitem = self.plex_db.getItem_byId(plex_id) try: kodi_id = plex_dbitem[0] file_id = plex_dbitem[1] @@ -465,13 +462,14 @@ class Movies(Items): LOG.debug('Removing %sid: %s file_id: %s', kodi_type, kodi_id, file_id) except TypeError: + LOG.error('Movie with plex_id %s not found in DB - cannot delete', + plex_id) return # Remove the plex reference - plex_db.removeItem(itemid) + self.plex_db.removeItem(plex_id) # Remove artwork - artwork.delete_artwork(kodi_id, kodi_type, kodicursor) - + self.artwork.delete_artwork(kodi_id, kodi_type, self.kodicursor) if kodi_type == v.KODI_TYPE_MOVIE: set_id = self.kodi_db.get_set_id(kodi_id) self.kodi_db.modify_countries(kodi_id, kodi_type) @@ -481,8 +479,8 @@ class Movies(Items): self.kodi_db.modify_tags(kodi_id, kodi_type) # Delete kodi movie and file self.kodi_db.remove_file(file_id) - kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", - (kodi_id,)) + self.kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", + (kodi_id,)) if set_id: self.kodi_db.delete_possibly_empty_set(set_id) if v.KODIVERSION >= 17: @@ -490,16 +488,17 @@ class Movies(Items): self.kodi_db.remove_ratings(kodi_id, kodi_type) elif kodi_type == v.KODI_TYPE_SET: # Delete kodi boxset - boxset_movies = plex_db.getItem_byParentId(kodi_id, - v.KODI_TYPE_MOVIE) + boxset_movies = self.plex_db.getItem_byParentId(kodi_id, + v.KODI_TYPE_MOVIE) for movie in boxset_movies: plexid = movie[0] movieid = movie[1] self.kodi_db.removefromBoxset(movieid) # Update plex reference - plex_db.updateParentId(plexid, None) - kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,)) - LOG.debug("Deleted %s %s from kodi database", kodi_type, itemid) + self.plex_db.updateParentId(plexid, None) + self.kodicursor.execute("DELETE FROM sets WHERE idSet = ?", + (kodi_id,)) + LOG.debug("Deleted %s %s from kodi database", kodi_type, plex_id) class TVShows(Items): diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index e2e2867a..d458dab9 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -769,15 +769,13 @@ class KodiDBMethods(object): )) self.cursor.execute(query, (setid, movieid,)) - def removefromBoxset(self, movieid): - - query = ' '.join(( - - "UPDATE movie", - "SET idSet = null", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (movieid,)) + def remove_from_set(self, movieid): + """ + Remove the movie with movieid [int] from an associated movie set, movie + collection + """ + self.cursor.execute('UPDATE movie SET idSet = null WHERE idMovie = ?', + (movieid,)) def get_set_id(self, kodi_id): """ From 058d417e78357b4ec20e15f855ad23d471c9bf47 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 12:08:59 +0100 Subject: [PATCH 442/509] Fix AttributeError --- 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 ca13750d..10b52587 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -493,7 +493,7 @@ class Movies(Items): for movie in boxset_movies: plexid = movie[0] movieid = movie[1] - self.kodi_db.removefromBoxset(movieid) + self.kodi_db.remove_from_set(movieid) # Update plex reference self.plex_db.updateParentId(plexid, None) self.kodicursor.execute("DELETE FROM sets WHERE idSet = ?", From 3961c8bc21594f4615d7cad61dbb5b729f75fdda Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 13:02:04 +0100 Subject: [PATCH 443/509] Fix episode rating not being correct --- 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 10b52587..b6c09e60 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -895,7 +895,7 @@ class TVShows(Items): userrating = ? WHERE idEpisode = ? ''' - kodicursor.execute(query, (title, plot, rating, writer, + kodicursor.execute(query, (title, plot, ratingid, writer, premieredate, runtime, director, season, episode, title, airs_before_season, airs_before_episode, playurl, pathid, fileid, seasonid, userdata['UserRating'], episodeid)) From 8943083533677e10255fa5a1b4a1f4fb39a514ac Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 15:23:32 +0100 Subject: [PATCH 444/509] Fix tv shows not being correctly deleted - Fixes #375 --- resources/lib/itemtypes.py | 213 ++++++++++++------------------ resources/lib/librarysync.py | 3 +- resources/lib/plexdb_functions.py | 24 ++-- 3 files changed, 94 insertions(+), 146 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index b6c09e60..0d315e34 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -949,7 +949,7 @@ class TVShows(Items): episode, title, showid, airs_before_season, airs_before_episode, playurl, pathid, seasonid, userdata['UserRating'])) - elif v.KODIVERSION == 16: + else: # Kodi Jarvis query = ''' INSERT INTO episode( idEpisode, idFile, c00, c01, c03, c04, @@ -962,20 +962,6 @@ class TVShows(Items): rating, writer, premieredate, runtime, director, season, episode, title, showid, airs_before_season, airs_before_episode, playurl, pathid, seasonid)) - else: - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16, c18, c19) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (episodeid, fileid, title, plot, - rating, writer, premieredate, runtime, director, season, - episode, title, showid, airs_before_season, - airs_before_episode, playurl, pathid)) # Create or update the reference in plex table Add reference is # idempotent; the call here updates also fileid and pathid when item is @@ -1007,153 +993,118 @@ class TVShows(Items): playcount, dateplayed) - def remove(self, itemid): + @catch_exceptions(warnuser=True) + def remove(self, plex_id): """ - Remove showid, fileid, pathid, plex reference + Remove the entire TV shows object (show, season or episode) including + all associated entries from the Kodi DB. """ - plex_db = self.plex_db - kodicursor = self.kodicursor - - plex_dbitem = plex_db.getItem_byId(itemid) - try: - kodiid = plex_dbitem[0] - fileid = plex_dbitem[1] - parentid = plex_dbitem[3] - mediatype = plex_dbitem[4] - LOG.info("Removing %s kodiid: %s fileid: %s", - mediatype, kodiid, fileid) - except TypeError: + plex_dbitem = self.plex_db.getItem_byId(plex_id) + if plex_dbitem is None: + LOG.info('Cannot delete plex_id %s - not found in DB', plex_id) return - - ##### PROCESS ITEM ##### + kodi_id = plex_dbitem[0] + file_id = plex_dbitem[1] + parent_id = plex_dbitem[3] + kodi_type = plex_dbitem[4] + LOG.info("Removing %s with kodi_id: %s file_id: %s parent_id: %s", + kodi_type, kodi_id, file_id, parent_id) # Remove the plex reference - plex_db.removeItem(itemid) - - ##### IF EPISODE ##### - - if mediatype == v.KODI_TYPE_EPISODE: - # Delete kodi episode and file, verify season and tvshow - self.removeEpisode(kodiid, fileid) + self.plex_db.removeItem(plex_id) + ##### EPISODE ##### + if kodi_type == v.KODI_TYPE_EPISODE: + # Delete episode, verify season and tvshow + self.remove_episode(kodi_id, file_id) # Season verification - season = plex_db.getItem_byKodiId(parentid, v.KODI_TYPE_SEASON) - try: - showid = season[1] - except TypeError: - return - season_episodes = plex_db.getItem_byParentId(parentid, - v.KODI_TYPE_EPISODE) - if not season_episodes: - self.removeSeason(parentid) - plex_db.removeItem(season[0]) - - # Show verification - show = plex_db.getItem_byKodiId(showid, v.KODI_TYPE_SHOW) - query = ' '.join(( - - "SELECT totalCount", - "FROM tvshowcounts", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (showid,)) - 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, - v.KODI_TYPE_SEASON) - for season in seasons: - self.removeSeason(season[1]) - # Delete plex season entries - plex_db.removeItems_byParentId(showid, - v.KODI_TYPE_SEASON) - self.removeShow(showid) - plex_db.removeItem(show[0]) - - ##### IF TVSHOW ##### - - elif mediatype == v.KODI_TYPE_SHOW: - # Remove episodes, seasons, tvshow - seasons = plex_db.getItem_byParentId(kodiid, - v.KODI_TYPE_SEASON) - for season in seasons: - seasonid = season[1] - season_episodes = plex_db.getItem_byParentId( - seasonid, v.KODI_TYPE_EPISODE) - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - # Remove plex episodes - plex_db.removeItems_byParentId(seasonid, - v.KODI_TYPE_EPISODE) - # Remove plex seasons - plex_db.removeItems_byParentId(kodiid, - v.KODI_TYPE_SEASON) - - # Remove tvshow - self.removeShow(kodiid) - - ##### IF SEASON ##### - - elif mediatype == v.KODI_TYPE_SEASON: + season = self.plex_db.getItem_byKodiId(parent_id, + v.KODI_TYPE_SEASON) + if not self.plex_db.getItem_byParentId(parent_id, + v.KODI_TYPE_EPISODE): + # No episode left for season - so delete the season + self.remove_season(parent_id) + self.plex_db.removeItem(season[0]) + show = self.plex_db.getItem_byKodiId(season[1], + v.KODI_TYPE_SHOW) + if not self.plex_db.getItem_byParentId(season[1], + v.KODI_TYPE_SEASON): + # No seasons for show left - so delete entire show + self.remove_show(season[1]) + self.plex_db.removeItem(show[0]) + ##### SEASON ##### + elif kodi_type == v.KODI_TYPE_SEASON: # Remove episodes, season, verify tvshow - season_episodes = plex_db.getItem_byParentId(kodiid, - v.KODI_TYPE_EPISODE) - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - # Remove plex episodes - plex_db.removeItems_byParentId(kodiid, v.KODI_TYPE_EPISODE) - + for episode in self.plex_db.getItem_byParentId( + kodi_id, v.KODI_TYPE_EPISODE): + self.remove_episode(episode[1], episode[2]) + self.plex_db.removeItem(episode[0]) # Remove season - self.removeSeason(kodiid) - + self.remove_season(kodi_id) # Show verification - seasons = plex_db.getItem_byParentId(parentid, v.KODI_TYPE_SEASON) - if not seasons: - # There's no seasons, delete the show - self.removeShow(parentid) - plex_db.removeItem_byKodiId(parentid, v.KODI_TYPE_SHOW) - LOG.debug("Deleted %s: %s from kodi database", mediatype, itemid) + if not self.plex_db.getItem_byParentId(parent_id, + v.KODI_TYPE_SEASON): + # There's no other season left, delete the show + self.remove_show(parent_id) + self.plex_db.removeItem_byKodiId(parent_id, v.KODI_TYPE_SHOW) + ##### TVSHOW ##### + elif kodi_type == v.KODI_TYPE_SHOW: + # Remove episodes, seasons and the tvshow itself + for season in self.plex_db.getItem_byParentId(kodi_id, + v.KODI_TYPE_SEASON): + for episode in self.plex_db.getItem_byParentId( + season[1], v.KODI_TYPE_EPISODE): + self.remove_episode(episode[1], episode[2]) + self.plex_db.removeItem(episode[0]) + self.remove_season(season[1]) + self.plex_db.removeItem(season[0]) + self.remove_show(kodi_id) - def removeShow(self, kodi_id): + LOG.debug("Deleted %s %s from Kodi database", kodi_type, plex_id) + + def remove_show(self, kodi_id): """ Remove a TV show, and only the show, no seasons or episodes """ - kodicursor = self.kodicursor self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW) - self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_SHOW, kodicursor) - kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) + self.artwork.delete_artwork(kodi_id, + v.KODI_TYPE_SHOW, + self.kodicursor) + self.kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", + (kodi_id,)) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.remove_ratings(kodi_id, v.KODI_TYPE_SHOW) - LOG.info("Removed tvshow: %s.", kodi_id) + LOG.info("Removed tvshow: %s", kodi_id) - def removeSeason(self, kodi_id): + def remove_season(self, kodi_id): """ Remove a season, and only a season, not the show or episodes """ - kodicursor = self.kodicursor - self.artwork.delete_artwork(kodi_id, "season", kodicursor) - kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", - (kodi_id,)) - LOG.info("Removed season: %s.", kodi_id) + self.artwork.delete_artwork(kodi_id, + v.KODI_TYPE_SEASON, + self.kodicursor) + self.kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", + (kodi_id,)) + LOG.info("Removed season: %s", kodi_id) - def removeEpisode(self, kodi_id, file_id): + def remove_episode(self, kodi_id, file_id): """ - Remove an episode, and episode only + Remove an episode, and episode only from the Kodi DB (not Plex DB) """ - kodicursor = self.kodicursor self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.remove_file(file_id) - self.artwork.delete_artwork(kodi_id, "episode", kodicursor) - kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", - (kodi_id,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) + self.artwork.delete_artwork(kodi_id, + v.KODI_TYPE_EPISODE, + self.kodicursor) + self.kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", + (kodi_id,)) if v.KODIVERSION >= 17: self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.remove_ratings(kodi_id, v.KODI_TYPE_EPISODE) - LOG.info("Removed episode: %s.", kodi_id) + LOG.info("Removed episode: %s", kodi_id) class Music(Items): @@ -1754,7 +1705,7 @@ class Music(Items): def remove(self, itemid): """ - Remove kodiid, fileid, pathid, plex reference + Remove kodiid, file_id, pathid, plex reference """ plex_db = self.plex_db diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 8bba4960..6bfb196f 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1171,7 +1171,8 @@ class LibrarySync(Thread): elif item['type'] in (v.PLEX_TYPE_SHOW, v.PLEX_TYPE_SEASON, v.PLEX_TYPE_EPISODE): - log.debug("Removing episode/season/tv show %s" % item['ratingKey']) + log.debug("Removing episode/season/show with plex id %s", + item['ratingKey']) self.videoLibUpdate = True with itemtypes.TVShows() as show: show.remove(item['ratingKey']) diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index ca7949cf..d0e665d4 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -220,16 +220,13 @@ class Plex_DB_Functions(): None if not found """ query = ''' - SELECT kodi_id, kodi_fileid, kodi_pathid, - parent_id, kodi_type, plex_type - FROM plex - WHERE plex_id = ? + SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, + plex_type + FROM plex WHERE plex_id = ? + LIMIT 1 ''' - try: - self.plexcursor.execute(query, (plex_id,)) - return self.plexcursor.fetchone() - except: - return None + self.plexcursor.execute(query, (plex_id,)) + return self.plexcursor.fetchone() def getItem_byWildId(self, plex_id): """ @@ -271,14 +268,13 @@ class Plex_DB_Functions(): def getItem_byParentId(self, parent_id, kodi_type): """ - Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id, + Returns a list of tuples (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 = ?" + WHERE parent_id = ? AND kodi_type = ? ''' self.plexcursor.execute(query, (parent_id, kodi_type,)) return self.plexcursor.fetchall() @@ -382,8 +378,8 @@ class Plex_DB_Functions(): """ Removes the one entry with plex_id """ - query = "DELETE FROM plex WHERE plex_id = ?" - self.plexcursor.execute(query, (plex_id,)) + self.plexcursor.execute('DELETE FROM plex WHERE plex_id = ?', + (plex_id,)) def removeWildItem(self, plex_id): """ From 93e3d42e23d6026422316026cf05bf4448a9ec55 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 18:54:05 +0100 Subject: [PATCH 445/509] Fix playstates not being copied/reset correctly --- resources/lib/kodimonitor.py | 5 +++-- resources/lib/player.py | 5 +++-- resources/lib/state.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 5a55a390..59973de8 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -4,6 +4,7 @@ PKC Kodi Monitoring implementation from logging import getLogger from json import loads from threading import Thread +import copy from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ getLocalizedString @@ -60,8 +61,8 @@ class KodiMonitor(Monitor): self.xbmcplayer = Player() Monitor.__init__(self) for playerid in state.PLAYER_STATES: - state.PLAYER_STATES[playerid] = dict(state.PLAYSTATE) - state.OLD_PLAYER_STATES[playerid] = dict(state.PLAYSTATE) + state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) + state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) LOG.info("Kodi monitor started.") def onScanStarted(self, library): diff --git a/resources/lib/player.py b/resources/lib/player.py index 74c38411..17fa75e1 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -2,6 +2,7 @@ ############################################################################### from logging import getLogger +import copy from xbmc import Player @@ -23,9 +24,9 @@ def playback_cleanup(): """ PKC cleanup after playback ends/is stopped """ + LOG.debug('playback_cleanup called') # We might have saved a transient token from a user flinging media via # Companion (if we could not use the playqueue to store the token) - LOG.debug('playback_cleanup called') state.PLEX_TRANSIENT_TOKEN = None for playerid in state.ACTIVE_PLAYERS: status = state.PLAYER_STATES[playerid] @@ -38,7 +39,7 @@ def playback_cleanup(): '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) # Reset the player's status - status = dict(state.PLAYSTATE) + status = copy.deepcopy(state.PLAYSTATE) # As all playback has halted, reset the players that have been active state.ACTIVE_PLAYERS = [] LOG.debug('Finished PKC playback cleanup') diff --git a/resources/lib/state.py b/resources/lib/state.py index 4fbdc907..3633267d 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -108,7 +108,7 @@ OLD_PLAYER_STATES = { 1: {}, 2: {} } -# "empty" dict for the PLAYER_STATES above +# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate! PLAYSTATE = { 'type': None, 'time': { From 46adc51cf6d9ad6ffabd89a912103311b79faaa1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 18:57:00 +0100 Subject: [PATCH 446/509] Fix old playerstate not being copied/reset correctly --- resources/lib/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 17fa75e1..cb4d472e 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -31,7 +31,7 @@ def playback_cleanup(): for playerid in state.ACTIVE_PLAYERS: status = state.PLAYER_STATES[playerid] # Remember the last played item later - state.OLD_PLAYER_STATES[playerid] = dict(status) + state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status) # Stop transcoding if status['playmethod'] == 'Transcode': LOG.debug('Tell the PMS to stop transcoding') From 5012ab84c8e28aa2a7ddd8f9bb3eb50361596d63 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 20:10:02 +0100 Subject: [PATCH 447/509] Fix videos not being correctly marked as played - Hopefully fixes #423 --- resources/lib/kodidb_functions.py | 15 +++++++++ resources/lib/player.py | 52 ++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index d458dab9..7c430b9e 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -655,6 +655,21 @@ class KodiDBMethods(object): """ self.cursor.execute("DELETE FROM bookmark") + def get_playcount(self, file_id): + """ + Returns the playcount for the item file_id or None if not found + """ + query = ''' + SELECT playCount FROM files + WHERE idFile = ? LIMIT 1 + ''' + self.cursor.execute(query, (file_id, )) + try: + answ = self.cursor.fetchone()[0] + except TypeError: + answ = None + return answ + def addPlaystate(self, file_id, resume_seconds, total_seconds, playcount, dateplayed): # Delete existing resume point diff --git a/resources/lib/player.py b/resources/lib/player.py index cb4d472e..c954a090 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -6,9 +6,11 @@ import copy from xbmc import Player +import kodidb_functions as kodidb +import plexdb_functions as plexdb from downloadutils import DownloadUtils as DU from plexbmchelper.subscribers import LOCKER -import playqueue as PQ +from utils import kodi_time_to_millis, unix_date_to_kodi, unix_timestamp import variables as v import state @@ -38,6 +40,11 @@ def playback_cleanup(): DU().downloadUrl( '{server}/video/:/transcode/universal/stop', parameters={'session': v.PKC_MACHINE_IDENTIFIER}) + if playerid == 1: + # Bookmarks might not be pickup up correctly, so let's do them + # manually. Applies to addon paths, but direct paths might have + # started playback via PMS + _record_playstate(status) # Reset the player's status status = copy.deepcopy(state.PLAYSTATE) # As all playback has halted, reset the players that have been active @@ -45,6 +52,49 @@ def playback_cleanup(): LOG.debug('Finished PKC playback cleanup') +def _record_playstate(status): + if not status['plex_id']: + LOG.debug('No Plex id found to record playstate for status %s', status) + return + with plexdb.Get_Plex_DB() as plex_db: + kodi_db_item = plex_db.getItem_byId(status['plex_id']) + if kodi_db_item is None: + # Item not (yet) in Kodi library + LOG.debug('No playstate update due to Plex id not found: %s', status) + return + time = float(kodi_time_to_millis(status['time'])) / 1000 + totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000 + try: + progress = time / totaltime + except ZeroDivisionError: + progress = 0.0 + LOG.debug('Playback progress %s (%s of %s seconds)', + progress, time, totaltime) + playcount = status['playcount'] + if playcount is None: + LOG.info('playcount not found, looking it up in the Kodi DB') + with kodidb.GetKodiDB('video') as kodi_db: + playcount = kodi_db.get_playcount(kodi_db_item[1]) + playcount = 0 if playcount is None else playcount + if time < v.IGNORE_SECONDS_AT_START: + LOG.debug('Ignoring playback less than %s seconds', + v.IGNORE_SECONDS_AT_START) + # Annoying Plex bug - it'll reset an already watched video to unwatched + playcount = 0 + time = 0 + elif progress >= v.MARK_PLAYED_AT: + LOG.debug('Recording entirely played video since progress > %s', + v.MARK_PLAYED_AT) + playcount += 1 + time = 0 + with kodidb.GetKodiDB('video') as kodi_db: + kodi_db.addPlaystate(kodi_db_item[1], + time, + totaltime, + playcount, + unix_date_to_kodi(unix_timestamp())) + + class PKC_Player(Player): def __init__(self): Player.__init__(self) From ff53086d0b6262dc2e2f42fa00cd0027b7b93075 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 11 Mar 2018 20:12:33 +0100 Subject: [PATCH 448/509] Version bump --- README.md | 2 +- addon.xml | 13 +++++++++++-- changelog.txt | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 65857b7d..ed7db882 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.7-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.8-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 3fd427dd..330edef8 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,16 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.7 (beta only): + version 2.0.8 (beta only): +- Fix videos not being correctly marked as played +- Improve playback startup resiliance +- Fix playerstates not being copied/reset correctly +- Fix tv shows not being correctly deleted +- Fix episode rating not being correct +- Make generally sure that we're correctly deleting videos from the Kodi DB +- Fix disabling of background sync (websockets) + +version 2.0.7 (beta only): - Fix another UnicodeDecodeError for playlists - Hardcode plugin-calls instead of using urlencode - Fix Kodi 18 log warnings by declaring all settings variables diff --git a/changelog.txt b/changelog.txt index 3deff21f..f39531d7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +version 2.0.8 (beta only): +- Fix videos not being correctly marked as played +- Improve playback startup resiliance +- Fix playerstates not being copied/reset correctly +- Fix tv shows not being correctly deleted +- Fix episode rating not being correct +- Make generally sure that we're correctly deleting videos from the Kodi DB +- Fix disabling of background sync (websockets) + version 2.0.7 (beta only): - Fix another UnicodeDecodeError for playlists - Hardcode plugin-calls instead of using urlencode From bc8546b4ff82ef20af1ecbdf2ee82ef6ecbdf988 Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Mar 2018 07:41:53 +0100 Subject: [PATCH 449/509] Fix AttributeError on playback start - Hopefully fixes #428 --- resources/lib/kodimonitor.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 59973de8..496b3016 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -279,18 +279,17 @@ class KodiMonitor(Monitor): except TypeError: kodi_id = None # If using direct paths and starting playback from a widget - if not path.startswith('http'): - if not kodi_id: - kodi_id = kodiid_from_filename(path, kodi_type) - if not plex_id and kodi_id: - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) - try: - plex_id = plex_dbitem[0] - plex_type = plex_dbitem[2] - except TypeError: - # No plex id, hence item not in the library. E.g. clips - pass + if not kodi_id and kodi_type and path and not path.startswith('http'): + kodi_id = kodiid_from_filename(path, kodi_type) + if not plex_id and kodi_id and kodi_type: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) + try: + plex_id = plex_dbitem[0] + plex_type = plex_dbitem[2] + except TypeError: + # No plex id, hence item not in the library. E.g. clips + pass return kodi_id, kodi_type, plex_id, plex_type @staticmethod From 2ba74bb95dbf2f34a6ff567bcf265e0ad29325bb Mon Sep 17 00:00:00 2001 From: croneter Date: Wed, 14 Mar 2018 07:53:42 +0100 Subject: [PATCH 450/509] Version bump --- README.md | 2 +- addon.xml | 7 +++++-- changelog.txt | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ed7db882..256670b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.8-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.9-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 330edef8..b57a4da9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,10 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.8 (beta only): + version 2.0.9 (beta only): +- Fix AttributeError on playback start + +version 2.0.8 (beta only): - Fix videos not being correctly marked as played - Improve playback startup resiliance - Fix playerstates not being copied/reset correctly diff --git a/changelog.txt b/changelog.txt index f39531d7..ed7e04b0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 2.0.9 (beta only): +- Fix AttributeError on playback start + version 2.0.8 (beta only): - Fix videos not being correctly marked as played - Improve playback startup resiliance From c48ef5012f7a6af27a3af7e449260b6090e85162 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 08:24:56 +0100 Subject: [PATCH 451/509] Fix wrong item being reported using direct paths - Fixes #428 --- resources/lib/kodimonitor.py | 48 ++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 496b3016..eb40e186 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -260,10 +260,6 @@ class KodiMonitor(Monitor): kodi_id = json_item.get('id') kodi_type = json_item.get('type') path = json_item.get('file') - if not path and not kodi_id: - LOG.debug('Aborting playback report - no Kodi id or file for %s', - json_item) - raise RuntimeError # Plex id will NOT be set with direct paths plex_id = state.PLEX_IDS.get(path) try: @@ -340,30 +336,39 @@ class KodiMonitor(Monitor): playqueue = PQ.PLAYQUEUES[playerid] info = js.get_player_props(playerid) json_item = js.get_item(playerid) - path = json_item.get('file') pos = info['position'] if info['position'] != -1 else 0 LOG.debug('Detected position %s for %s', pos, playqueue) + LOG.debug('Detected Kodi playing item properties: %s', json_item) status = state.PLAYER_STATES[playerid] + path = json_item.get('file') try: item = playqueue.items[pos] except IndexError: - try: - kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) - except RuntimeError: - return - LOG.info('Need to initialize Plex and PKC playqueue') - try: - if plex_id: - item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) + # PKC playqueue not yet initialized + initialize = True + else: + if item.kodi_id: + if (item.kodi_id != json_item.get('id') or + item.kodi_type != json_item.get('type')): + initialize = True else: - item = PL.init_Plex_playlist(playqueue, - kodi_item={'id': kodi_id, - 'type': kodi_type, - 'file': path}) - except PL.PlaylistError: - LOG.info('Could not initialize our playlist') - # Avoid errors - item = PL.Playlist_Item() + initialize = False + else: + # E.g. clips set-up previously with no Kodi DB entry + if item.file != json_item.get('file'): + initialize = True + else: + initialize = False + if initialize: + LOG.debug('Need to initialize Plex and PKC playqueue') + if not json_item.get('id') or not json_item.get('type'): + LOG.debug('No Kodi id or type obtained - aborting report') + return + kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) + if not plex_id: + LOG.debug('No Plex id obtained - aborting playback report') + return + item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) # Set the Plex container key (e.g. using the Plex playqueue) container_key = None if info['playlistid'] != -1: @@ -375,6 +380,7 @@ class KodiMonitor(Monitor): container_key = '/library/metadata/%s' % plex_id LOG.debug('Set the Plex container_key to: %s', container_key) else: + LOG.debug('No need to initialize playqueues') kodi_id = item.kodi_id kodi_type = item.kodi_type plex_id = item.plex_id From 4e85b65318ef0f94c68ba82262f3ce93feb7570f Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 10:25:51 +0100 Subject: [PATCH 452/509] Direct paths: correctly clean up after context menu play --- resources/lib/kodidb_functions.py | 8 ++++++++ resources/lib/playback.py | 5 ++++- resources/lib/player.py | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 7c430b9e..b14a94ff 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -204,6 +204,14 @@ class KodiDBMethods(object): self.cursor.execute(query, (file_id, path_id, filename, date_added)) return file_id + def clean_file_table(self): + """ + Hack: using Direct Paths, Kodi adds all addon paths to the files table + but without a dateAdded entry. This method cleans up all file entries + without a dateAdded entry - to be called after playback has ended. + """ + self.cursor.execute('DELETE FROM files where dateAdded IS NULL') + def remove_file(self, file_id): """ Removes the entry for file_id from the files table. Will also delete diff --git a/resources/lib/playback.py b/resources/lib/playback.py index dd233d95..044a753e 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -164,7 +164,10 @@ def _ensure_resolve(abort=False): will be destroyed. """ if RESOLVE: - state.PKC_CAUSED_STOP = True + LOG.debug('Passing dummy path to Kodi') + if not state.CONTEXT_MENU_PLAY: + # Because playback won't start with context menu play + state.PKC_CAUSED_STOP = True result = Playback_Successful() result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') pickle_me(result) diff --git a/resources/lib/player.py b/resources/lib/player.py index c954a090..3aaa02af 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -53,6 +53,9 @@ def playback_cleanup(): def _record_playstate(status): + with kodidb.GetKodiDB('video') as kodi_db: + # Hack - remove any obsolete file entries Kodi made + kodi_db.clean_file_table() if not status['plex_id']: LOG.debug('No Plex id found to record playstate for status %s', status) return From 8e1b77fcfe752839dcd3f805142a26ea5ca538e0 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 10:40:15 +0100 Subject: [PATCH 453/509] Fix correctly recording ended (not stopped!) video --- resources/lib/player.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 3aaa02af..0dbfecda 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -22,9 +22,11 @@ LOG = getLogger("PLEX." + __name__) @LOCKER.lockthis -def playback_cleanup(): +def playback_cleanup(ended=False): """ - PKC cleanup after playback ends/is stopped + PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi + completely finished playing an item (because we will get and use wrong + timing data otherwise) """ LOG.debug('playback_cleanup called') # We might have saved a transient token from a user flinging media via @@ -44,7 +46,7 @@ def playback_cleanup(): # Bookmarks might not be pickup up correctly, so let's do them # manually. Applies to addon paths, but direct paths might have # started playback via PMS - _record_playstate(status) + _record_playstate(status, ended) # Reset the player's status status = copy.deepcopy(state.PLAYSTATE) # As all playback has halted, reset the players that have been active @@ -52,7 +54,7 @@ def playback_cleanup(): LOG.debug('Finished PKC playback cleanup') -def _record_playstate(status): +def _record_playstate(status, ended): with kodidb.GetKodiDB('video') as kodi_db: # Hack - remove any obsolete file entries Kodi made kodi_db.clean_file_table() @@ -65,14 +67,18 @@ def _record_playstate(status): # Item not (yet) in Kodi library LOG.debug('No playstate update due to Plex id not found: %s', status) return - time = float(kodi_time_to_millis(status['time'])) / 1000 totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000 - try: - progress = time / totaltime - except ZeroDivisionError: - progress = 0.0 - LOG.debug('Playback progress %s (%s of %s seconds)', - progress, time, totaltime) + if ended: + progress = 0.99 + time = v.IGNORE_SECONDS_AT_START + 1 + else: + time = float(kodi_time_to_millis(status['time'])) / 1000 + try: + progress = time / totaltime + except ZeroDivisionError: + progress = 0.0 + LOG.debug('Playback progress %s (%s of %s seconds)', + progress, time, totaltime) playcount = status['playcount'] if playcount is None: LOG.info('playcount not found, looking it up in the Kodi DB') @@ -143,4 +149,4 @@ class PKC_Player(Player): Will be called when playback ends due to the media file being finished """ LOG.debug("ONPLAYBACK_ENDED") - playback_cleanup() + playback_cleanup(ended=True) From f0c1562ab5bab95ed3369b5fa3f757e0abc1c7d3 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 11:28:31 +0100 Subject: [PATCH 454/509] Remove obsolete resumable flag --- resources/lib/kodimonitor.py | 1 - resources/lib/playback.py | 2 -- resources/lib/state.py | 2 -- 3 files changed, 5 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index eb40e186..651c3946 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -415,7 +415,6 @@ class SpecialMonitor(Thread): if (getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and getInfoLabel('Control.GetLabel(1002)') in strings): # Remember that the item IS indeed resumable - state.RESUMABLE = True control = int(Window(10106).getFocusId()) if control == 1002: # Start from beginning diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 044a753e..bcc883ee 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -175,7 +175,6 @@ def _ensure_resolve(abort=False): # Reset some playback variables state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False - state.RESUMABLE = False state.RESUME_PLAYBACK = False @@ -319,7 +318,6 @@ def _conclude_playback(playqueue, pos): listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('resumetime', str(item.offset)) # Reset the resumable flag - state.RESUMABLE = False result.listitem = listitem pickle_me(result) LOG.info('Done concluding playback') diff --git a/resources/lib/state.py b/resources/lib/state.py index 3633267d..b35cd5e7 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -145,8 +145,6 @@ PLAYSTATE = { # paths for playback (since we're not receiving a Kodi id) PLEX_IDS = {} PLAYED_INFO = {} -# Flag whether Kodi item where the playback is being started is even resumable -RESUMABLE = False # Set by SpecialMonitor - did user choose to resume playback or start from the # beginning? RESUME_PLAYBACK = False From 229b0491b6bc0ca5a00029584d0673a36b465b81 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 11:30:15 +0100 Subject: [PATCH 455/509] Do not play trailers for resumable movies using playback via PMS --- resources/lib/playback.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index bcc883ee..80b509da 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -105,8 +105,9 @@ def _playback_init(plex_id, plex_type, playqueue, pos): # playqueues # Fail the item we're trying to play now so we can restart the player _ensure_resolve() + api = API(xml[0]) trailers = False - if (plex_type == v.PLEX_TYPE_MOVIE and not state.RESUME_PLAYBACK and + if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" From 491aa3258651e2ce722e228407e326845fcaec21 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 13:12:33 +0100 Subject: [PATCH 456/509] Don't record last played date if state unwatched --- resources/lib/player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 0dbfecda..023b4eff 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -80,6 +80,7 @@ def _record_playstate(status, ended): LOG.debug('Playback progress %s (%s of %s seconds)', progress, time, totaltime) playcount = status['playcount'] + last_played = unix_date_to_kodi(unix_timestamp()) if playcount is None: LOG.info('playcount not found, looking it up in the Kodi DB') with kodidb.GetKodiDB('video') as kodi_db: @@ -90,6 +91,7 @@ def _record_playstate(status, ended): v.IGNORE_SECONDS_AT_START) # Annoying Plex bug - it'll reset an already watched video to unwatched playcount = 0 + last_played = None time = 0 elif progress >= v.MARK_PLAYED_AT: LOG.debug('Recording entirely played video since progress > %s', @@ -101,7 +103,7 @@ def _record_playstate(status, ended): time, totaltime, playcount, - unix_date_to_kodi(unix_timestamp())) + last_played) class PKC_Player(Player): From 1d718c99c6ce0a61459fcc5c6916a5f284268bca Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 15 Mar 2018 13:46:56 +0100 Subject: [PATCH 457/509] Always resume playback if playback initiated via context menu --- resources/lib/playback.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 80b509da..270dc2ce 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -136,16 +136,19 @@ def _playback_init(plex_id, plex_type, playqueue, pos): # Sleep a bit to let setResolvedUrl do its thing - bit ugly sleep(200) _process_stack(playqueue, stack) + # Always resume if playback initiated via PMS and there IS a resume + # point + offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None # Reset some playback variables state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False # Do NOT set offset, because Kodi player will return here to resolveURL # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=threaded_playback, - args=(playqueue.kodi_pl, pos, None)) + args=(playqueue.kodi_pl, pos, offset)) thread.setDaemon(True) - LOG.info('Done initializing playback, starting Kodi player at pos %s', - pos) + LOG.info('Done initializing playback, starting Kodi player at pos %s and ' + 'resume point %s', pos, offset) # By design, PKC will start Kodi playback using Player().play(). Kodi # caches paths like our plugin://pkc. If we use Player().play() between # 2 consecutive startups of exactly the same Kodi library item, Kodi's From 600a22d158d9fc5b9c9ed85c9478dcbd4c8ec188 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 16 Mar 2018 07:37:27 +0100 Subject: [PATCH 458/509] Fix for "In Progress" not appearing - Partially fixes #428 --- resources/language/resource.language.en_gb/strings.po | 5 +++++ resources/lib/initialsetup.py | 1 + resources/lib/kodimonitor.py | 1 + resources/lib/player.py | 11 ++++++++--- resources/lib/state.py | 2 ++ resources/settings.xml | 1 + 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 939dfbcd..865dc6fd 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1560,6 +1560,11 @@ msgctxt "#39064" msgid "Recently Added: Also show already watched episodes" msgstr "" +# PKC settings, Appearance Tweaks +msgctxt "#39065" +msgid "Force-refresh Kodi skin on stopping playback" +msgstr "" + msgctxt "#39066" msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 34544c47..279a4991 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -68,6 +68,7 @@ def reload_pkc(): state.REMAP_PATH = settings('remapSMB') == 'true' state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') + state.FORCE_RELOAD_SKIN = settings('forceReloadSkin') == 'true' # Init some Queues() state.COMMAND_PIPELINE_QUEUE = Queue() state.COMPANION_QUEUE = Queue(maxsize=100) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 651c3946..e54f5362 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,6 +47,7 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', + 'forceReloadSkin': 'FORCE_RELOAD_SKIN', 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } diff --git a/resources/lib/player.py b/resources/lib/player.py index 023b4eff..040e7b44 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -4,7 +4,7 @@ from logging import getLogger import copy -from xbmc import Player +import xbmc import kodidb_functions as kodidb import plexdb_functions as plexdb @@ -104,11 +104,16 @@ def _record_playstate(status, ended): totaltime, playcount, last_played) + # Hack to force "in progress" widget to appear if it wasn't visible before + if (state.FORCE_RELOAD_SKIN and + xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): + LOG.debug('Refreshing skin to update widgets') + xbmc.executebuiltin('ReloadSkin()') -class PKC_Player(Player): +class PKC_Player(xbmc.Player): def __init__(self): - Player.__init__(self) + xbmc.Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index b35cd5e7..3cdfa910 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -30,6 +30,8 @@ INDICATE_MEDIA_VERSIONS = False RUN_LIB_SCAN = None # Number of items to fetch and display in widgets FETCH_PMS_ITEM_NUMBER = None +# Hack to force Kodi widget for "in progress" to show up if it was empty before +FORCE_RELOAD_SKIN = True # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? diff --git a/resources/settings.xml b/resources/settings.xml index 0ef67089..953e6288 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -144,6 +144,7 @@ + From bb2f4601f53857f0b67ae2972130d23864a3480a Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 16 Mar 2018 07:52:49 +0100 Subject: [PATCH 459/509] Clean Kodi DB more thoroughly after playback start via PMS --- resources/lib/kodidb_functions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index b14a94ff..f0db9d5a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -210,7 +210,18 @@ class KodiDBMethods(object): but without a dateAdded entry. This method cleans up all file entries without a dateAdded entry - to be called after playback has ended. """ + self.cursor.execute('SELECT idFile FROM files WHERE dateAdded IS NULL') + files = self.cursor.fetchall() self.cursor.execute('DELETE FROM files where dateAdded IS NULL') + for file in files: + self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', + (file[0],)) + self.cursor.execute('DELETE FROM settings WHERE idFile = ?', + (file[0],)) + self.cursor.execute('DELETE FROM streamdetails WHERE idFile = ?', + (file[0],)) + self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', + (file[0],)) def remove_file(self, file_id): """ From c54da58d8703862a757a795859ae5d5f9baac411 Mon Sep 17 00:00:00 2001 From: croneter Date: Fri, 16 Mar 2018 08:05:53 +0100 Subject: [PATCH 460/509] Version bump --- README.md | 2 +- addon.xml | 14 ++++++++++++-- changelog.txt | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 256670b2..aa4e34e7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.9-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.10-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index b57a4da9..aca6a36b 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,17 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.9 (beta only): + version 2.0.10 (beta only): +- Fix wrong item being reported using direct paths +- Direct paths: correctly clean up after context menu play +- Always resume playback if playback initiated via context menu +- Do not play trailers for resumable movies using playback via PMS +- Fix for "In Progress" widget not appearing +- Fix correctly recording ended (not stopped!) video +- Don't record last played date if state unwatched +- Clean Kodi DB more thoroughly after playback start via PMS + +version 2.0.9 (beta only): - Fix AttributeError on playback start version 2.0.8 (beta only): diff --git a/changelog.txt b/changelog.txt index ed7e04b0..7fe62e96 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +version 2.0.10 (beta only): +- Fix wrong item being reported using direct paths +- Direct paths: correctly clean up after context menu play +- Always resume playback if playback initiated via context menu +- Do not play trailers for resumable movies using playback via PMS +- Fix for "In Progress" widget not appearing +- Fix correctly recording ended (not stopped!) video +- Don't record last played date if state unwatched +- Clean Kodi DB more thoroughly after playback start via PMS + version 2.0.9 (beta only): - Fix AttributeError on playback start From 7096aa35b2c70a28ccf9b3de515c1eca9c579ca8 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 18 Mar 2018 13:48:29 +0100 Subject: [PATCH 461/509] Addon paths: Don't store show id in path --- resources/lib/itemtypes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 0d315e34..04ce41ac 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -836,7 +836,7 @@ class TVShows(Items): # GET THE FILE AND PATH ##### if state.DIRECT_PATHS: playurl = api.file_path(force_first_media=True) - playurl = api.validate_playurl(playurl, api.plex_type()) + playurl = api.validate_playurl(playurl, v.PLEX_TYPE_EPISODE) if playurl is None: return False if "\\" in playurl: @@ -847,19 +847,20 @@ class TVShows(Items): filename = playurl.rsplit("/", 1)[1] path = playurl.replace(filename, "") parent_path_id = self.kodi_db.parent_path_id(path) + pathid = self.kodi_db.add_video_path(path, + id_parent_path=parent_path_id) else: # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! - path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) + path = 'plugin://%s.tvshows/' % v.ADDON_ID filename = ('%s?plex_id=%s&plex_type=%s&mode=play' % (path, itemid, v.PLEX_TYPE_EPISODE)) playurl = filename - parent_path_id = self.kodi_db.parent_path_id(path) + # Root path tvshows/ already saved in Kodi DB + pathid = self.kodi_db.add_video_path(path) # add/retrieve pathid and fileid # if the path or file already exists, the calls return current value - pathid = self.kodi_db.add_video_path(path, - id_parent_path=parent_path_id) fileid = self.kodi_db.add_file(filename, pathid, dateadded) # UPDATE THE EPISODE ##### From 6fcbf2977903efa6ccd5c5dedb39f7e4f7f6d286 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 18 Mar 2018 14:50:37 +0100 Subject: [PATCH 462/509] Addon paths: include real filename in plugin calls --- resources/lib/itemtypes.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 04ce41ac..97814cd6 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -288,9 +288,16 @@ class Movies(Items): scraper='metadata.local') if do_indirect: # Set plugin path and media flags using real filename + filename = api.file_path(force_first_media=True) + if "\\" in filename: + # Local path + filename = filename.rsplit("\\", 1)[1] + else: + # Network share + filename = filename.rsplit("/", 1)[1] path = 'plugin://%s.movies/' % v.ADDON_ID - filename = ('%s?plex_id=%s&plex_type=%s&mode=play' - % (path, itemid, v.PLEX_TYPE_MOVIE)) + filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' + % (path, itemid, v.PLEX_TYPE_MOVIE, filename)) playurl = filename pathid = self.kodi_db.get_path(path) @@ -837,8 +844,6 @@ class TVShows(Items): if state.DIRECT_PATHS: playurl = api.file_path(force_first_media=True) playurl = api.validate_playurl(playurl, v.PLEX_TYPE_EPISODE) - if playurl is None: - return False if "\\" in playurl: # Local path filename = playurl.rsplit("\\", 1)[1] @@ -852,9 +857,16 @@ class TVShows(Items): else: # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! + filename = api.file_path(force_first_media=True) + if "\\" in filename: + # Local path + filename = filename.rsplit("\\", 1)[1] + else: + # Network share + filename = filename.rsplit("/", 1)[1] path = 'plugin://%s.tvshows/' % v.ADDON_ID - filename = ('%s?plex_id=%s&plex_type=%s&mode=play' - % (path, itemid, v.PLEX_TYPE_EPISODE)) + filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' + % (path, itemid, v.PLEX_TYPE_EPISODE, filename)) playurl = filename # Root path tvshows/ already saved in Kodi DB pathid = self.kodi_db.add_video_path(path) From ea57eb5f93501c5af006541962aabccf680b10c1 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 18 Mar 2018 15:08:55 +0100 Subject: [PATCH 463/509] Save NaN and not 0 to Kodi DB if playcount is zero --- resources/lib/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 040e7b44..a895577d 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -90,7 +90,7 @@ def _record_playstate(status, ended): LOG.debug('Ignoring playback less than %s seconds', v.IGNORE_SECONDS_AT_START) # Annoying Plex bug - it'll reset an already watched video to unwatched - playcount = 0 + playcount = None last_played = None time = 0 elif progress >= v.MARK_PLAYED_AT: From 303adbf02efd86d2598691c98644a137f1916fde Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 18 Mar 2018 15:23:54 +0100 Subject: [PATCH 464/509] Revert "Fix for "In Progress" not appearing" This reverts commit 600a22d158d9fc5b9c9ed85c9478dcbd4c8ec188. --- resources/language/resource.language.en_gb/strings.po | 5 ----- resources/lib/initialsetup.py | 1 - resources/lib/kodimonitor.py | 1 - resources/lib/player.py | 11 +++-------- resources/lib/state.py | 2 -- resources/settings.xml | 1 - 6 files changed, 3 insertions(+), 18 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 865dc6fd..939dfbcd 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1560,11 +1560,6 @@ msgctxt "#39064" msgid "Recently Added: Also show already watched episodes" msgstr "" -# PKC settings, Appearance Tweaks -msgctxt "#39065" -msgid "Force-refresh Kodi skin on stopping playback" -msgstr "" - msgctxt "#39066" msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 279a4991..34544c47 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -68,7 +68,6 @@ def reload_pkc(): state.REMAP_PATH = settings('remapSMB') == 'true' state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') - state.FORCE_RELOAD_SKIN = settings('forceReloadSkin') == 'true' # Init some Queues() state.COMMAND_PIPELINE_QUEUE = Queue() state.COMPANION_QUEUE = Queue(maxsize=100) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index e54f5362..651c3946 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,7 +47,6 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'forceReloadSkin': 'FORCE_RELOAD_SKIN', 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } diff --git a/resources/lib/player.py b/resources/lib/player.py index a895577d..43f772e8 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -4,7 +4,7 @@ from logging import getLogger import copy -import xbmc +from xbmc import Player import kodidb_functions as kodidb import plexdb_functions as plexdb @@ -104,16 +104,11 @@ def _record_playstate(status, ended): totaltime, playcount, last_played) - # Hack to force "in progress" widget to appear if it wasn't visible before - if (state.FORCE_RELOAD_SKIN and - xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): - LOG.debug('Refreshing skin to update widgets') - xbmc.executebuiltin('ReloadSkin()') -class PKC_Player(xbmc.Player): +class PKC_Player(Player): def __init__(self): - xbmc.Player.__init__(self) + Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index 3cdfa910..b35cd5e7 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -30,8 +30,6 @@ INDICATE_MEDIA_VERSIONS = False RUN_LIB_SCAN = None # Number of items to fetch and display in widgets FETCH_PMS_ITEM_NUMBER = None -# Hack to force Kodi widget for "in progress" to show up if it was empty before -FORCE_RELOAD_SKIN = True # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? diff --git a/resources/settings.xml b/resources/settings.xml index 953e6288..0ef67089 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -144,7 +144,6 @@ - From 206c2a319bfc692e2ab6c0c36502175843cdb850 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 18 Mar 2018 19:18:44 +0100 Subject: [PATCH 465/509] Tweak code for episode artwork --- resources/lib/PlexAPI.py | 10 +++++++--- resources/lib/itemtypes.py | 11 ++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 843d85ec..bde44444 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -760,13 +760,17 @@ class API(object): # Grab artwork from Plex artworks = {} + if self.plex_type() == v.PLEX_TYPE_EPISODE: + # Episodes is a bit special, only get the thumb + art = self._one_artwork('thumb') + if art: + artworks['thumb'] = art + return artworks for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems(): art = self._one_artwork(plex_artwork) if art: artworks[kodi_artwork] = art - if self.plex_type() in (v.PLEX_TYPE_EPISODE, - v.PLEX_TYPE_SONG, - v.PLEX_TYPE_ALBUM): + if self.plex_type() in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_ALBUM): # Get parent item artwork if the main item is missing artwork if 'fanart' not in artworks: art = self._one_artwork('parentArt') diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 97814cd6..56854776 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -991,13 +991,10 @@ class TVShows(Items): self.kodi_db.modify_people(episodeid, v.KODI_TYPE_EPISODE, api.people_list()) - # Wide "screenshot" of particular episode - poster = item.attrib.get('thumb') - if poster: - poster = api.attach_plex_token_to_url( - "%s%s" % (self.server, poster)) - artwork.modify_art( - poster, episodeid, v.KODI_TYPE_EPISODE, "thumb", kodicursor) + artwork.modify_artwork(api.artwork(), + episodeid, + v.KODI_TYPE_EPISODE, + kodicursor) streams = api.mediastreams() self.kodi_db.modify_streams(fileid, streams, runtime) self.kodi_db.addPlaystate(fileid, From 88cece30663c7bdb21a25145bf3dfbc0afad4e16 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 08:52:01 +0100 Subject: [PATCH 466/509] Less logging --- resources/lib/PlexAPI.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index bde44444..5da849c1 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1314,7 +1314,6 @@ class API(object): append_sxxexx) self.add_video_streams(listitem) artwork = self.artwork() - LOG.debug('artwork: %s', artwork) listitem.setArt(artwork) return listitem From f25eccb22cdc56f741df22cbea0268e66a3a2373 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 09:16:29 +0100 Subject: [PATCH 467/509] Code optimization --- resources/lib/PlexAPI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 5da849c1..4211c334 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1313,8 +1313,7 @@ class API(object): append_show_title, append_sxxexx) self.add_video_streams(listitem) - artwork = self.artwork() - listitem.setArt(artwork) + listitem.setArt(self.artwork()) return listitem def _create_photo_listitem(self, listitem=None): From 4d2b040c08be9d9a1afac5ea5a0ef24a82befdd3 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 10:37:42 +0100 Subject: [PATCH 468/509] Fix playback artwork for episodes --- resources/lib/PlexAPI.py | 47 ++++++++++++++++++++++++++++++-------- resources/lib/playback.py | 2 +- resources/lib/variables.py | 9 ++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 4211c334..e5dfd50b 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -731,7 +731,7 @@ class API(object): 'minSize=1&upscale=0&url=%s' % (self.server, artwork)) return artwork - def artwork(self, kodi_id=None, kodi_type=None): + def artwork(self, kodi_id=None, kodi_type=None, full_artwork=False): """ Gets the URLs to the Plex artwork. Dict keys will be missing if there is no corresponding artwork. @@ -748,7 +748,36 @@ class API(object): 'fanart' } 'landscape' and 'icon' might be implemented later + Passing full_artwork=True returns ALL the artwork for the item, so not + just 'thumb' for episodes, but also season and show artwork """ + artworks = {} + if self.plex_type() == v.PLEX_TYPE_EPISODE: + # Artwork lookup for episodes is broken for addon paths + if full_artwork: + with plexdb.Get_Plex_DB() as plex_db: + db_item = plex_db.getItem_byId(self.plex_id()) + try: + kodi_id = db_item[0] + except TypeError: + pass + else: + with kodidb.GetKodiDB('video') as kodi_db: + return kodi_db.get_art(kodi_id, v.KODI_TYPE_EPISODE) + # If episode is not in Kodi DB + for kodi_artwork, plex_artwork \ + in v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems(): + art = self._one_artwork(plex_artwork) + if art: + artworks[kodi_artwork] = art + else: + # Episodes is a bit special, only get the thumb, because all + # the other artwork will be saved under season and show + art = self._one_artwork('thumb') + if art: + artworks['thumb'] = art + return artworks + if kodi_id: # in Kodi database, potentially with additional e.g. clearart if self.plex_type() in v.PLEX_VIDEOTYPES: @@ -759,13 +788,8 @@ class API(object): return kodi_db.get_art(kodi_id, kodi_type) # Grab artwork from Plex - artworks = {} - if self.plex_type() == v.PLEX_TYPE_EPISODE: - # Episodes is a bit special, only get the thumb - art = self._one_artwork('thumb') - if art: - artworks['thumb'] = art - return artworks + # if self.plex_type() == v.PLEX_TYPE_EPISODE: + for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems(): art = self._one_artwork(plex_artwork) if art: @@ -1313,7 +1337,7 @@ class API(object): append_show_title, append_sxxexx) self.add_video_streams(listitem) - listitem.setArt(self.artwork()) + listitem.setArt(self.artwork(full_artwork=True)) return listitem def _create_photo_listitem(self, listitem=None): @@ -1389,7 +1413,10 @@ class API(object): listitem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: - metadata['mediatype'] = 'episode' + if state.DIRECT_PATHS: + # Do NOT set a link to the Kodi DB to force Kodi to use our + # ListItem artwork for Addon Paths + metadata['mediatype'] = 'episode' _, show, season, episode = self.episode_data() season = -1 if season is None else int(season) episode = -1 if episode is None else int(episode) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 270dc2ce..2973c5a6 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -204,7 +204,7 @@ def _prep_playlist_stack(xml): for item in xml: api = API(item) if (state.CONTEXT_MENU_PLAY is False and - api.plex_type() != v.PLEX_TYPE_CLIP): + api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)): # If user chose to play via PMS or force transcode, do not # use the item path stored in the Kodi DB with plexdb.Get_Plex_DB() as plex_db: diff --git a/resources/lib/variables.py b/resources/lib/variables.py index d6ed6e3a..ce89e4da 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -324,6 +324,15 @@ KODI_TO_PLEX_ARTWORK = { 'fanart': 'art' } +KODI_TO_PLEX_ARTWORK_EPISODE = { + 'tvshow.poster': 'grandparentThumb', + 'tvshow.fanart': 'grandparentArt', + 'season.poster': 'parentThumb', + 'season.fanart': 'parentArt', + 'poster': 'thumb', + 'fanart': 'art' +} + # Might be implemented in the future: 'icon', 'landscape' (16:9) ALL_KODI_ARTWORK = ( 'thumb', From 19770240aaf2b9ba7ead8f0016f9bd3fd9048f57 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 11:08:09 +0100 Subject: [PATCH 469/509] Grab existing Kodi artwork for episodes --- resources/lib/PlexAPI.py | 41 +++++++++++++++++++++++---------------- resources/lib/playback.py | 3 +++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index e5dfd50b..560e85e8 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -754,28 +754,35 @@ class API(object): artworks = {} if self.plex_type() == v.PLEX_TYPE_EPISODE: # Artwork lookup for episodes is broken for addon paths + # Episodes is a bit special, only get the thumb, because all + # the other artwork will be saved under season and show + art = self._one_artwork('thumb') + if art: + artworks['thumb'] = art if full_artwork: with plexdb.Get_Plex_DB() as plex_db: db_item = plex_db.getItem_byId(self.plex_id()) try: - kodi_id = db_item[0] + season_id = db_item[3] except TypeError: - pass - else: - with kodidb.GetKodiDB('video') as kodi_db: - return kodi_db.get_art(kodi_id, v.KODI_TYPE_EPISODE) - # If episode is not in Kodi DB - for kodi_artwork, plex_artwork \ - in v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems(): - art = self._one_artwork(plex_artwork) - if art: - artworks[kodi_artwork] = art - else: - # Episodes is a bit special, only get the thumb, because all - # the other artwork will be saved under season and show - art = self._one_artwork('thumb') - if art: - artworks['thumb'] = art + return artworks + # Grab artwork from the season + with kodidb.GetKodiDB('video') as kodi_db: + season_art = kodi_db.get_art(season_id, v.KODI_TYPE_SEASON) + for kodi_art in season_art: + artworks['season.%s' % kodi_art] = season_art[kodi_art] + # Get the show id + with plexdb.Get_Plex_DB() as plex_db: + db_item = plex_db.getItem_byId(self.grandparent_id()) + try: + show_id = db_item[0] + except TypeError: + return artworks + # Grab more artwork from the show + with kodidb.GetKodiDB('video') as kodi_db: + show_art = kodi_db.get_art(show_id, v.KODI_TYPE_SHOW) + for kodi_art in show_art: + artworks['tvshow.%s' % kodi_art] = show_art[kodi_art] return artworks if kodi_id: diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 2973c5a6..60db0262 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -215,6 +215,9 @@ def _prep_playlist_stack(xml): # We will never store clips (trailers) in the Kodi DB. # Also set kodi_id to None for playback via PMS, so that we're # using add-on paths. + # Also do NOT associate episodes with library items for addon paths + # as artwork lookup is broken (episode path does not link back to + # season and show) kodi_id = None kodi_type = None for part, _ in enumerate(item[0]): From 3c1bb34f86f9f0efe32d223304d2d399a524dad6 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 11:17:15 +0100 Subject: [PATCH 470/509] Remove obsolete code --- resources/lib/variables.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index ce89e4da..d6ed6e3a 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -324,15 +324,6 @@ KODI_TO_PLEX_ARTWORK = { 'fanart': 'art' } -KODI_TO_PLEX_ARTWORK_EPISODE = { - 'tvshow.poster': 'grandparentThumb', - 'tvshow.fanart': 'grandparentArt', - 'season.poster': 'parentThumb', - 'season.fanart': 'parentArt', - 'poster': 'thumb', - 'fanart': 'art' -} - # Might be implemented in the future: 'icon', 'landscape' (16:9) ALL_KODI_ARTWORK = ( 'thumb', From 524466360f63a2dbd00cd282e4299c92c37bb4b6 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 11:26:01 +0100 Subject: [PATCH 471/509] Link episode ListItem with Kodi library item Enables full metadata such as Show and Season info --- resources/lib/PlexAPI.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 560e85e8..20e42a46 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1420,10 +1420,7 @@ class API(object): listitem.setProperty('totaltime', str(userdata['Runtime'])) if typus == v.PLEX_TYPE_EPISODE: - if state.DIRECT_PATHS: - # Do NOT set a link to the Kodi DB to force Kodi to use our - # ListItem artwork for Addon Paths - metadata['mediatype'] = 'episode' + metadata['mediatype'] = 'episode' _, show, season, episode = self.episode_data() season = -1 if season is None else int(season) episode = -1 if episode is None else int(episode) From d8555ee0cc60ccb65157e91ec115364140550204 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 11:48:17 +0100 Subject: [PATCH 472/509] Fix playback resuming potentially too often --- resources/lib/kodimonitor.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 651c3946..a56b84fc 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -412,17 +412,20 @@ class SpecialMonitor(Thread): # "Start from beginning", "Play from beginning" strings = (getLocalizedString(12021), getLocalizedString(12023)) while not self.stopped(): - if (getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and - getInfoLabel('Control.GetLabel(1002)') in strings): - # Remember that the item IS indeed resumable - control = int(Window(10106).getFocusId()) - if control == 1002: - # Start from beginning - state.RESUME_PLAYBACK = False - elif control == 1001: - state.RESUME_PLAYBACK = True + if getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): + if getInfoLabel('Control.GetLabel(1002)') in strings: + # Remember that the item IS indeed resumable + control = int(Window(10106).getFocusId()) + if control == 1002: + # Start from beginning + state.RESUME_PLAYBACK = False + elif control == 1001: + state.RESUME_PLAYBACK = True + else: + # User chose something else from the context menu + state.RESUME_PLAYBACK = False else: - # User chose something else from the context menu + # Different context menu is displayed state.RESUME_PLAYBACK = False sleep(200) LOG.info("#====---- Special Monitor Stopped ----====#") From 82349bca889e4559b6d27efdfd0b786b34fe9c4f Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 20 Mar 2018 11:56:51 +0100 Subject: [PATCH 473/509] Version bump --- README.md | 5 +++-- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ resources/lib/variables.py | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aa4e34e7..340fbb0f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.10-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.11-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) @@ -74,11 +74,12 @@ PKC synchronizes your media from your Plex server to the native Kodi database. H + Chinese Simplified, thanks @everdream + Norwegian, thanks @mjorud + Portuguese, thanks @goncalo532 + + Russian, thanks @UncleStark + [Please help translating](https://www.transifex.com/croneter/pkc) ### Download and Installation -Install PKC via the PlexKodiConnect Kodi repository below (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically. +Install PKC via the PlexKodiConnect Kodi repository download button just below (do NOT use the standard GitHub download!). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically. | Stable version | Beta version | |----------------|--------------| diff --git a/addon.xml b/addon.xml index aca6a36b..67ab941a 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,14 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.10 (beta only): + version 2.0.11 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix playback for add-on paths +- Fix artwork for episodes for add-on paths +- Revert "Fix for "In Progress" not appearing" +- Fix playback resuming potentially too often + +version 2.0.10 (beta only): - Fix wrong item being reported using direct paths - Direct paths: correctly clean up after context menu play - Always resume playback if playback initiated via context menu diff --git a/changelog.txt b/changelog.txt index 7fe62e96..abe05b95 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 2.0.11 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix playback for add-on paths +- Fix artwork for episodes for add-on paths +- Revert "Fix for "In Progress" not appearing" +- Fix playback resuming potentially too often + version 2.0.10 (beta only): - Fix wrong item being reported using direct paths - Direct paths: correctly clean up after context menu play diff --git a/resources/lib/variables.py b/resources/lib/variables.py index d6ed6e3a..0431662e 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -75,7 +75,7 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort')) PKC_MACHINE_IDENTIFIER = None # Minimal PKC version needed for the Kodi database - otherwise need to recreate -MIN_DB_VERSION = '2.0.5' +MIN_DB_VERSION = '2.0.11' # Database paths _DB_VIDEO_VERSION = { From 4e4e1cea6bb02b654116f2cf39d62145f221e146 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 16:56:54 +0100 Subject: [PATCH 474/509] Fix resume not working for some Kodi interface languages --- resources/lib/kodimonitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index a56b84fc..4ee9cb37 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -11,7 +11,7 @@ from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \ from xbmcgui import Window import plexdb_functions as plexdb -from utils import window, settings, plex_command, thread_methods +from utils import window, settings, plex_command, thread_methods, try_encode from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER @@ -410,7 +410,8 @@ class SpecialMonitor(Thread): def run(self): LOG.info("----====# Starting Special Monitor #====----") # "Start from beginning", "Play from beginning" - strings = (getLocalizedString(12021), getLocalizedString(12023)) + strings = (try_encode(getLocalizedString(12021)), + try_encode(getLocalizedString(12023))) while not self.stopped(): if getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): if getInfoLabel('Control.GetLabel(1002)') in strings: From 1a7ac665db1d70a277fa77b87340794e085e9b50 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 17:03:26 +0100 Subject: [PATCH 475/509] Fix library sync crash TypeError - Fixes #436 --- resources/lib/kodidb_functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f0db9d5a..bb90493c 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -230,7 +230,10 @@ class KodiDBMethods(object): """ self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1', (file_id,)) - path_id = self.cursor.fetchone()[0] + try: + path_id = self.cursor.fetchone()[0] + except TypeError: + return self.cursor.execute('DELETE FROM files WHERE idFile = ?', (file_id,)) self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', From baf60c2cc8d09c129adfa398f08c346488716a66 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 17:25:21 +0100 Subject: [PATCH 476/509] Simplify error message - Fixes #435 --- resources/language/resource.language.en_gb/strings.po | 7 ++----- resources/lib/PlexAPI.py | 7 +++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 939dfbcd..94137806 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1428,12 +1428,9 @@ msgctxt "#39030" msgid "Add network credentials to allow Kodi access to your content? Note: Skipping this step may generate a message during the initial scan of your content if Kodi can't locate your content." msgstr "" +# Error message displayed when verifying Direct Path sync paths passed by Plex msgctxt "#39031" -msgid "Kodi can't locate file: " -msgstr "" - -msgctxt "#39032" -msgid "Please verify the path. You may need to verify your network credentials in the add-on settings or use different Plex paths. Stop syncing?" +msgid "Kodi cannot locate the file %s. Please verify your PKC settings. Stop syncing?" msgstr "" msgctxt "#39033" diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 20e42a46..742c1252 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -1540,10 +1540,9 @@ class API(object): Returns True if sync should stop, else False """ LOG.warn('Cannot access file: %s', url) - resp = dialog('yesno', - heading=lang(29999), - line1=lang(39031) + url, - line2=lang(39032)) + # Kodi cannot locate the file #s. Please verify your PKC settings. Stop + # syncing? + resp = dialog('yesno', heading='{plex}', line1=lang(39031) % url) return resp def set_listitem_artwork(self, listitem): From 22503657d230e16225d58ed8abfe447595f514c2 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 17:26:11 +0100 Subject: [PATCH 477/509] Revert "Revert "Fix for "In Progress" not appearing"" This reverts commit 303adbf02efd86d2598691c98644a137f1916fde. --- resources/language/resource.language.en_gb/strings.po | 5 +++++ resources/lib/initialsetup.py | 1 + resources/lib/kodimonitor.py | 1 + resources/lib/player.py | 11 ++++++++--- resources/lib/state.py | 2 ++ resources/settings.xml | 1 + 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 94137806..9766267a 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1557,6 +1557,11 @@ msgctxt "#39064" msgid "Recently Added: Also show already watched episodes" msgstr "" +# PKC settings, Appearance Tweaks +msgctxt "#39065" +msgid "Force-refresh Kodi skin on stopping playback" +msgstr "" + msgctxt "#39066" msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 34544c47..279a4991 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -68,6 +68,7 @@ def reload_pkc(): state.REMAP_PATH = settings('remapSMB') == 'true' state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') + state.FORCE_RELOAD_SKIN = settings('forceReloadSkin') == 'true' # Init some Queues() state.COMMAND_PIPELINE_QUEUE = Queue() state.COMPANION_QUEUE = Queue(maxsize=100) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4ee9cb37..0109b2e0 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,6 +47,7 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', + 'forceReloadSkin': 'FORCE_RELOAD_SKIN', 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } diff --git a/resources/lib/player.py b/resources/lib/player.py index 43f772e8..a895577d 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -4,7 +4,7 @@ from logging import getLogger import copy -from xbmc import Player +import xbmc import kodidb_functions as kodidb import plexdb_functions as plexdb @@ -104,11 +104,16 @@ def _record_playstate(status, ended): totaltime, playcount, last_played) + # Hack to force "in progress" widget to appear if it wasn't visible before + if (state.FORCE_RELOAD_SKIN and + xbmc.getCondVisibility('Window.IsVisible(Home.xml)')): + LOG.debug('Refreshing skin to update widgets') + xbmc.executebuiltin('ReloadSkin()') -class PKC_Player(Player): +class PKC_Player(xbmc.Player): def __init__(self): - Player.__init__(self) + xbmc.Player.__init__(self) LOG.info("Started playback monitor.") def onPlayBackStarted(self): diff --git a/resources/lib/state.py b/resources/lib/state.py index b35cd5e7..3cdfa910 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -30,6 +30,8 @@ INDICATE_MEDIA_VERSIONS = False RUN_LIB_SCAN = None # Number of items to fetch and display in widgets FETCH_PMS_ITEM_NUMBER = None +# Hack to force Kodi widget for "in progress" to show up if it was empty before +FORCE_RELOAD_SKIN = True # Stemming from the PKC settings.xml # Shall we show Kodi dialogs when synching? diff --git a/resources/settings.xml b/resources/settings.xml index 0ef67089..953e6288 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -144,6 +144,7 @@ + From 79d87c5b0104013620bc64cc25c31c3209bd1c61 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 17:27:57 +0100 Subject: [PATCH 478/509] Change default setting to force reload skin after playback stop to False - Fixes #434 --- resources/lib/initialsetup.py | 2 +- resources/lib/kodimonitor.py | 2 +- resources/settings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 279a4991..11cb86ae 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -68,7 +68,7 @@ def reload_pkc(): state.REMAP_PATH = settings('remapSMB') == 'true' state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') - state.FORCE_RELOAD_SKIN = settings('forceReloadSkin') == 'true' + state.FORCE_RELOAD_SKIN = settings('forceReloadSkinOnPlaybackStop') == 'true' # Init some Queues() state.COMMAND_PIPELINE_QUEUE = Queue() state.COMPANION_QUEUE = Queue(maxsize=100) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 0109b2e0..be1d2255 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -47,7 +47,7 @@ STATE_SETTINGS = { 'remapSMBphotoOrg': 'remapSMBphotoOrg', 'remapSMBphotoNew': 'remapSMBphotoNew', 'enableMusic': 'ENABLE_MUSIC', - 'forceReloadSkin': 'FORCE_RELOAD_SKIN', + 'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN', 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' } diff --git a/resources/settings.xml b/resources/settings.xml index 953e6288..f7ab311a 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -144,7 +144,7 @@ - + From bb15f6264892e0c664a7ac897b899361365b736b Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 18:51:11 +0100 Subject: [PATCH 479/509] Fix widget navigating to entire TV show not working --- addon.xml | 2 +- resources/lib/command_pipeline.py | 2 ++ resources/lib/json_rpc.py | 8 ++++++++ resources/lib/kodidb_functions.py | 18 ++++++++++++++++++ resources/lib/playback_starter.py | 17 ++++++++++++++++- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 67ab941a..7e9e9880 100644 --- a/addon.xml +++ b/addon.xml @@ -4,7 +4,7 @@ - + video audio image diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 64bda2e4..6b889445 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -57,6 +57,8 @@ class Monitor_Window(Thread): elif value.startswith('CONTEXT_menu?'): queue.put('dummy?mode=context_menu&%s' % value.replace('CONTEXT_menu?', '')) + elif value.startswith('NAVIGATE'): + queue.put(value.replace('NAVIGATE-', '')) else: raise NotImplementedError('%s not implemented' % value) else: diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 82a96b40..22501d51 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -496,3 +496,11 @@ def ping(): Pings the JSON RPC interface """ return JsonRPC('JSONRPC.Ping').execute() + + +def activate_window(window, parameters): + """ + Pass the parameters as str/unicode to open the corresponding window + """ + return JsonRPC('GUI.ActivateWindow').execute({'window': window, + 'parameters': [parameters]}) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index bb90493c..7ed8d678 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -223,6 +223,24 @@ class KodiDBMethods(object): self.cursor.execute('DELETE FROM stacktimes WHERE idFile = ?', (file[0],)) + def show_id_from_path(self, path): + """ + Returns the idShow for path [unicode] or None + """ + self.cursor.execute('SELECT idPath FROM path WHERE strPath = ? LIMIT 1', + (path, )) + try: + path_id = self.cursor.fetchone()[0] + except TypeError: + return + query = 'SELECT idShow FROM tvshowlinkpath WHERE idPath = ? LIMIT 1' + self.cursor.execute(query, (path_id, )) + try: + show_id = self.cursor.fetchone()[0] + except TypeError: + show_id = None + return show_id + def remove_file(self, file_id): """ Removes the entry for file_id from the files table. Will also delete diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 4bb2e0b4..7e8b01f8 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -7,6 +7,9 @@ from urlparse import parse_qsl import playback from context_entry import ContextMenu import state +import json_rpc as js +from pickler import pickle_me, Playback_Successful +import kodidb_functions as kodidb ############################################################################### @@ -20,7 +23,19 @@ class Playback_Starter(Thread): Processes new plays """ def triage(self, item): - _, params = item.split('?', 1) + try: + _, params = item.split('?', 1) + except ValueError: + # e.g. when plugin://...tvshows is called for entire season + with kodidb.GetKodiDB('video') as kodi_db: + show_id = kodi_db.show_id_from_path(item) + if show_id: + js.activate_window('videos', + 'videodb://tvshows/titles/%s' % show_id) + else: + LOG.error('Could not find tv show id for %s', item) + pickle_me(Playback_Successful()) + return params = dict(parse_qsl(params)) mode = params.get('mode') LOG.debug('Received mode: %s, params: %s', mode, params) From 2f99ac8282824eed25caf8103beee4b65e49a990 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 22 Mar 2018 18:53:12 +0100 Subject: [PATCH 480/509] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 340fbb0f..defd438d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.11-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.12-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 7e9e9880..81a8ffa2 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,14 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.11 (beta only): + version 2.0.12 (beta only): +- Fix resume not working for some Kodi interface languages +- Fix widget navigating to entire TV show not working +- Fix library sync crash TypeError +- Revert "Revert "Fix for "In Progress" not appearing"" +- Simplify error message + +version 2.0.11 (beta only): - WARNING: You will need to reset the Kodi database! - Fix playback for add-on paths - Fix artwork for episodes for add-on paths diff --git a/changelog.txt b/changelog.txt index abe05b95..9bd2ab5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 2.0.12 (beta only): +- Fix resume not working for some Kodi interface languages +- Fix widget navigating to entire TV show not working +- Fix library sync crash TypeError +- Revert "Revert "Fix for "In Progress" not appearing"" +- Simplify error message + version 2.0.11 (beta only): - WARNING: You will need to reset the Kodi database! - Fix playback for add-on paths From bfd4415fa1a19d1853a12ef91205c226b6bcc6b5 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 07:47:58 +0200 Subject: [PATCH 481/509] Use identical add-on paths for On Deck and browsing folders --- resources/lib/entrypoint.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index c2741eee..40e97cdb 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -562,8 +562,11 @@ def getOnDeck(viewid, mediatype, tagname, limit): if directpaths: url = api.file_path() else: - url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play' - % (v.ADDON_ID, api.plex_id(), api.plex_type())) + url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play&filename=%s' + % (v.ADDON_ID, + api.plex_id(), + api.plex_type(), + api.file_path(force_first_media=True))) xbmcplugin.addDirectoryItem( handle=HANDLE, url=url, @@ -831,8 +834,11 @@ def __build_item(xml_element): elif api.plex_type() == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: - url = 'plugin://%s/?plex_id=%s&plex_type=%s&mode=play' \ - % (v.ADDON_TYPE[api.plex_type()], api.plex_id(), api.plex_type()) + url = 'plugin://%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s' \ + % (v.ADDON_TYPE[api.plex_type()], + api.plex_id(), + api.plex_type(), + api.file_path(force_first_media=True)) xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=listitem) From d7891d6ec20d481665020eaa8e6119bae907a07a Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 08:01:24 +0200 Subject: [PATCH 482/509] New API method to retrieve only filename --- resources/lib/PlexAPI.py | 16 ++++++++++++++++ resources/lib/itemtypes.py | 16 ++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 742c1252..19cfe07a 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -112,6 +112,22 @@ class API(object): """ return self.item[self.mediastream][self.part] + def file_name(self, force_first_media=False): + """ + Returns only the filename, e.g. 'movie.mkv' as unicode or None if not + found + """ + ans = self.file_path(force_first_media=force_first_media) + if ans is None: + return + if "\\" in ans: + # Local path + filename = ans.rsplit("\\", 1)[1] + else: + # Network share + filename = ans.rsplit("/", 1)[1] + return filename + def file_path(self, force_first_media=False): """ Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv' diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 56854776..611a66eb 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -288,13 +288,7 @@ class Movies(Items): scraper='metadata.local') if do_indirect: # Set plugin path and media flags using real filename - filename = api.file_path(force_first_media=True) - if "\\" in filename: - # Local path - filename = filename.rsplit("\\", 1)[1] - else: - # Network share - filename = filename.rsplit("/", 1)[1] + filename = api.file_name(force_first_media=True) path = 'plugin://%s.movies/' % v.ADDON_ID filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' % (path, itemid, v.PLEX_TYPE_MOVIE, filename)) @@ -857,13 +851,7 @@ class TVShows(Items): else: # Set plugin path - do NOT use "intermediate" paths for the show # as with direct paths! - filename = api.file_path(force_first_media=True) - if "\\" in filename: - # Local path - filename = filename.rsplit("\\", 1)[1] - else: - # Network share - filename = filename.rsplit("/", 1)[1] + filename = api.file_name(force_first_media=True) path = 'plugin://%s.tvshows/' % v.ADDON_ID filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' % (path, itemid, v.PLEX_TYPE_EPISODE, filename)) From f0393771a9f89cb4f2374ebe29c27b03554cb76e Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 08:02:31 +0200 Subject: [PATCH 483/509] Fix "Use identical add-on paths for On Deck and browsing folders" --- resources/lib/entrypoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 40e97cdb..a4b4e763 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -566,7 +566,7 @@ def getOnDeck(viewid, mediatype, tagname, limit): % (v.ADDON_ID, api.plex_id(), api.plex_type(), - api.file_path(force_first_media=True))) + api.file_name(force_first_media=True))) xbmcplugin.addDirectoryItem( handle=HANDLE, url=url, @@ -838,7 +838,7 @@ def __build_item(xml_element): % (v.ADDON_TYPE[api.plex_type()], api.plex_id(), api.plex_type(), - api.file_path(force_first_media=True)) + api.file_name(force_first_media=True)) xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=listitem) From b23c6e293275fbfb687fc406329d78311a1c5559 Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 08:20:39 +0200 Subject: [PATCH 484/509] Fix resume for On Deck and browse by folder --- resources/lib/entrypoint.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index a4b4e763..8f922eed 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -567,6 +567,9 @@ def getOnDeck(viewid, mediatype, tagname, limit): api.plex_id(), api.plex_type(), api.file_name(force_first_media=True))) + if api.resume_point(): + listitem.setProperty('resumetime', + str(api.resume_point())) xbmcplugin.addDirectoryItem( handle=HANDLE, url=url, @@ -839,6 +842,8 @@ def __build_item(xml_element): api.plex_id(), api.plex_type(), api.file_name(force_first_media=True)) + if api.resume_point(): + listitem.setProperty('resumetime', str(api.resume_point())) xbmcplugin.addDirectoryItem(handle=HANDLE, url=url, listitem=listitem) From fc836bebe6ee634cfab13ffaecdd75fd1da72fac Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 18:07:16 +0200 Subject: [PATCH 485/509] Use an empty video file to "fail" playback --- empty_video.mp4 | Bin 0 -> 262 bytes resources/lib/playback.py | 5 ++++- resources/lib/variables.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 empty_video.mp4 diff --git a/empty_video.mp4 b/empty_video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..299de0ff6ec3a618ea543e786a2d9fcfe746a97d GIT binary patch literal 262 zcmZQzU{FXasVvAW&d+6FU}6B#Kx~v)mTZ_?U}DI?z`&7Kl$r{nb5jyafb_N8{QNQ? zos(OZkpiTV0P_nlhmnB+h!6mU0~AK%J0MhIV=(~*lS)%c5`lD7ZYr1tsZ-2I$teOc xKp;0Ivna8kAP2&Okh+;U#UKZ(t}MyV2hy@Y_k#=pTkn%tmS$?9XJ`OK1^^C7B=rCQ literal 0 HcmV?d00001 diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 60db0262..0822d7ea 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -3,6 +3,7 @@ Used to kick off Kodi playback """ from logging import getLogger from threading import Thread +from os.path import join from xbmc import Player, sleep @@ -26,6 +27,8 @@ import state LOG = getLogger("PLEX." + __name__) # Do we need to return ultimately with a setResolvedUrl? RESOLVE = True +# We're "failing" playback with a video of 0 length +NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4') ############################################################################### @@ -173,7 +176,7 @@ def _ensure_resolve(abort=False): # Because playback won't start with context menu play state.PKC_CAUSED_STOP = True result = Playback_Successful() - result.listitem = PKC_ListItem(path='PKC_Dummy_Path_Which_Fails') + result.listitem = PKC_ListItem(path=NULL_VIDEO) pickle_me(result) if abort: # Reset some playback variables diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 0431662e..ed70569c 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -33,6 +33,7 @@ _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_VERSION = _ADDON.getAddonInfo('version') +ADDON_FOLDER = try_decode(xbmc.translatePath('special://home')) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) From 6aa3e612cf48e741327b53c0c6f6961d1288d09c Mon Sep 17 00:00:00 2001 From: Croneter Date: Tue, 27 Mar 2018 18:20:36 +0200 Subject: [PATCH 486/509] Adjust playback cleanup for empty video file --- resources/lib/player.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index a895577d..7c6fdd47 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -145,15 +145,15 @@ class PKC_Player(xbmc.Player): Will be called when playback is stopped by the user """ LOG.debug("ONPLAYBACK_STOPPED") - if state.PKC_CAUSED_STOP is True: - state.PKC_CAUSED_STOP = False - LOG.debug('PKC caused this playback stop - ignoring') - else: - playback_cleanup() + playback_cleanup() def onPlayBackEnded(self): """ Will be called when playback ends due to the media file being finished """ LOG.debug("ONPLAYBACK_ENDED") - playback_cleanup(ended=True) + if state.PKC_CAUSED_STOP is True: + state.PKC_CAUSED_STOP = False + LOG.debug('PKC caused this playback stop - ignoring') + else: + playback_cleanup() From 9f82b05c11026ce76f7cf3c1fe870e7b5eb8d399 Mon Sep 17 00:00:00 2001 From: Croneter Date: Wed, 28 Mar 2018 08:04:03 +0200 Subject: [PATCH 487/509] Fix rare KeyError for playback including trailers --- resources/lib/kodimonitor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index be1d2255..409b3812 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -212,6 +212,8 @@ class KodiMonitor(Monitor): } Will NOT be called if playback initiated by Kodi widgets """ + if 'id' not in data['item']: + return old = state.OLD_PLAYER_STATES[data['playlistid']] if (not state.DIRECT_PATHS and data['position'] == 0 and not PQ.PLAYQUEUES[data['playlistid']].items and From f23f6da62703072912c6eba46838c3552a93d9aa Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 29 Mar 2018 07:33:07 +0200 Subject: [PATCH 488/509] Fix PKC sometimes telling wrong item being played --- resources/lib/kodimonitor.py | 77 ++++++++++++++++++++---------------- resources/lib/state.py | 3 -- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 409b3812..ef473e74 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -60,6 +60,7 @@ class KodiMonitor(Monitor): """ def __init__(self): self.xbmcplayer = Player() + self._already_slept = False Monitor.__init__(self) for playerid in state.PLAYER_STATES: state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) @@ -257,30 +258,17 @@ class KodiMonitor(Monitor): else: LOG.debug('Detected PKC clear - ignoring') - def _get_ids(self, json_item): + def _get_ids(self, kodi_id, kodi_type, path): """ + Returns the tuple (plex_id, plex_type) or (None, None) """ - kodi_id = json_item.get('id') - kodi_type = json_item.get('type') - path = json_item.get('file') - # Plex id will NOT be set with direct paths - plex_id = state.PLEX_IDS.get(path) - try: - plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[kodi_type] - except KeyError: - plex_type = None # No Kodi id returned by Kodi, even if there is one. Ex: Widgets - if plex_id and not kodi_id: - with plexdb.Get_Plex_DB() as plex_db: - plex_dbitem = plex_db.getItem_byId(plex_id) - try: - kodi_id = plex_dbitem[0] - except TypeError: - kodi_id = None + plex_id = None + plex_type = None # If using direct paths and starting playback from a widget - if not kodi_id and kodi_type and path and not path.startswith('http'): + if not kodi_id and kodi_type and path: kodi_id = kodiid_from_filename(path, kodi_type) - if not plex_id and kodi_id and kodi_type: + if kodi_id: with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) try: @@ -289,7 +277,7 @@ class KodiMonitor(Monitor): except TypeError: # No plex id, hence item not in the library. E.g. clips pass - return kodi_id, kodi_type, plex_id, plex_type + return plex_id, plex_type @staticmethod def _add_remaining_items_to_playlist(playqueue): @@ -309,6 +297,24 @@ class KodiMonitor(Monitor): except PL.PlaylistError: LOG.info('Could not build Plex playlist for: %s', items) + def _json_item(self, playerid): + """ + Uses JSON RPC to get the playing item's info and returns the tuple + kodi_id, kodi_type, path + or None each time if not found. + """ + if not self._already_slept: + # SLEEP before calling this for the first time just after playback + # start as Kodi updates this info very late!! Might get previous + # element otherwise + self._already_slept = True + sleep(1000) + json_item = js.get_item(playerid) + LOG.debug('Kodi playing item properties: %s', json_item) + return (json_item.get('id'), + json_item.get('type'), + json_item.get('file')) + @LOCKER.lockthis def PlayBackStart(self, data): """ @@ -319,9 +325,9 @@ class KodiMonitor(Monitor): } Unfortunately when using Widgets, Kodi doesn't tell us shit """ + self._already_slept = False # Get the type of media we're playing try: - kodi_type = data['item']['type'] playerid = data['player']['playerid'] except (TypeError, KeyError): LOG.info('Aborting playback report - item invalid for updates %s', @@ -338,36 +344,41 @@ class KodiMonitor(Monitor): state.ACTIVE_PLAYERS.append(playerid) playqueue = PQ.PLAYQUEUES[playerid] info = js.get_player_props(playerid) - json_item = js.get_item(playerid) pos = info['position'] if info['position'] != -1 else 0 LOG.debug('Detected position %s for %s', pos, playqueue) - LOG.debug('Detected Kodi playing item properties: %s', json_item) status = state.PLAYER_STATES[playerid] - path = json_item.get('file') + kodi_id = data.get('id') + kodi_type = data.get('type') + path = data.get('file') try: item = playqueue.items[pos] except IndexError: # PKC playqueue not yet initialized + LOG.debug('Position %s not in PKC playqueue yet', pos) initialize = True else: - if item.kodi_id: - if (item.kodi_id != json_item.get('id') or - item.kodi_type != json_item.get('type')): + if not kodi_id: + kodi_id, kodi_type, path = self._json_item(playerid) + if kodi_id and item.kodi_id: + if item.kodi_id != kodi_id or item.kodi_type != kodi_type: + LOG.debug('Detected different Kodi id') initialize = True else: initialize = False else: # E.g. clips set-up previously with no Kodi DB entry - if item.file != json_item.get('file'): + if not path: + kodi_id, kodi_type, path = self._json_item(playerid) + if item.file != path: + LOG.debug('Detected different path') initialize = True else: initialize = False if initialize: LOG.debug('Need to initialize Plex and PKC playqueue') - if not json_item.get('id') or not json_item.get('type'): - LOG.debug('No Kodi id or type obtained - aborting report') - return - kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item) + if not kodi_id or not kodi_type: + kodi_id, kodi_type, path = self._json_item(playerid) + plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path) if not plex_id: LOG.debug('No Plex id obtained - aborting playback report') return @@ -381,7 +392,6 @@ class KodiMonitor(Monitor): container_key = '/playQueues/%s' % container_key elif plex_id is not None: container_key = '/library/metadata/%s' % plex_id - LOG.debug('Set the Plex container_key to: %s', container_key) else: LOG.debug('No need to initialize playqueues') kodi_id = item.kodi_id @@ -393,6 +403,7 @@ class KodiMonitor(Monitor): else: container_key = '/library/metadata/%s' % plex_id status.update(info) + LOG.debug('Set the Plex container_key to: %s', container_key) status['container_key'] = container_key status['file'] = path status['kodi_id'] = kodi_id diff --git a/resources/lib/state.py b/resources/lib/state.py index 3cdfa910..88c3e599 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -143,9 +143,6 @@ PLAYSTATE = { 'playmethod': None, 'playcount': None } -# Dict containing all filenames as keys with plex id as values - used for addon -# paths for playback (since we're not receiving a Kodi id) -PLEX_IDS = {} PLAYED_INFO = {} # Set by SpecialMonitor - did user choose to resume playback or start from the # beginning? From cd5b3a3e2bf297eed39482262a06139608307810 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 29 Mar 2018 07:35:54 +0200 Subject: [PATCH 489/509] Remove obsolete import --- resources/lib/kodimonitor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index ef473e74..673e2d8f 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -21,7 +21,6 @@ import playqueue as PQ import json_rpc as js import playlist_func as PL import state -import variables as v ############################################################################### From 3dd10ba29cd89573c069d6283844d7f6ca292b36 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 29 Mar 2018 07:37:20 +0200 Subject: [PATCH 490/509] Don't tell PMS last item is playing if non-Plex item is played --- resources/lib/kodimonitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 673e2d8f..631057dd 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -380,6 +380,7 @@ class KodiMonitor(Monitor): plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path) if not plex_id: LOG.debug('No Plex id obtained - aborting playback report') + status = copy.deepcopy(state.PLAYSTATE) return item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) # Set the Plex container key (e.g. using the Plex playqueue) From 27c6fedc9d47e63df18a77e8c743464ccb1a0ff1 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 29 Mar 2018 08:00:14 +0200 Subject: [PATCH 491/509] Version bump --- README.md | 2 +- addon.xml | 12 ++++++++++-- changelog.txt | 8 ++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index defd438d..32cfe472 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.12-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.13-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 81a8ffa2..611ca128 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,15 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.12 (beta only): + version 2.0.13 (beta only): +- Fix resume for On Deck and browse by folder +- Fix PKC sometimes telling wrong item being played +- Don't tell PMS last item is playing if non-Plex item is played +- Fix rare KeyError for playback including trailers +- Use an empty video file to "fail" playback +- Use identical add-on paths for On Deck and browsing folders + +version 2.0.12 (beta only): - Fix resume not working for some Kodi interface languages - Fix widget navigating to entire TV show not working - Fix library sync crash TypeError diff --git a/changelog.txt b/changelog.txt index 9bd2ab5a..5b03481f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,11 @@ +version 2.0.13 (beta only): +- Fix resume for On Deck and browse by folder +- Fix PKC sometimes telling wrong item being played +- Don't tell PMS last item is playing if non-Plex item is played +- Fix rare KeyError for playback including trailers +- Use an empty video file to "fail" playback +- Use identical add-on paths for On Deck and browsing folders + version 2.0.12 (beta only): - Fix resume not working for some Kodi interface languages - Fix widget navigating to entire TV show not working From 9a4533d7e0d61aaf543aac1213409334680deb59 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 31 Mar 2018 15:37:05 +0200 Subject: [PATCH 492/509] Ensure that playstate for ended (not stopped) video is recorded correctly --- resources/lib/player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 7c6fdd47..04df82bd 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -82,7 +82,7 @@ def _record_playstate(status, ended): playcount = status['playcount'] last_played = unix_date_to_kodi(unix_timestamp()) if playcount is None: - LOG.info('playcount not found, looking it up in the Kodi DB') + LOG.debug('playcount not found, looking it up in the Kodi DB') with kodidb.GetKodiDB('video') as kodi_db: playcount = kodi_db.get_playcount(kodi_db_item[1]) playcount = 0 if playcount is None else playcount @@ -156,4 +156,4 @@ class PKC_Player(xbmc.Player): state.PKC_CAUSED_STOP = False LOG.debug('PKC caused this playback stop - ignoring') else: - playback_cleanup() + playback_cleanup(ended=True) From e81bee01019592b0b8668dc0fbd0c9e112ab6f4c Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 31 Mar 2018 18:51:03 +0200 Subject: [PATCH 493/509] Fix resetting PKC player state - Should fix #445 --- resources/lib/kodimonitor.py | 2 +- resources/lib/player.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 631057dd..04f24877 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -380,7 +380,7 @@ class KodiMonitor(Monitor): plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path) if not plex_id: LOG.debug('No Plex id obtained - aborting playback report') - status = copy.deepcopy(state.PLAYSTATE) + state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) return item = PL.init_Plex_playlist(playqueue, plex_id=plex_id) # Set the Plex container key (e.g. using the Plex playqueue) diff --git a/resources/lib/player.py b/resources/lib/player.py index 04df82bd..982c31ce 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -48,7 +48,7 @@ def playback_cleanup(ended=False): # started playback via PMS _record_playstate(status, ended) # Reset the player's status - status = copy.deepcopy(state.PLAYSTATE) + state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) # As all playback has halted, reset the players that have been active state.ACTIVE_PLAYERS = [] LOG.debug('Finished PKC playback cleanup') From dcf2b9b4e460fe7d1148edc6a456babe9171f2ca Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 31 Mar 2018 20:32:55 +0200 Subject: [PATCH 494/509] Play the selected element first, then add the Kodi playqueue to the Plex playqueue - Fixes #446 --- resources/lib/playback.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 0822d7ea..116e3b96 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -96,13 +96,14 @@ def _playback_init(plex_id, plex_type, playqueue, pos): if playqueue.kodi_pl.size() > 1: # Special case - we already got a filled Kodi playqueue try: - _init_existing_kodi_playlist(playqueue) + _init_existing_kodi_playlist(playqueue, pos) except PL.PlaylistError: LOG.error('Aborting playback_init for longer Kodi playlist') _ensure_resolve(abort=True) return - # Now we need to use setResolvedUrl for the item at position pos - _conclude_playback(playqueue, pos) + # Now we need to use setResolvedUrl for the item at position ZERO + # playqueue.py will pick up the missing items + _conclude_playback(playqueue, 0) return # "Usual" case - consider trailers and parts and build both Kodi and Plex # playqueues @@ -185,21 +186,21 @@ def _ensure_resolve(abort=False): state.RESUME_PLAYBACK = False -def _init_existing_kodi_playlist(playqueue): +def _init_existing_kodi_playlist(playqueue, pos): """ Will take the playqueue's kodi_pl with MORE than 1 element and initiate playback (without adding trailers) """ LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size()) - for i, kodi_item in enumerate(js.playlist_get_items(playqueue.playlistid)): - if i == 0: - item = PL.init_Plex_playlist(playqueue, kodi_item=kodi_item) - else: - item = PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=kodi_item) - item.force_transcode = state.FORCE_TRANSCODE - LOG.debug('Done building Plex playlist from Kodi playlist') + kodi_items = js.playlist_get_items(playqueue.playlistid) + if not kodi_items: + raise PL.PlaylistError('No Kodi items returned') + item = PL.init_Plex_playlist(playqueue, kodi_item=kodi_items[pos]) + item.force_transcode = state.FORCE_TRANSCODE + # playqueue.py will add the rest - this will likely put the PMS under + # a LOT of strain if the following Kodi setting is enabled: + # Settings -> Player -> Videos -> Play next video automatically + LOG.debug('Done init_existing_kodi_playlist') def _prep_playlist_stack(xml): From c05b772e902183b8ed50ecf9180cfe3f30e9eb58 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 31 Mar 2018 20:34:09 +0200 Subject: [PATCH 495/509] Make sure that LOCK is released after adding one element - Partially fixes #446 --- resources/lib/playqueue.py | 70 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 551d89b0..e2634d9f 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -179,39 +179,42 @@ class PlayqueueMonitor(Thread): elif identical: LOG.debug('Detected playqueue item %s moved to position %s', i + j, i) - PL.move_playlist_item(playqueue, i + j, i) + with LOCK: + PL.move_playlist_item(playqueue, i + j, i) del old[j], index[j] break else: LOG.debug('Detected new Kodi element at position %s: %s ', i, new_item) - try: - if playqueue.id is None: - PL.init_Plex_playlist(playqueue, kodi_item=new_item) + with LOCK: + try: + if playqueue.id is None: + PL.init_Plex_playlist(playqueue, kodi_item=new_item) + else: + PL.add_item_to_PMS_playlist(playqueue, + i, + kodi_item=new_item) + except PL.PlaylistError: + # Could not add the element + pass + except IndexError: + # This is really a hack - happens when using Addon Paths + # and repeatedly starting the same element. Kodi will + # then not pass kodi id nor file path AND will also not + # start-up playback. Hence kodimonitor kicks off + # playback. Also see kodimonitor.py - _playlist_onadd() + pass else: - PL.add_item_to_PMS_playlist(playqueue, - i, - kodi_item=new_item) - except PL.PlaylistError: - # Could not add the element - pass - except IndexError: - # This is really a hack - happens when using Addon Paths - # and repeatedly starting the same element. Kodi will then - # not pass kodi id nor file path AND will also not - # start-up playback. Hence kodimonitor kicks off playback. - # Also see kodimonitor.py - _playlist_onadd() - pass - else: - for j in range(i, len(index)): - index[j] += 1 + for j in range(i, len(index)): + index[j] += 1 for i in reversed(index): if self.stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return LOG.debug('Detected deletion of playqueue element at pos %s', i) - PL.delete_playlist_item_from_PMS(playqueue, i) + with LOCK: + PL.delete_playlist_item_from_PMS(playqueue, i) LOG.debug('Done comparing playqueues') def run(self): @@ -223,18 +226,17 @@ class PlayqueueMonitor(Thread): if stopped(): break sleep(1000) - with LOCK: - for playqueue in PLAYQUEUES: - kodi_pl = js.playlist_get_items(playqueue.playlistid) - if playqueue.old_kodi_pl != kodi_pl: - if playqueue.id is None and (not state.DIRECT_PATHS or - state.CONTEXT_MENU_PLAY): - # Only initialize if directly fired up using direct - # paths. Otherwise let default.py do its magic - LOG.debug('Not yet initiating playback') - else: - # compare old and new playqueue - self._compare_playqueues(playqueue, kodi_pl) - playqueue.old_kodi_pl = list(kodi_pl) + for playqueue in PLAYQUEUES: + kodi_pl = js.playlist_get_items(playqueue.playlistid) + if playqueue.old_kodi_pl != kodi_pl: + if playqueue.id is None and (not state.DIRECT_PATHS or + state.CONTEXT_MENU_PLAY): + # Only initialize if directly fired up using direct + # paths. Otherwise let default.py do its magic + LOG.debug('Not yet initiating playback') + else: + # compare old and new playqueue + self._compare_playqueues(playqueue, kodi_pl) + playqueue.old_kodi_pl = list(kodi_pl) sleep(200) LOG.info("----===## PlayqueueMonitor stopped ##===----") From cb5565440239cb29757988f89edf9726ccfa6257 Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 31 Mar 2018 21:06:51 +0200 Subject: [PATCH 496/509] Version bump --- README.md | 2 +- addon.xml | 10 ++++++++-- changelog.txt | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 32cfe472..3e0e9f9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.13-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.14-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 611ca128..3af210f4 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,13 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.13 (beta only): + version 2.0.14 (beta only): +- Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing +- Play the selected element first, then add the Kodi playqueue to the Plex playqueue +- Ensure that playstate for ended (not stopped) video is recorded correctly +- Make sure that LOCK is released after adding one element + +version 2.0.13 (beta only): - Fix resume for On Deck and browse by folder - Fix PKC sometimes telling wrong item being played - Don't tell PMS last item is playing if non-Plex item is played diff --git a/changelog.txt b/changelog.txt index 5b03481f..2d516a18 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +version 2.0.14 (beta only): +- Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing +- Play the selected element first, then add the Kodi playqueue to the Plex playqueue +- Ensure that playstate for ended (not stopped) video is recorded correctly +- Make sure that LOCK is released after adding one element + version 2.0.13 (beta only): - Fix resume for On Deck and browse by folder - Fix PKC sometimes telling wrong item being played From 7d38ccf504e1224f83ee3b42219f5c222f329f01 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 1 Apr 2018 10:18:15 +0200 Subject: [PATCH 497/509] Hopefully fix ValueError for datetime.utcnow() - Should fix #448 --- resources/lib/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 3c3ac8b6..8a8319b3 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -13,7 +13,6 @@ from time import localtime, strftime from unicodedata import normalize import xml.etree.ElementTree as etree from functools import wraps, partial -from calendar import timegm from os.path import join from os import remove, walk, makedirs from shutil import rmtree @@ -33,6 +32,7 @@ LOG = getLogger("PLEX." + __name__) WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') +EPOCH = datetime.utcfromtimestamp(0) ############################################################################### # Main methods @@ -331,7 +331,7 @@ def unix_timestamp(seconds_into_the_future=None): future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future) else: future = datetime.utcnow() - return timegm(future.timetuple()) + return int((future - EPOCH).total_seconds()) def kodi_sql(media_type=None): From 35ff51e39f09907d573de6b5ed6b92e425510e05 Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 1 Apr 2018 10:45:22 +0200 Subject: [PATCH 498/509] Modify import --- resources/lib/initialsetup.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 11cb86ae..c74a9df6 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -14,7 +14,7 @@ from userclient import UserClient from clientinfo import getDeviceId import PlexFunctions as PF import plex_tv -from json_rpc import get_setting, set_setting +import json_rpc as js import playqueue as PQ from videonodes import VideoNodes import state @@ -103,16 +103,16 @@ def set_webserver(): """ Set the Kodi webserver details - used to set the texture cache """ - if get_setting('services.webserver') in (None, False): + if js.get_setting('services.webserver') in (None, False): # Enable the webserver, it is disabled - set_setting('services.webserver', True) + js.set_setting('services.webserver', True) # Set standard port and username # set_setting('services.webserverport', 8080) # set_setting('services.webserverusername', 'kodi') # Webserver already enabled - state.WEBSERVER_PORT = get_setting('services.webserverport') - state.WEBSERVER_USERNAME = get_setting('services.webserverusername') - state.WEBSERVER_PASSWORD = get_setting('services.webserverpassword') + state.WEBSERVER_PORT = js.get_setting('services.webserverport') + state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername') + state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword') def _write_pms_settings(url, token): From 0a978188b4ca90280101d293d9d006ce5a19976c Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 16:27:08 +0200 Subject: [PATCH 499/509] New JSON functions to retrieve and set Kodi settings --- resources/lib/json_rpc.py | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 22501d51..bc1589d0 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -504,3 +504,52 @@ def activate_window(window, parameters): """ return JsonRPC('GUI.ActivateWindow').execute({'window': window, 'parameters': [parameters]}) + + +def settings_getsections(): + ''' + Retrieve all Kodi settings sections + ''' + return JsonRPC('Settings.GetSections').execute({'level': 'expert'}) + + +def settings_getcategories(): + ''' + Retrieve all Kodi settings categories (one level below sections) + ''' + return JsonRPC('Settings.GetCategories').execute({'level': 'expert'}) + + +def settings_getsettings(filter_params): + ''' + Get all the settings for + filter_params = {'category': , 'section': } + e.g. = {'category':'videoplayer', 'section':'player'} + ''' + return JsonRPC('Settings.GetSettings').execute({ + 'level': 'expert', + 'filter': filter_params + }) + + +def settings_getsettingvalue(setting): + ''' + Pass in the setting id as a string (as retrieved from settings_getsettings), + e.g. 'videoplayer.autoplaynextitem' or None is something went wrong + ''' + ret = JsonRPC('Settings.GetSettingValue').execute({'setting': setting}) + try: + ret = ret['result']['value'] + except (TypeError, KeyError): + ret = None + return ret + + +def settings_setsettingvalue(setting, value): + ''' + Set the Kodi setting (str) to value (type depends, see JSON wiki) + ''' + return JsonRPC('Settings.SetSettingValue').execute({ + 'setting': setting, + 'value': value + }) From 5facbddfc7ebba58436231e8cca66f7c729836c8 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 16:29:56 +0200 Subject: [PATCH 500/509] Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report --- .../language/resource.language.en_gb/strings.po | 5 +++++ resources/lib/initialsetup.py | 14 ++++++++++++++ resources/settings.xml | 1 + 3 files changed, 20 insertions(+) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 9766267a..6a5b7268 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -31,6 +31,11 @@ msgctxt "#30002" msgid "Preferred playback method" msgstr "" +# Warning displayed if Kodi setting is enabled. Be sure to escape the quotes again! The exact wording can be found in the Kodi settings, player settings, videos +msgctxt "#30003" +msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?" +msgstr "" + msgctxt "#30004" msgid "Log level" msgstr "" diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index c74a9df6..a1bdb352 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -529,6 +529,20 @@ class InitialSetup(object): # Do we need to migrate stuff? check_migration() + # Display a warning if Kodi puts ALL movies into the queue, basically + # breaking playback reporting for PKC + if js.settings_getsettingvalue('videoplayer.autoplaynextitem'): + LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!') + if settings('warned_setting_videoplayer.autoplaynextitem') == 'false': + # Only warn once + settings('warned_setting_videoplayer.autoplaynextitem', + value='true') + # Warning: Kodi setting "Play next video automatically" is + # enabled. This could break PKC. Deactivate? + if dialog('yesno', lang(29999), lang(30003)): + js.settings_setsettingvalue('videoplayer.autoplaynextitem', + False) + # If a Plex server IP has already been set # return only if the right machine identifier is found if self.server: diff --git a/resources/settings.xml b/resources/settings.xml index f7ab311a..cca16fda 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -122,6 +122,7 @@ + From 8c5baf80eee1e4ac5cb389e1078a73e58ce364f7 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 17:09:44 +0200 Subject: [PATCH 501/509] Only remember which player has been active if we got a Plex id --- resources/lib/kodimonitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 04f24877..c6d0efda 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -339,8 +339,6 @@ class KodiMonitor(Monitor): except IndexError: LOG.error('Could not retreive active player - aborting') return - # Remember that this player has been active - state.ACTIVE_PLAYERS.append(playerid) playqueue = PQ.PLAYQUEUES[playerid] info = js.get_player_props(playerid) pos = info['position'] if info['position'] != -1 else 0 @@ -402,6 +400,8 @@ class KodiMonitor(Monitor): container_key = '/playQueues/%s' % playqueue.id else: container_key = '/library/metadata/%s' % plex_id + # Remember that this player has been active + state.ACTIVE_PLAYERS.append(playerid) status.update(info) LOG.debug('Set the Plex container_key to: %s', container_key) status['container_key'] = container_key From 98a544a764be00ad26b761a2898bcb87702672a3 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 17:19:34 +0200 Subject: [PATCH 502/509] Don't clean the Kodi file table --- resources/lib/player.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index 982c31ce..67c5dcdc 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -55,9 +55,6 @@ def playback_cleanup(ended=False): def _record_playstate(status, ended): - with kodidb.GetKodiDB('video') as kodi_db: - # Hack - remove any obsolete file entries Kodi made - kodi_db.clean_file_table() if not status['plex_id']: LOG.debug('No Plex id found to record playstate for status %s', status) return From 5b58db6cec6835b5d099614333907f5336c051b5 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 18:07:49 +0200 Subject: [PATCH 503/509] Fix Plex Companion thinking video is playing again - Fixes #449 --- 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 2db7e765..17d8019a 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -197,7 +197,7 @@ class SubscriptionMgr(object): if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') self.server = self.server.replace('/', '') - status = 'paused' if info['speed'] == '0' else 'playing' + status = 'paused' if int(info['speed']) == 0 else 'playing' duration = kodi_time_to_millis(info['totaltime']) shuffle = '1' if info['shuffled'] else '0' mute = '1' if info['muted'] is True else '0' @@ -362,7 +362,7 @@ class SubscriptionMgr(object): item = playqueue.items[info['position']] except IndexError: return self.last_params - status = 'paused' if info['speed'] == '0' else 'playing' + status = 'paused' if int(info['speed']) == 0 else 'playing' params = { 'state': status, 'ratingKey': item.plex_id, From 97a78eb4034c4cb0fa8aaedda6afdb77fbc8ffa5 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 18:13:05 +0200 Subject: [PATCH 504/509] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3e0e9f9f..2373e18a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.14-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.15-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 3af210f4..a47e3fe9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,14 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.14 (beta only): + version 2.0.15 (beta only): +- Fix Plex Companion thinking video is playing again +- Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report +- Don't clean the Kodi file table +- Only remember which player has been active if we got a Plex id +- Hopefully fix ValueError for datetime.utcnow() + +version 2.0.14 (beta only): - Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing - Play the selected element first, then add the Kodi playqueue to the Plex playqueue - Ensure that playstate for ended (not stopped) video is recorded correctly diff --git a/changelog.txt b/changelog.txt index 2d516a18..ef786ea8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 2.0.15 (beta only): +- Fix Plex Companion thinking video is playing again +- Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report +- Don't clean the Kodi file table +- Only remember which player has been active if we got a Plex id +- Hopefully fix ValueError for datetime.utcnow() + version 2.0.14 (beta only): - Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing - Play the selected element first, then add the Kodi playqueue to the Plex playqueue From f481bd2980eb5dba425bd25b9e32041eefd658a7 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 18:32:52 +0200 Subject: [PATCH 505/509] Do NOT delete playstates before getting new ones from the PMS --- resources/lib/kodidb_functions.py | 6 ------ resources/lib/librarysync.py | 2 -- 2 files changed, 8 deletions(-) diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 7ed8d678..e53a41f1 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -689,12 +689,6 @@ class KodiDBMethods(object): resume = None return resume - def delete_all_playstates(self): - """ - Entirely resets the table bookmark and thus all resume points - """ - self.cursor.execute("DELETE FROM bookmark") - def get_playcount(self, file_id): """ Returns the playcount for the item file_id or None if not found diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 6bfb196f..a5cfc91c 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -251,8 +251,6 @@ class LibrarySync(Thread): with kodidb.GetKodiDB('video') as kodi_db: # Setup the paths for addon-paths (even when using direct paths) kodi_db.setup_path_table() - # Delete all resume points because we'll get new ones - kodi_db.delete_all_playstates() process = { 'movies': self.PlexMovies, From fe19451a2ddac40ece19d00babe075e00ccd96d7 Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 2 Apr 2018 18:35:23 +0200 Subject: [PATCH 506/509] Version bump --- README.md | 2 +- addon.xml | 7 +++++-- changelog.txt | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2373e18a..38e0653a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.0.15-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.0.16-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index a47e3fe9..37f4d703 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -61,7 +61,10 @@ Indbygget Integration af Plex i Kodi Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar! Brug på eget ansvar - version 2.0.15 (beta only): + version 2.0.16 (beta only): +- Do NOT delete playstates before getting new ones from the PMS + +version 2.0.15 (beta only): - Fix Plex Companion thinking video is playing again - Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report - Don't clean the Kodi file table diff --git a/changelog.txt b/changelog.txt index ef786ea8..9e30fce7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +version 2.0.16 (beta only): +- Do NOT delete playstates before getting new ones from the PMS + version 2.0.15 (beta only): - Fix Plex Companion thinking video is playing again - Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report From 01d1d342aafdc5599ed7b5d75db207aaf1408fd3 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 3 Apr 2018 12:43:59 +0200 Subject: [PATCH 507/509] Activate Kodi background updates to hide "Compressing Database" --- resources/lib/initialsetup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index a1bdb352..4431b906 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -542,6 +542,9 @@ class InitialSetup(object): if dialog('yesno', lang(29999), lang(30003)): js.settings_setsettingvalue('videoplayer.autoplaynextitem', False) + # Set any video library updates to happen in the background in order to + # hide "Compressing database" + js.settings_setsettingvalue('videolibrary.backgroundupdate', True) # If a Plex server IP has already been set # return only if the right machine identifier is found From 14ef7ae247f093519bfc58c38c301ab083292980 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 3 Apr 2018 16:53:59 +0200 Subject: [PATCH 508/509] Fix information screen and Plex option not working - Fixes #440 --- resources/lib/entrypoint.py | 49 ++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 8f922eed..7fd7dfc9 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -193,47 +193,40 @@ def GetSubFolders(nodeindex): ##### LISTITEM SETUP FOR VIDEONODES ##### def createListItem(item, append_show_title=False, append_sxxexx=False): + log.debug('createListItem called with append_show_title %s, append_sxxexx ' + '%s, item: %s', append_show_title, append_sxxexx, item) title = item['title'] li = ListItem(title) - li.setProperty('IsPlayable', "true") - + li.setProperty('IsPlayable', 'true') metadata = { 'duration': str(item['runtime']/60), 'Plot': item['plot'], 'Playcount': item['playcount'] } - if "episode" in item: + if 'episode' in item: episode = item['episode'] metadata['Episode'] = episode - - if "season" in item: + if 'season' in item: season = item['season'] metadata['Season'] = season - if season and episode: - li.setProperty('episodeno', "s%.2de%.2d" % (season, episode)) + li.setProperty('episodeno', 's%.2de%.2d' % (season, episode)) if append_sxxexx is True: - title = "S%.2dE%.2d - %s" % (season, episode, title) - - if "firstaired" in item: + title = 'S%.2dE%.2d - %s' % (season, episode, title) + if 'firstaired' in item: metadata['Premiered'] = item['firstaired'] - - if "showtitle" in item: + if 'showtitle' in item: metadata['TVshowTitle'] = item['showtitle'] if append_show_title is True: title = item['showtitle'] + ' - ' + title - - if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']),1)) - - if "director" in item: - metadata['Director'] = " / ".join(item['director']) - - if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) - - if "cast" in item: + if 'rating' in item: + metadata['Rating'] = str(round(float(item['rating']), 1)) + if 'director' in item: + metadata['Director'] = item['director'] + if 'writer' in item: + metadata['Writer'] = item['writer'] + if 'cast' in item: cast = [] castandrole = [] for person in item['cast']: @@ -244,16 +237,17 @@ def createListItem(item, append_show_title=False, append_sxxexx=False): metadata['CastAndRole'] = castandrole metadata['Title'] = title + metadata['mediatype'] = 'episode' + metadata['dbid'] = str(item['episodeid']) li.setLabel(title) + li.setInfo(type='Video', infoLabels=metadata) - li.setInfo(type="Video", infoLabels=metadata) li.setProperty('resumetime', str(item['resume']['position'])) li.setProperty('totaltime', str(item['resume']['total'])) li.setArt(item['art']) - li.setThumbnailImage(item['art'].get('thumb','')) + li.setThumbnailImage(item['art'].get('thumb', '')) li.setArt({'icon': 'DefaultTVShows.png'}) - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart','')) + li.setProperty('fanart_image', item['art'].get('tvshow.fanart', '')) try: li.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) except TypeError: @@ -262,7 +256,6 @@ def createListItem(item, append_show_title=False, append_sxxexx=False): for key, value in item['streamdetails'].iteritems(): for stream in value: li.addStreamInfo(key, stream) - return li ##### GET NEXTUP EPISODES FOR TAGNAME ##### From c12c9c08d88daeeaca83ef272bc2c00b09e76fd3 Mon Sep 17 00:00:00 2001 From: croneter Date: Tue, 3 Apr 2018 17:07:37 +0200 Subject: [PATCH 509/509] Simplify code --- resources/lib/kodimonitor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index c6d0efda..fcb29d19 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -431,14 +431,7 @@ class SpecialMonitor(Thread): if getInfoLabel('Control.GetLabel(1002)') in strings: # Remember that the item IS indeed resumable control = int(Window(10106).getFocusId()) - if control == 1002: - # Start from beginning - state.RESUME_PLAYBACK = False - elif control == 1001: - state.RESUME_PLAYBACK = True - else: - # User chose something else from the context menu - state.RESUME_PLAYBACK = False + state.RESUME_PLAYBACK = True if control == 1001 else False else: # Different context menu is displayed state.RESUME_PLAYBACK = False