Merge pull request #1005 from croneter/beta-version

Bump master
This commit is contained in:
croneter 2019-10-03 18:58:07 +02:00 committed by GitHub
commit 3208d1d71d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1256 additions and 576 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-2.9.5-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.9.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![stable version](https://img.shields.io/badge/stable_version-2.9.9-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.9.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)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.5" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.9" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" />
@ -83,7 +83,30 @@
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
<news>version 2.9.5:
<news>version 2.9.9:
- Versions 2.9.6 - 2.9.8 for everyone
version 2.9.8 (beta only):
- Fix Play Error in scenarios (older PMS version?) where posting playqueues using an uri `server://` is not possible and `library://` is necessary
- Fix rare AttributeError on PKC startup when modifying advancedsettings.xml
- Update translations
version 2.9.7 (beta only):
- Correctly escape URLs for Direct Paths
- Update settings to inform user that reboot is necessary
- Optimize code
- Don't migrate PKC settings if we're dealing with a clean new PKC installation
- Force-scan every single item in the library - seems like we could lose some recently added items otherwise when updating PKC
version 2.9.6 (beta only):
- Rework logic for using direct paths, direct play, direct streaming and transcoding, using the PMS StreamingBrain: Let PMS StreamingBrain decide on whether we need to force-transcode, New setting to choose "Direct Streaming", Allow for 4k transcoding and direct streaming, New setting to force transcode only 4K and above
- Fix PKC background sync synching items to Kodi even though entire section should not be synched
- Force a full sync of all items after choosing a new PMS, changing a PMS' address and changing which Plex libraries to sync
- Only enforce advancedsettings.xml 'cleanonupdate' to be false for PKC add-on paths
- Never give up trying to connect to the PMS or Alexa using websockets
- Fix resume when force-transcoding
version 2.9.5:
- Version 2.9.4 for everyone
version 2.9.4 (beta only):

View file

@ -1,3 +1,26 @@
version 2.9.9:
- Versions 2.9.6 - 2.9.8 for everyone
version 2.9.8 (beta only):
- Fix Play Error in scenarios (older PMS version?) where posting playqueues using an uri `server://` is not possible and `library://` is necessary
- Fix rare AttributeError on PKC startup when modifying advancedsettings.xml
- Update translations
version 2.9.7 (beta only):
- Correctly escape URLs for Direct Paths
- Update settings to inform user that reboot is necessary
- Optimize code
- Don't migrate PKC settings if we're dealing with a clean new PKC installation
- Force-scan every single item in the library - seems like we could lose some recently added items otherwise when updating PKC
version 2.9.6 (beta only):
- Rework logic for using direct paths, direct play, direct streaming and transcoding, using the PMS StreamingBrain: Let PMS StreamingBrain decide on whether we need to force-transcode, New setting to choose "Direct Streaming", Allow for 4k transcoding and direct streaming, New setting to force transcode only 4K and above
- Fix PKC background sync synching items to Kodi even though entire section should not be synched
- Force a full sync of all items after choosing a new PMS, changing a PMS' address and changing which Plex libraries to sync
- Only enforce advancedsettings.xml 'cleanonupdate' to be false for PKC add-on paths
- Never give up trying to connect to the PMS or Alexa using websockets
- Fix resume when force-transcoding
version 2.9.5:
- Version 2.9.4 for everyone

View file

@ -698,6 +698,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server je online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1040,8 +1051,9 @@ msgstr "Vyhledávám Plex servery"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Použito při synchronizaci a při pokusu o přímé přehrávání"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -698,6 +698,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Serveren er online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1044,8 +1055,9 @@ msgstr "Søg efter Plex Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Brugt af Sync og når du forsøger at direkte spille"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -702,6 +702,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server ist online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS muss transkodieren"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS muss Direct Streamen"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1055,9 +1066,11 @@ msgstr "Suche Plex Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Verwendet für Synchronisierung sowie beim Versuch, Direct Play zu nutzen"
"Verwendet für Synchronisierung und Direct Paths. Bei Änderungen Kodi neu "
"starten!"
# PKC Settings, category name
msgctxt "#39057"

View file

@ -668,6 +668,16 @@ msgctxt "#33003"
msgid "Server is online"
msgstr ""
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -963,7 +973,7 @@ msgstr ""
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgid "Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name

View file

@ -706,6 +706,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1057,8 +1068,9 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -707,6 +707,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1058,8 +1069,9 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -706,6 +706,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1057,8 +1068,9 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -709,6 +709,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Le serveur est en ligne"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1065,8 +1076,9 @@ msgstr "Recherche d'un serveur Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Used by Sync and when attempting to Direct Play"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -713,6 +713,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Le serveur est en ligne"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1069,8 +1080,9 @@ msgstr "Recherche d'un serveur Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Used by Sync and when attempting to Direct Play"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -706,6 +706,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "A szerver elérhető"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1056,8 +1067,9 @@ msgstr "Plex szerver keresése"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Szinkronizáció és közvetlen lejátszás esetén használt"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -706,6 +706,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Il server è online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1059,10 +1070,9 @@ msgstr "Ricerca del server Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Utilizzato dalla sincronizzazione e quando si utilizza la riproduzione "
"diretta"
# PKC Settings, category name
msgctxt "#39057"

View file

@ -702,6 +702,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Serveris prisijungęs"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1054,8 +1065,9 @@ msgstr "Ieškomas „Plex“ serveris"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Naudojamas sinchronizuojant ir bandant tiesioginį atkūrimą"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -695,6 +695,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Serveris ir tiešsaistē"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1036,8 +1047,9 @@ msgstr "Meklē Plex Serveri"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Izmanto Sinhronizējot un tad, kad mēģina Tiešo Atskaņošanu"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -703,6 +703,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server is online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1048,8 +1059,9 @@ msgstr "Plex Server zoeken"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Gebruikt door Sync en bij Direct Play"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -700,6 +700,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server er tilgjengelig"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1041,8 +1052,9 @@ msgstr "Søker etter Plex Media Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Brukes av Sync og ved direkte avspilling"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -690,6 +690,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está on-line"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1036,8 +1047,9 @@ msgstr "A procurar o(s) servidor(es) Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Usado pela sincronização e quando tentando Reprodução Direta"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -693,6 +693,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está on-line"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1039,8 +1050,9 @@ msgstr "A procurar o(s) servidor(es) Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Usado pela sincronização e quando tentando Reprodução Direta"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -707,6 +707,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Сервер в сети"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1051,8 +1062,9 @@ msgstr "Поиск сервера Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Используется при синхронизации и прямом воспроизведении"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -701,6 +701,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server är online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1042,8 +1053,9 @@ msgstr "Letar efter Plex Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Används av synkronisering och vid försök att direktspela"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -691,6 +691,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Сервер онлайн"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1036,8 +1047,9 @@ msgstr "Пошук PMS"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Використовувати синхронізацією та при спробі прямого відтворення"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -685,6 +685,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "服务器在线"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1006,8 +1017,9 @@ msgstr "正搜索Plex服务器"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "用于同步和何时尝试直接播放"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -683,6 +683,17 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "伺服器已上線"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1002,8 +1013,9 @@ msgstr "正在搜索Plex伺服器"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "通過同步和嘗試使用直接播放"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
# PKC Settings, category name
msgctxt "#39057"

View file

@ -75,8 +75,24 @@ class Sync(object):
# Could we access the paths?
self.path_verified = False
# List of Section() items representing Plex library sections
self._sections = []
# List of section_ids we're synching to Kodi - will be automatically
# re-built if sections are set a-new
self.section_ids = set()
self.load()
@property
def sections(self):
return self._sections
@sections.setter
def sections(self, sections):
self._sections = sections
# Sets are faster when using "in" test than lists
self.section_ids = set([x.section_id for x in sections if x.sync_to_kodi])
def load(self):
self.direct_paths = utils.settings('useDirectPaths') == '1'
self.enable_music = utils.settings('enableMusic') == 'true'

View file

@ -509,9 +509,13 @@ class InitialSetup(object):
# (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')
cleanonupdate = xml.get_setting(
['videolibrary', 'cleanonupdate']) == 'true'
if utils.settings('useDirectPaths') != '1':
# Disable cleaning of library - not compatible with PKC
# Only do this for add-on paths
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'],
@ -522,6 +526,7 @@ class InitialSetup(object):
except utils.ParseError:
cache = None
reboot = False
cleanonupdate = 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)
@ -644,6 +649,11 @@ class InitialSetup(object):
utils.lang(39081), utils.lang(39082)) == 1:
LOG.debug("User opted to use direct paths.")
utils.settings('useDirectPaths', value="1")
if cleanonupdate:
# Re-enable cleanonupdate
with utils.XmlKodiSetting('advancedsettings.xml') as xml:
xml.set_setting(['videolibrary', 'cleanonupdate'],
value='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 utils.yesno_dialog(utils.lang(29999), utils.lang(39033)):

View file

@ -6,7 +6,7 @@ from ntpath import dirname
from ..plex_db import PlexDB, PLEXDB_LOCK
from ..kodi_db import KodiVideoDB, KODIDB_LOCK
from .. import utils, timing
from .. import utils, timing, app
LOG = getLogger('PLEX.itemtypes.common')
@ -136,3 +136,12 @@ class ItemBase(object):
duration,
view_count,
timing.plex_date_to_kodi(lastViewedAt))
@staticmethod
def sync_this_item(section_id):
"""
Returns False if we are NOT synching the corresponding Plex library
with section_id [int] to Kodi or if this sections has not yet been
encountered by PKC
"""
return section_id in app.SYNC.section_ids

View file

@ -20,11 +20,12 @@ class Movie(ItemBase):
Process single movie
"""
api = API(xml)
plex_id = api.plex_id
# Cannot parse XML, abort
if not plex_id:
LOG.error('Cannot parse XML data for movie: %s', xml.attrib)
if not self.sync_this_item(api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
api.library_section_id())
return
plex_id = api.plex_id
movie = self.plexdb.movie(plex_id)
if movie:
update_item = True

View file

@ -159,10 +159,12 @@ class Artist(MusicMixin, ItemBase):
Process a single artist
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error('Cannot process artist %s', xml.attrib)
if not self.sync_this_item(api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
api.library_section_id())
return
plex_id = api.plex_id
artist = self.plexdb.artist(plex_id)
if not artist:
update_item = False
@ -224,9 +226,6 @@ class Album(MusicMixin, ItemBase):
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error('Error processing album: %s', xml.attrib)
return
album = self.plexdb.album(plex_id)
if album:
update_item = True
@ -389,9 +388,6 @@ class Song(MusicMixin, ItemBase):
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error('Error processing song: %s', xml.attrib)
return
song = self.plexdb.song(plex_id)
if song:
update_item = True

View file

@ -148,10 +148,12 @@ class Show(TvShowMixin, ItemBase):
Process a single show
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
if not self.sync_this_item(api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
api.library_section_id())
return
plex_id = api.plex_id
show = self.plexdb.show(plex_id)
if not show:
update_item = False
@ -286,11 +288,12 @@ class Season(TvShowMixin, ItemBase):
Process a single season of a certain tv show
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error('Error getting plex_id for season, skipping: %s',
xml.attrib)
if not self.sync_this_item(api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
api.library_section_id())
return
plex_id = api.plex_id
season = self.plexdb.season(plex_id)
if not season:
update_item = False
@ -354,11 +357,12 @@ class Episode(TvShowMixin, ItemBase):
Process single episode
"""
api = API(xml)
plex_id = api.plex_id
if not plex_id:
LOG.error('Error getting plex_id for episode, skipping: %s',
xml.attrib)
if not self.sync_this_item(api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
api.library_section_id())
return
plex_id = api.plex_id
episode = self.plexdb.episode(plex_id)
if not episode:
update_item = False

View file

@ -255,7 +255,7 @@ class FullSync(common.fullsync_mixin):
"""
try:
for kind in kinds:
for section in (x for x in sections.SECTIONS
for section in (x for x in app.SYNC.sections
if x.section_type == kind[1]):
if self.isCanceled():
LOG.debug('Need to exit now')

View file

@ -15,7 +15,6 @@ from ..utils import etree
LOG = getLogger('PLEX.sync.sections')
BATCH_SIZE = 500
SECTIONS = []
# Need a way to interrupt our synching process
IS_CANCELED = None
@ -590,11 +589,10 @@ def sync_from_pms(parent_self, pick_libraries=False):
return _sync_from_pms(pick_libraries)
finally:
IS_CANCELED = None
LOG.info('Done synching sections from the PMS: %s', SECTIONS)
LOG.info('Done synching sections from the PMS: %s', app.SYNC.sections)
def _sync_from_pms(pick_libraries):
global SECTIONS
# Re-set value in order to make sure we got the lastest user input
app.SYNC.enable_music = utils.settings('enableMusic') == 'true'
xml = PF.get_plex_sections()
@ -649,7 +647,7 @@ def _sync_from_pms(pick_libraries):
# Counter that tells us how many sections we have - e.g. for skins and
# listings
utils.window('Plex.nodes.total', str(len(sections)))
SECTIONS = sections
app.SYNC.sections = sections
return True

View file

@ -13,10 +13,14 @@ LOG = getLogger('PLEX.migration')
def check_migration():
LOG.info('Checking whether we need to migrate something')
last_migration = utils.settings('last_migrated_PKC_version')
if last_migration == v.ADDON_VERSION:
# Ensure later migration if user downgraded PKC!
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
if last_migration == '':
LOG.info('New, clean PKC installation - no migration necessary')
return
elif last_migration == v.ADDON_VERSION:
LOG.info('Already migrated to PKC version %s' % v.ADDON_VERSION)
# Ensure later migration if user downgraded PKC!
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
return
if not utils.compare_version(last_migration, '1.8.2'):
@ -62,4 +66,21 @@ def check_migration():
# Re-sync all playlists to Kodi
utils.wipe_synched_playlists()
if not utils.compare_version(last_migration, '2.9.7'):
LOG.info('Migrating to version 2.9.6')
# Allow for a new "Direct Stream" setting (number 2), so shift the
# last setting for "force transcoding"
current_playback_type = utils.cast(int, utils.settings('playType')) or 0
if current_playback_type == 2:
current_playback_type = 3
utils.settings('playType', value=str(current_playback_type))
if not utils.compare_version(last_migration, '2.9.8'):
LOG.info('Migrating to version 2.9.7')
# Force-scan every single item in the library - seems like we could
# loose some recently added items otherwise
# Caused by 65a921c3cc2068c4a34990d07289e2958f515156
from . import library_sync
library_sync.force_full_sync()
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)

View file

@ -19,7 +19,7 @@ from . import playlist_func as PL
from . import playqueue as PQ
from . import json_rpc as js
from . import transfer
from .playutils import PlayUtils
from .playback_decision import set_playurl
from . import variables as v
from . import app
@ -211,7 +211,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
# Release default.py
_ensure_resolve()
api = API(xml[0])
if app.SYNC.direct_paths and api.resume_point():
if api.resume_point() and (app.SYNC.direct_paths or
app.PLAYSTATE.context_menu_play):
# Since Kodi won't ask if user wants to resume playback -
# we need to ask ourselves
resume = resume_dialog(int(api.resume_point()))
@ -234,7 +235,10 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
playqueue.clear()
if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion
xml = PF.init_plex_playqueue(plex_id, plex_type, trailers=trailers)
xml = PF.init_plex_playqueue(plex_id,
plex_type,
xml.get('librarySectionUUID'),
trailers=trailers)
if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s', plex_id)
# "Play error"
@ -460,17 +464,15 @@ def _conclude_playback(playqueue, pos):
api = API(item.xml)
api.part = item.part or 0
listitem = api.listitem(listitem=transfer.PKCListItem)
playutils = PlayUtils(api, item)
playurl = playutils.getPlayUrl()
set_playurl(api, item)
else:
listitem = transfer.PKCListItem()
api = None
playurl = item.file
if not playurl:
if not item.file:
LOG.info('Did not get a playurl, aborting playback silently')
_ensure_resolve(abort=True)
_ensure_resolve()
return
listitem.setPath(playurl.encode('utf-8'))
listitem.setPath(item.file.encode('utf-8'))
if item.playmethod == 'DirectStream':
listitem.setSubtitles(api.cache_external_subs())
elif item.playmethod == 'Transcode':

View file

@ -0,0 +1,458 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from requests import exceptions
from .downloadutils import DownloadUtils as DU
from .plex_api import API
from . import plex_functions as PF, utils, app, variables as v
LOG = getLogger('PLEX.playback_decision')
# largest signed 32bit integer: 2147483
MAX_SIGNED_INT = int(2**31 - 1)
# PMS answer codes
DIRECT_PLAY_OK = 1000
CONVERSION_OK = 1001 # PMS can either direct stream or transcode
def set_playurl(api, item):
if api.mediastream_number() is None:
# E.g. user could choose between several media streams and cancelled
return
item.playmethod = int(utils.settings('playType'))
LOG.info('User chose playback method %s in PKC settings',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
_initial_best_playback_method(api, item)
LOG.info('PKC decided on playback method %s',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
if item.playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
# No need to ask the PMS whether we can play - we circumvent
# the PMS entirely
LOG.info('The playurl for %s is: %s',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod], item.file)
return
LOG.info('Lets ask the PMS next')
try:
_pms_playback_decision(api, item)
except (exceptions.RequestException, AttributeError, IndexError, SystemExit) as err:
LOG.warn('Could not find suitable settings for playback, aborting')
LOG.warn('Error received: %s', err)
item.playmethod = None
item.file = None
else:
item.file = api.transcode_video_path(item.playmethod,
quality=item.quality)
LOG.info('The playurl for %s is: %s',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod], item.file)
def _initial_best_playback_method(api, item):
"""
Sets the highest available playback method without talking to the PMS
Also sets self.path for a direct path, if available and accessible
"""
item.file = api.file_path()
item.file = api.validate_playurl(item.file, api.plex_type, force_check=True)
# Check whether we have a strm file that we need to throw at Kodi 1:1
if item.file is not None and item.file.endswith('.strm'):
# Use direct path in any case, regardless of user setting
LOG.debug('.strm file detected')
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
elif _must_transcode(api, item):
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_PLAY,
v.PLAYBACK_METHOD_DIRECT_STREAM):
pass
elif item.file is None:
# E.g. direct path was not possible to access
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
else:
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
def _pms_playback_decision(api, item):
"""
We CANNOT distinguish direct playing from direct streaming from the PMS'
answer
"""
ask_for_user_quality_settings = False
if item.playmethod <= 2:
LOG.info('Asking PMS with maximal quality settings')
item.quality = _max_quality()
decision_api = _ask_pms(api, item)
if decision_api.decision_code() > CONVERSION_OK:
ask_for_user_quality_settings = True
else:
ask_for_user_quality_settings = True
if ask_for_user_quality_settings:
item.quality = _transcode_quality()
LOG.info('Asking PMS with user quality settings')
decision_api = _ask_pms(api, item)
# Process the PMS answer
if decision_api.decision_code() > CONVERSION_OK:
LOG.error('Neither DirectPlay, DirectStream nor transcoding possible')
error = '%s\n%s' % (decision_api.general_play_decision_text(),
decision_api.transcode_decision_text())
utils.messageDialog(heading=utils.lang(29999),
msg=error)
raise AttributeError('Neither DirectPlay, DirectStream nor transcoding possible')
if (item.playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY and
decision_api.decision_code() == DIRECT_PLAY_OK):
# All good
return
LOG.info('PMS video stream decision: %s, PMS audio stream decision: %s, '
'PMS subtitle stream decision: %s',
decision_api.video_decision(),
decision_api.audio_decision(),
decision_api.subtitle_decision())
# Only look at the video stream since that'll be most CPU-intensive for
# the PMS
video_direct_streaming = decision_api.video_decision() == 'copy'
if video_direct_streaming:
if item.playmethod < v.PLAYBACK_METHOD_DIRECT_STREAM:
LOG.warn('The PMS forces us to direct stream')
# "PMS enforced direct streaming"
utils.dialog('notification',
utils.lang(29999),
utils.lang(33005),
icon='{plex}')
item.playmethod = v.PLAYBACK_METHOD_DIRECT_STREAM
else:
if item.playmethod < v.PLAYBACK_METHOD_TRANSCODE:
LOG.warn('The PMS forces us to transcode')
# "PMS enforced transcoding"
utils.dialog('notification',
utils.lang(29999),
utils.lang(33004),
icon='{plex}')
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
def _ask_pms(api, item):
xml = PF.playback_decision(path=api.path_and_plex_id(),
media=api.mediastream,
part=api.part,
playmethod=item.playmethod,
video=api.plex_type in v.PLEX_VIDEOTYPES,
args=item.quality)
decision_api = API(xml)
LOG.info('PMS general decision %s: %s',
decision_api.general_play_decision_code(),
decision_api.general_play_decision_text())
LOG.info('PMS Direct Play decision %s: %s',
decision_api.direct_play_decision_code(),
decision_api.direct_play_decision_text())
LOG.info('PMS MDE decision %s: %s',
decision_api.mde_play_decision_code(),
decision_api.mde_play_decision_text())
LOG.info('PMS transcoding decision %s: %s',
decision_api.transcode_decision_code(),
decision_api.transcode_decision_text())
return decision_api
def _must_transcode(api, item):
"""
Returns True if we need to transcode because
- codec is in h265
- 10bit video codec
- HEVC codec
- 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'
"""
if api.plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
LOG.info('Plex clip or music track, not transcoding')
return False
if item.playmethod == v.PLAYBACK_METHOD_TRANSCODE:
return True
videoCodec = api.video_codec()
LOG.debug("videoCodec received from the PMS: %s", videoCodec)
if item.force_transcode is True:
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.')
return False
if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
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.')
return False
if bitrate > _get_max_bitrate():
LOG.info('Video bitrate of %s is higher than the maximal video'
'bitrate of %s that the user chose. Transcoding',
bitrate, _get_max_bitrate())
return True
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
if videoCodec['resolution'] == '4k':
resolution = 2160
else:
LOG.info('No video resolution from PMS, not transcoding.')
return False
if 'h265' in codec or 'hevc' in codec:
if resolution >= _getH265():
LOG.info('Option to transcode h265/HEVC enabled. Resolution '
'of the media: %s, transcoding limit resolution: %s',
resolution, _getH265())
return True
return False
def _transcode_quality():
return {
'maxVideoBitrate': get_bitrate(),
'videoResolution': get_resolution(),
'videoQuality': 100,
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
}
def _max_quality():
return {
'maxVideoBitrate': MAX_SIGNED_INT,
'videoResolution': '3840x2160', # 4K
'videoQuality': 100,
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
}
def get_bitrate():
"""
Get the desired transcoding bitrate from the settings
"""
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': 35000,
'12': 50000
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, MAX_SIGNED_INT)
def get_resolution():
"""
Get the desired transcoding resolutions from the settings
"""
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '420x420',
'1': '576x320',
'2': '720x480',
'3': '1024x768',
'4': '1280x720',
'5': '1280x720',
'6': '1920x1080',
'7': '1920x1080',
'8': '1920x1080',
'9': '1920x1080',
'10': '1920x1080',
'11': '3840x2160',
'12': '3840x2160'
}
return res[chosen]
def _get_max_bitrate():
max_bitrate = utils.settings('maxVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': MAX_SIGNED_INT # deactivated
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(max_bitrate, MAX_SIGNED_INT)
def _getH265():
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
OR 2147483 (MAX_SIGNED_INT, int) if user chose not to transcode
"""
H265 = {
'0': MAX_SIGNED_INT,
'1': 480,
'2': 720,
'3': 1080,
'4': 2160
}
return H265[utils.settings('transcodeH265')]
def audio_subtitle_prefs(api, listitem):
"""
For transcoding only
Called at the very beginning of play; used to change audio and subtitle
stream by a PUT request to the PMS
"""
# Set media and part where we're at
if (api.mediastream is None and
api.mediastream_number() is None):
return
try:
mediastreams = api.plex_media_streams()
except (TypeError, IndexError):
LOG.error('Could not get media %s, part %s',
api.mediastream, api.part)
return
part_id = mediastreams.attrib['id']
audio_streams_list = []
audio_streams = []
subtitle_streams_list = []
# No subtitles as an option
subtitle_streams = [utils.lang(39706)]
downloadable_streams = []
download_subs = []
# selectAudioIndex = ""
select_subs_index = ""
audio_numb = 0
# Remember 'no subtitles'
sub_num = 1
default_sub = None
for stream in mediastreams:
# Since Plex returns all possible tracks together, have to sort
# them.
index = stream.attrib.get('id')
typus = stream.attrib.get('streamType')
# Audio
if typus == "2":
codec = stream.attrib.get('codec')
channellayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'],
codec,
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb + 1,
utils.lang(39707), # unknown
codec,
channellayout)
audio_streams_list.append(index)
audio_streams.append(utils.try_encode(track))
audio_numb += 1
# Subtitles
elif typus == "3":
try:
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
except KeyError:
track = "%s %s (%s)" % (sub_num + 1,
utils.lang(39707), # unknown
stream.attrib.get('codec'))
default = stream.attrib.get('default')
forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key')
if default:
track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced:
track = "%s - %s" % (track, utils.lang(39709)) # Forced
if downloadable:
# We do know the language - temporarily download
if 'language' in stream.attrib:
path = api.download_external_subtitles(
'{server}%s' % stream.attrib['key'],
"subtitle.%s.%s" % (stream.attrib['languageCode'],
stream.attrib['codec']))
# We don't know the language - no need to download
else:
path = api.attach_plex_token_to_url(
"%s%s" % (app.CONN.server,
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(utils.try_encode(path))
else:
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
if stream.attrib.get('selected') == '1' and downloadable:
# Only show subs without asking user if they can be
# turned off
default_sub = index
subtitle_streams_list.append(index)
subtitle_streams.append(utils.try_encode(track))
sub_num += 1
if audio_numb > 1:
resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp > -1:
# User selected some audio track
args = {
'audioStreamID': audio_streams_list[resp],
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
if sub_num == 1:
# No subtitles
return
select_subs_index = None
if (utils.settings('pickPlexSubtitles') == 'true' and
default_sub is not None):
LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub
else:
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp > 0:
select_subs_index = subtitle_streams_list[resp - 1]
else:
# User selected no subtitles or backed out of dialog
select_subs_index = ''
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:
select_subs_index = ''
# Now prep the PMS for our choice
args = {
'subtitleStreamID': select_subs_index,
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)

View file

@ -158,7 +158,7 @@ class PlaylistItem(object):
uri = None [str] PMS path to item; will be auto-set with plex_id
guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
playmethod = None [str] either 'DirectPath', '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
@ -177,6 +177,8 @@ class PlaylistItem(object):
self.playmethod = None
self.playcount = None
self.offset = None
# Transcoding quality, if needed
self.quality = None
# If Plex video consists of several parts; part number
self.part = 0
self.force_transcode = False

View file

@ -1,372 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .downloadutils import DownloadUtils as DU
from . import utils, app
from . import variables as v
###############################################################################
LOG = getLogger('PLEX.playutils')
###############################################################################
class PlayUtils():
def __init__(self, api, playqueue_item):
"""
init with api (PlexAPI wrapper of the PMS xml element) and
playqueue_item (PlaylistItem())
"""
self.api = api
self.item = playqueue_item
def getPlayUrl(self):
"""
Returns the playurl [unicode] for the part or returns None.
(movie might consist of several files)
"""
if self.api.mediastream_number() is None:
return
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.transcode_video_path('DirectStream')
self.item.playmethod = 'DirectStream'
else:
LOG.info("File is transcoding.")
playurl = self.api.transcode_video_path(
'Transcode',
quality={
'maxVideoBitrate': self.get_bitrate(),
'videoResolution': self.get_resolution(),
'videoQuality': '100',
'mediaBufferSize': int(
utils.settings('kodi_video_cache')) / 1024,
})
self.item.playmethod = 'Transcode'
LOG.info("The playurl is: %s", playurl)
self.item.file = playurl
return playurl
def isDirectPlay(self):
"""
Returns the path/playurl if we can direct play, None otherwise
"""
# True for e.g. plex.tv watch later
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.file_path()
if path is not None and path.endswith('.strm'):
LOG.info('.strm file detected')
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'
if utils.settings('playType') != "0":
# User forcing to play via HTTP
LOG.info("User chose to not direct play")
return
if self.mustTranscode():
return
return self.api.validate_playurl(path,
self.api.plex_type,
force_check=True)
def mustTranscode(self):
"""
Returns True if we need to transcode because
- codec is in h265
- 10bit video codec
- HEVC codec
- 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'
"""
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.video_codec()
LOG.info("videoCodec: %s", videoCodec)
if self.item.force_transcode is True:
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.')
return False
if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
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.')
return False
if bitrate > self.get_max_bitrate():
LOG.info('Video bitrate of %s is higher than the maximal video'
'bitrate of %s that the user chose. Transcoding',
bitrate, self.get_max_bitrate())
return True
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
if videoCodec['resolution'] == '4k':
resolution = 2160
else:
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 '
'of the media: %s, transcoding limit resolution: %s',
resolution, self.getH265())
return True
return False
def isDirectStream(self):
# Never transcode Music
if self.api.plex_type == 'track':
return True
# set to 'Transcode=2'
if utils.settings('playType') == "2":
# User forcing to play via HTTP
LOG.info("User chose to transcode")
return False
if self.mustTranscode():
return False
return True
@staticmethod
def get_max_bitrate():
# get the addon video quality
videoQuality = utils.settings('maxVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': 99999999 # deactivated
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
@staticmethod
def getH265():
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
OR 9999999 (int) if user chose not to transcode
"""
H265 = {
'0': 99999999,
'1': 480,
'2': 720,
'3': 1080
}
return H265[utils.settings('transcodeH265')]
@staticmethod
def get_bitrate():
"""
Get the desired transcoding bitrate from the settings
"""
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
@staticmethod
def get_resolution():
"""
Get the desired transcoding resolutions from the settings
"""
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '420x420',
'1': '576x320',
'2': '720x480',
'3': '1024x768',
'4': '1280x720',
'5': '1280x720',
'6': '1920x1080',
'7': '1920x1080',
'8': '1920x1080',
'9': '1920x1080',
'10': '1920x1080',
}
return res[chosen]
def audio_subtitle_prefs(self, listitem):
"""
For transcoding only
Called at the very beginning of play; used to change audio and subtitle
stream by a PUT request to the PMS
"""
# Set media and part where we're at
if (self.api.mediastream is None and
self.api.mediastream_number() is None):
return
try:
mediastreams = self.api.plex_media_streams()
except (TypeError, IndexError):
LOG.error('Could not get media %s, part %s',
self.api.mediastream, self.api.part)
return
part_id = mediastreams.attrib['id']
audio_streams_list = []
audio_streams = []
subtitle_streams_list = []
# No subtitles as an option
subtitle_streams = [utils.lang(39706)]
downloadable_streams = []
download_subs = []
# selectAudioIndex = ""
select_subs_index = ""
audio_numb = 0
# Remember 'no subtitles'
sub_num = 1
default_sub = None
for stream in mediastreams:
# Since Plex returns all possible tracks together, have to sort
# them.
index = stream.attrib.get('id')
typus = stream.attrib.get('streamType')
# Audio
if typus == "2":
codec = stream.attrib.get('codec')
channellayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'],
codec,
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb + 1,
utils.lang(39707), # unknown
codec,
channellayout)
audio_streams_list.append(index)
audio_streams.append(utils.try_encode(track))
audio_numb += 1
# Subtitles
elif typus == "3":
try:
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
except KeyError:
track = "%s %s (%s)" % (sub_num + 1,
utils.lang(39707), # unknown
stream.attrib.get('codec'))
default = stream.attrib.get('default')
forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key')
if default:
track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced:
track = "%s - %s" % (track, utils.lang(39709)) # Forced
if downloadable:
# We do know the language - temporarily download
if 'language' in stream.attrib:
path = self.api.download_external_subtitles(
'{server}%s' % stream.attrib['key'],
"subtitle.%s.%s" % (stream.attrib['languageCode'],
stream.attrib['codec']))
# We don't know the language - no need to download
else:
path = self.api.attach_plex_token_to_url(
"%s%s" % (app.CONN.server,
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(utils.try_encode(path))
else:
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
if stream.attrib.get('selected') == '1' and downloadable:
# Only show subs without asking user if they can be
# turned off
default_sub = index
subtitle_streams_list.append(index)
subtitle_streams.append(utils.try_encode(track))
sub_num += 1
if audio_numb > 1:
resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp > -1:
# User selected some audio track
args = {
'audioStreamID': audio_streams_list[resp],
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
if sub_num == 1:
# No subtitles
return
select_subs_index = None
if (utils.settings('pickPlexSubtitles') == 'true' and
default_sub is not None):
LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub
else:
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp > 0:
select_subs_index = subtitle_streams_list[resp - 1]
else:
# User selected no subtitles or backed out of dialog
select_subs_index = ''
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:
select_subs_index = ''
# Now prep the PMS for our choice
args = {
'subtitleStreamID': select_subs_index,
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)

View file

@ -10,11 +10,12 @@ from .artwork import Artwork
from .file import File
from .media import Media
from .user import User
from .playback import Playback
from ..plex_db import PlexDB
class API(Base, Artwork, File, Media, User):
class API(Base, Artwork, File, Media, User, Playback):
pass

View file

@ -6,12 +6,13 @@ from logging import getLogger
from ..utils import cast
from ..downloadutils import DownloadUtils as DU
from .. import utils, variables as v, app, path_ops, clientinfo
from .. import plex_functions as PF
LOG = getLogger('PLEX.api')
class Media(object):
def should_stream(self):
def optimized_for_streaming(self):
"""
Returns True if the item's 'optimizedForStreaming' is set, False other-
wise
@ -209,7 +210,9 @@ class Media(object):
Transcode Video support; returns the URL to get a media started
Input:
action 'DirectStream' or 'Transcode'
action 'DirectPlay'
'DirectStream'
'Transcode'
quality: {
'videoResolution': e.g. '1024x768',
@ -224,47 +227,23 @@ class Media(object):
"""
if self.mediastream is None and self.mediastream_number() is None:
return
quality = {} if quality is None else quality
xargs = clientinfo.getXArgsDeviceInfo()
# For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls
if action == "DirectStream":
headers = clientinfo.getXArgsDeviceInfo()
if action == v.PLAYBACK_METHOD_DIRECT_PLAY:
path = self.xml[self.mediastream][self.part].get('key')
url = app.CONN.server + path
# e.g. Trailers already feature an '?'!
return utils.extend_url(url, xargs)
# For Transcoding
headers = {
'X-Plex-Platform': 'Android',
'X-Plex-Platform-Version': '7.0',
'X-Plex-Product': 'Plex for Android',
'X-Plex-Version': '5.8.0.475'
}
return utils.extend_url(app.CONN.server + path, headers)
# Direct Streaming and Transcoding
arguments = PF.transcoding_arguments(path=self.path_and_plex_id(),
media=self.mediastream,
part=self.part,
playmethod=action,
args=quality)
headers.update(arguments)
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.xml.get('key')
transcode_path = app.CONN.server + \
'/video/:/transcode/universal/start.m3u8'
args = {
'audioBoost': utils.settings('audioBoost'),
'autoAdjustQuality': 0,
'directPlay': 0,
'directStream': 1,
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
'fastSeek': 1,
'path': path,
'mediaIndex': self.mediastream,
'partIndex': self.part,
'hasMDE': 1,
'location': 'lan',
'subtitleSize': utils.settings('subtitleSize')
}
LOG.debug("Setting transcode quality to: %s", quality)
xargs.update(headers)
xargs.update(args)
xargs.update(quality)
return utils.extend_url(transcode_path, xargs)
return utils.extend_url(transcode_path, headers)
def cache_external_subs(self):
"""
@ -353,13 +332,7 @@ class Media(object):
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
if app.SYNC.escape_path:
try:
protocol, hostname, args = path.split(':', 2)
except ValueError:
pass
else:
args = utils.quote(args)
path = '%s:%s:%s' % (protocol, hostname, args)
path = utils.escape_path(path)
if (app.SYNC.path_verified and not force_check) or omit_check:
return path

View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from ..utils import cast
class Playback(object):
def decision_code(self):
"""
Returns the general_play_decision_code or mde_play_decision_code if
not available. Returns None if something went wrong
"""
return self.general_play_decision_code() or self.mde_play_decision_code()
def general_play_decision_code(self):
"""
Returns the 'generalDecisionCode' as an int or None
Generally, the 1xxx codes constitute a a success decision, 2xxx a
general playback error, 3xxx a direct play error, and 4xxx a transcode
error.
General decisions can include:
1000: Direct play OK.
1001: Direct play not available; Conversion OK.
2000: Neither direct play nor conversion is available.
2001: Not enough bandwidth for any playback of this item.
2002: Number of allowed streams has been reached. Stop a playback or ask
admin for more permissions.
2003: File is unplayable.
2004: Streaming Session doesnt exist or timed out.
2005: Client stopped playback.
2006: Admin Terminated Playback.
"""
return cast(int, self.xml.get('generalDecisionCode'))
def general_play_decision_text(self):
"""
Returns the text associated with the general_play_decision_code() as
text in unicode or None
"""
return self.xml.get('generalDecisionText')
def mde_play_decision_code(self):
return cast(int, self.xml.get('mdeDecisionCode'))
def mde_play_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('mdeDecisionText')
def direct_play_decision_code(self):
return cast(int, self.xml.get('directPlayDecisionCode'))
def direct_play_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('directPlayDecisionText')
def transcode_decision_code(self):
return cast(int, self.xml.get('directPlayDecisionCode'))
def transcode_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('directPlayDecisionText')
def video_decision(self):
"""
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
an existing video stream into a new container. Returns "transcode" if
the video stream will be transcoded.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '1':
return stream.get('decision')
def audio_decision(self):
"""
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
an existing audio stream into a new container. Returns "transcode" if
the audio stream will be transcoded.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '2':
return stream.get('decision')
def subtitle_decision(self):
"""
Returns the PMS' decision on the subtitle stream.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '3':
return stream.get('decision')

View file

@ -820,10 +820,10 @@ def get_plex_sections():
return xml
def init_plex_playqueue(plex_id, plex_type, trailers=False):
def init_plex_playqueue(plex_id, plex_type, section_uuid, trailers=False):
"""
Returns raw API metadata XML dump for a playlist with e.g. trailers.
"""
"""
url = "{server}/playQueues"
args = {
'type': plex_type,
@ -839,8 +839,30 @@ def init_plex_playqueue(plex_id, plex_type, trailers=False):
try:
xml[0].tag
except (IndexError, TypeError, AttributeError):
LOG.error("Error retrieving metadata for %s", url)
return
LOG.warn('Need to initialize Plex playqueue the old fashioned way')
xml = init_plex_playqueue_old_fashioned(plex_id, section_uuid, url, args)
return xml
def init_plex_playqueue_old_fashioned(plex_id, section_uuid, url, args):
"""
In rare cases (old PMS version?), the PMS does not allow to add media using
an uri
server://<machineIdentifier>/com.plexapp.plugins.library/library...
We need to use
library://<librarySectionUUID>/item/...
This involves an extra step to grab the librarySectionUUID for plex_id
"""
args['uri'] = 'library://{0}/item/%2Flibrary%2Fmetadata%2F{1}'.format(
section_uuid, plex_id)
xml = DU().downloadUrl(utils.extend_url(url, args), action_type="POST")
try:
xml[0].tag
except (IndexError, TypeError, AttributeError):
LOG.error('Error initializing the playqueue the old fashioned way %s',
utils.extend_url(url, args))
xml = None
return xml
@ -1040,3 +1062,74 @@ def show_episodes(plex_id):
'skipRefresh': 1,
}
return DownloadChunks(utils.extend_url(url, arguments))
def transcoding_arguments(path, media, part, playmethod, args=None):
if playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY:
direct_play = 1
direct_stream = 1
elif playmethod == v.PLAYBACK_METHOD_DIRECT_STREAM:
direct_play = 0
direct_stream = 1
elif playmethod == v.PLAYBACK_METHOD_TRANSCODE:
direct_play = 0
direct_stream = 0
arguments = {
# e.g. '/library/metadata/831399'
'path': path,
# 1 if you want to directPlay, 0 if you want to transcode
'directPlay': direct_play,
# 1 if you want to play a stream copy of data into a new container. This
# is unlikely to come up but its possible if you are playing something
# with a lot of tracks, a direct stream can result in lower bandwidth
# when a direct play would be over the limit.
# Assume Kodi can always handle any stream thrown at it!
'directStream': direct_stream,
# Same for audio - assume Kodi can play any audio stream passed in!
'directStreamAudio': direct_stream,
# This tells the server that you definitively know that the client can
# direct play (when you have directPlay=1) the content in spite of what
# client profiles may say about what the client can play. When this is
# set and directPlay=1, the server just checks bandwidth restrictions
# and gives you a reservation if bandwidth restrictions are met
'hasMDE': direct_play,
# where # is an integer, 0 indexed. If you specify directPlay, this is
# required. -1 indicates let the server choose.
'mediaIndex': media,
# Similar to mediaIndex but indicates which part you want to direct
# play. Really only comes into play with multi-part files which are
# uncommon. -1 here means concatenate the parts together but that
# requires the transcoder.
'partIndex': part,
# all the rest
'audioBoost': utils.settings('audioBoost'),
'autoAdjustQuality': 1,
'protocol': 'hls', # seen in the wild: 'http', 'dash', 'http', 'hls'
'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
'fastSeek': 1,
# none, embedded, sidecar
# Essentially indicating what you want to do with subtitles and state
# you arent want it to burn them into the video (requires transcoding)
'subtitles': 'none',
# 'subtitleSize': utils.settings('subtitleSize')
'copyts': 1
}
if args:
arguments.update(args)
return arguments
def playback_decision(path, media, part, playmethod, video=True, args=None):
"""
Let's the PMS decide how we should playback this file
"""
arguments = transcoding_arguments(path, media, part, playmethod, args=args)
if video:
url = '{server}/video/:/transcode/universal/decision'
else:
url = '{server}/music/:/transcode/universal/decision'
LOG.debug('Asking the PMS if we can play this video with settings: %s',
arguments)
return DU().downloadUrl(utils.extend_url(url, arguments),
headerOptions=v.STREAMING_HEADERS,
reraise=True)

View file

@ -59,21 +59,14 @@ 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-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Device': v.DEVICE,
'X-Plex-Device-Name': v.DEVICENAME,
# 'X-Plex-Device-Screen-Resolution': '1916x1018,1920x1080',
'X-Plex-Model': v.MODEL,
'X-Plex-Platform': v.PLATFORM,
'X-Plex-Platform-Version': v.PLATFORM_VERSION,
'X-Plex-Product': v.ADDON_NAME,
'X-Plex-Version': v.ADDON_VERSION,
'hasMDE': '1',
# 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'],
}

View file

@ -5,7 +5,7 @@ import logging
import sys
import xbmc
from . import utils, clientinfo, timing
from . import utils, clientinfo
from . import initialsetup
from . import kodimonitor
from . import sync, library_sync
@ -51,11 +51,11 @@ class Service(object):
return
# 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("PKC Direct Paths: %s",
utils.settings('useDirectPaths') == '1')
LOG.info("Escape paths: %s", utils.settings('escapePath') == 'true')
LOG.info("Synching Plex artwork to Kodi: %s",
utils.settings('usePlexArtwork') == 'true')
LOG.info("Number of sync threads: %s",
@ -198,7 +198,8 @@ class Service(object):
app.ACCOUNT.set_unauthenticated()
self.server_has_been_online = False
self.welcome_msg = False
# Force a full sync
# Force a full sync of all items
library_sync.force_full_sync()
app.SYNC.run_lib_scan = 'full'
# Enable the main loop to continue
app.APP.suspend = False
@ -268,7 +269,8 @@ class Service(object):
app.ACCOUNT.set_unauthenticated()
self.server_has_been_online = False
self.welcome_msg = False
# Force a full sync
# Force a full sync of all items
library_sync.force_full_sync()
app.SYNC.run_lib_scan = 'full'
# Enable the main loop to continue
app.APP.suspend = False
@ -295,7 +297,8 @@ class Service(object):
# Get newest sections from the PMS
if not sections.sync_from_pms(self, pick_libraries=True):
return
# Force a full sync
# Force a full sync of all items
library_sync.force_full_sync()
app.SYNC.run_lib_scan = 'full'
finally:
app.APP.resume_threads()

View file

@ -137,7 +137,6 @@ class Sync(backgroundthread.KillableThread):
playlist_monitor = None
initial_sync_done = False
last_websocket_processing = 0
one_day_in_seconds = 60 * 60 * 24
# Link to Websocket queue
queue = app.APP.websocket_queue
@ -170,7 +169,6 @@ class Sync(backgroundthread.KillableThread):
return
if not install_sync_done:
# Very FIRST sync ever upon installation or reset of Kodi DB
last_time_sync = timing.unix_timestamp()
LOG.info('Initial start-up full sync starting')
xbmc.executebuiltin('InhibitIdleShutdown(true)')
# This call will block until scan is completed

View file

@ -54,6 +54,8 @@ REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
# Grab Plex id from an URL-encoded string
REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
SAFE_URL_CHARACTERS = "%/:=&?~#+!$,;'@()*[]".encode('utf-8')
def garbageCollect():
gc.collect(2)
@ -373,6 +375,21 @@ def urlparse(url, scheme='', allow_fragments=True):
return _urlparse.urlparse(url, scheme, allow_fragments)
def escape_path(path):
"""
Uses urllib.quote to escape to escape path [unicode]. See here for the
reasoning whether a character is safe or not and whether or not it should
be escaped:
https://bugs.python.org/issue918368
Letters, digits, and the characters '_.-' are never quoted. Choosing the
"wrong" characters for a password for USERNAME:PASSWORD@host.com will get
you in trouble (e.g. '@')
Returns the escaped path as unicode
"""
return urllib.quote(path.encode('utf-8'),
safe=SAFE_URL_CHARACTERS).decode('utf-8')
def quote(s, safe='/'):
"""
unicode-safe way to use urllib.quote(). Pass in either str or unicode
@ -380,7 +397,7 @@ def quote(s, safe='/'):
"""
if isinstance(s, unicode):
s = s.encode('utf-8')
s = urllib.quote(s, safe)
s = urllib.quote(s, safe.encode('utf-8'))
return s.decode('utf-8')
@ -391,7 +408,7 @@ def quote_plus(s, safe=''):
"""
if isinstance(s, unicode):
s = s.encode('utf-8')
s = urllib.quote_plus(s, safe)
s = urllib.quote_plus(s, safe.encode('utf-8'))
return s.decode('utf-8')
@ -921,8 +938,9 @@ class XmlKodiSetting(object):
if not append:
old = self.get_setting(node_list)
if (old is not None and
old.text.strip() == value and
old.attrib == attrib):
((old.text is not None and old.text.strip() == value) or
(old.text is None and value == '')) and
(old.attrib or {}) == attrib):
# Already set exactly these values
return old
LOG.debug('Adding etree to: %s, value: %s, attrib: %s, append: %s',

View file

@ -70,9 +70,6 @@ else:
DEVICE = "Unknown"
MODEL = platform.release() or 'Unknown'
# Plex' own platform for e.g. Plex Media Player
PLATFORM = 'Konvergo'
PLATFORM_VERSION = '2.26.0.947-1e21fa2b'
DEVICENAME = try_decode(_ADDON.getSetting('deviceName'))
if not DEVICENAME:
@ -138,6 +135,7 @@ EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
# Multiply Plex time by this factor to receive Kodi time
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
# Playlist stuff
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
@ -447,6 +445,58 @@ CONTENT_FROM_PLEX_TYPE = {
None: CONTENT_TYPE_FILE
}
# Plex profile for transcoding and direct streaming
# Uses the empty Generic.xml at Plex Media Server/Resources/Profiles for any
# Playback decisions
PLATFORM = 'Generic'
# Version seems to be irrelevant for the generic platform
PLATFORM_VERSION = '1.0.0'
# Overrides (replace=true) any existing entries in generic.xml
STREAMING_HEADERS = {
'X-Plex-Client-Profile-Extra':
# Would allow to DirectStream anything, but seems to be rather faulty
# 'add-transcode-target('
# 'type=videoProfile&'
# 'context=streaming&'
# 'protocol=hls&'
# 'container=mpegts&'
# 'videoCodec=h264,*&'
# 'audioCodec=aac,*&'
# 'subtitleCodec=ass,pgs,vobsub,srt,*&'
# 'replace=true)'
('add-transcode-target('
'type=videoProfile&'
'context=streaming&'
'protocol=hls&'
'container=mpegts&'
'videoCodec=h264,hevc,mpeg4,mpeg2video&'
'audioCodec=aac,flac,vorbis,opus,ac3,eac3,mp3,mp2,pcm&'
'subtitleCodec=ass,pgs,vobsub&'
'replace=true)'
'+add-direct-play-profile('
'type=videoProfile&'
'container=*&'
'videoCodec=*&'
'audioCodec=*&'
'subtitleCodec=*&'
'replace=true)')
}
PLAYBACK_METHOD_DIRECT_PATH = 0
PLAYBACK_METHOD_DIRECT_PLAY = 1
PLAYBACK_METHOD_DIRECT_STREAM = 2
PLAYBACK_METHOD_TRANSCODE = 3
EXPLICIT_PLAYBACK_METHOD = {
PLAYBACK_METHOD_DIRECT_PATH: 'DirectPath',
PLAYBACK_METHOD_DIRECT_PLAY: 'DirectPlay',
PLAYBACK_METHOD_DIRECT_STREAM: 'DirectStream',
PLAYBACK_METHOD_TRANSCODE: 'Transcode',
None: None
}
KODI_TO_PLEX_ARTWORK = {
'poster': 'thumb',
'banner': 'banner',

View file

@ -19,6 +19,7 @@ class WebSocket(backgroundthread.KillableThread):
def __init__(self):
self.ws = None
self.redirect_uri = None
self.sleeptime = 0
super(WebSocket, self).__init__()
def process(self, opcode, message):
@ -45,6 +46,16 @@ class WebSocket(backgroundthread.KillableThread):
def getUri(self):
raise NotImplementedError
def __sleep(self):
"""
Sleeps for 2^self.sleeptime where sleeping period will be doubled with
each unsuccessful connection attempt.
Will sleep at most 64 seconds
"""
app.APP.monitor.waitForAbort(2**self.sleeptime)
if self.sleeptime < 6:
self.sleeptime += 1
def run(self):
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
app.APP.register_thread(self)
@ -58,7 +69,6 @@ class WebSocket(backgroundthread.KillableThread):
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
def _run(self):
counter = 0
while not self.isCanceled():
# In the event the server goes offline
if self.isSuspended():
@ -68,8 +78,6 @@ class WebSocket(backgroundthread.KillableThread):
self.ws = None
if self.wait_while_suspended():
# Abort was requested while waiting. We should exit
LOG.info("##===---- %s Stopped ----===##",
self.__class__.__name__)
return
try:
self.process(*self.receive(self.ws))
@ -77,8 +85,8 @@ class WebSocket(backgroundthread.KillableThread):
# No worries if read timed out
pass
except websocket.WebSocketConnectionClosedException:
LOG.info("%s: connection closed, (re)connecting",
self.__class__.__name__)
LOG.debug("%s: connection closed, (re)connecting",
self.__class__.__name__)
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
@ -89,41 +97,25 @@ class WebSocket(backgroundthread.KillableThread):
enable_multithread=True)
except IOError:
# Server is probably offline
LOG.info("%s: Error connecting", self.__class__.__name__)
LOG.debug("%s: IOError connecting", self.__class__.__name__)
self.ws = None
counter += 1
if counter >= 10:
LOG.info('%s: Repeated IOError detected. Stopping now',
self.__class__.__name__)
break
app.APP.monitor.waitForAbort(1)
self.__sleep()
except websocket.WebSocketTimeoutException:
LOG.info("%s: Timeout while connecting, trying again",
self.__class__.__name__)
LOG.debug("%s: WebSocketTimeoutException", self.__class__.__name__)
self.ws = None
app.APP.monitor.waitForAbort(1)
self.__sleep()
except websocket.WebsocketRedirect as e:
LOG.info('301 redirect detected')
self.redirect_uri = e.headers.get('location', e.headers.get('Location'))
LOG.debug('301 redirect detected: %s', e)
self.redirect_uri = e.headers.get('location',
e.headers.get('Location'))
if self.redirect_uri:
self.redirect_uri.decode('utf-8')
counter += 1
if counter >= 10:
LOG.info('%s: Repeated WebsocketRedirect detected. Stopping now',
self.__class__.__name__)
break
except websocket.WebSocketException as e:
LOG.info('%s: WebSocketException: %s',
self.__class__.__name__, e)
if ('Handshake Status 401' in e.args or
'Handshake Status 403' in e.args):
counter += 1
if counter >= 5:
LOG.info('%s: Error in handshake detected. '
'Stopping now', self.__class__.__name__)
break
self.redirect_uri = self.redirect_uri.decode('utf-8')
self.ws = None
app.APP.monitor.waitForAbort(1)
self.__sleep()
except websocket.WebSocketException as e:
LOG.debug('%s: WebSocketException: %s', self.__class__.__name__, e)
self.ws = None
self.__sleep()
except Exception as e:
LOG.error('%s: Unknown exception encountered when '
'connecting: %s', self.__class__.__name__, e)
@ -131,9 +123,9 @@ class WebSocket(backgroundthread.KillableThread):
LOG.error("%s: Traceback:\n%s",
self.__class__.__name__, traceback.format_exc())
self.ws = None
app.APP.monitor.waitForAbort(1)
self.__sleep()
else:
counter = 0
self.sleeptime = 0
except Exception as e:
LOG.error("%s: Unknown exception encountered: %s",
self.__class__.__name__, e)

View file

@ -89,7 +89,7 @@
</category>
<category label="39057"><!-- Customize Paths -->
<setting type="lsep" label="39056" /><!-- Used by Sync and to attempt to direct play -->
<setting type="lsep" label="39056" /><!-- Used by sync and when attempting Direct Paths. Restart Kodi on changes! -->
<setting id="replaceSMB" type="bool" label="39034" default="true" /> <!-- replace all Plex paths with SMB paths -->
<setting id="remapSMB" type="bool" label="39035" default="false" /> <!-- replace Plex paths /volume1/media or \\myserver\media with a custom SMB path smb://NAS/mystuff -->
<setting id="remapSMBmovieOrg" type="text" label="39037" default="" visible="eq(-1,true)" subsetting="true" /> <!-- Original Plex MOVIE path to replace -->
@ -111,10 +111,10 @@
<setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" />
<setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" visible="false"/>
<setting type="sep" />
<setting id="playType" type="enum" label="30002" values="Direct Play (default)|Direct Stream|Force Transcode" default="0" />
<setting id="transcoderVideoQualities" type="enum" label="30160" values="420x420, 320kbps|576x320, 720kbps|720x480, 1.5Mbps|1024x768, 2Mbps|1280x720, 3Mbps|1280x720, 4Mbps|1920x1080, 8Mbps|1920x1080, 10Mbps|1920x1080, 12Mbps|1920x1080, 20Mbps|1920x1080, 40Mbps" default="10" /><!-- Video Quality if Transcoding necessary -->
<setting id="playType" type="enum" label="30002" values="Try Direct Path|Direct Play|Direct Stream|Force Transcode" default="0" />
<setting id="transcoderVideoQualities" type="enum" label="30160" values="420x420, 320kbps|576x320, 720kbps|720x480, 1.5Mbps|1024x768, 2Mbps|720p, 3Mbps|720p, 4Mbps|1080p, 8Mbps|1080p, 10Mbps|1080p, 12Mbps|1080p, 20Mbps|1080p, 40Mbps|4K, 35Mbps|4K, 50Mbps" default="10" /><!-- Video Quality if Transcoding necessary -->
<setting id="maxVideoQualities" type="enum" label="30143" values="320kbps|720kbps|1.5Mbps|2Mbps|3Mbps|4Mbps|8Mbps|10Mbps|12Mbps|20Mbps|40Mbps|deactivated" default="11" /><!-- Always transcode if video bitrate is above -->
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled (default)|480p (and higher)|720p (and higher)|1080p" /><!-- Force transcode h265/HEVC -->
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled (default)|480p (and higher)|720p (and higher)|1080p (and higher)|4K" /><!-- Force transcode h265/HEVC -->
<setting id="transcodeHi10P" type="bool" label="39063" default="false"/>
<setting id="audioBoost" type="slider" label="39001" default="0" range="0,10,100" option="int"/>
<setting id="subtitleSize" label="39002" type="slider" option="int" range="0,30,300" default="100" />