commit
3208d1d71d
49 changed files with 1256 additions and 576 deletions
|
@ -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)
|
||||
|
|
27
addon.xml
27
addon.xml
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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':
|
||||
|
|
458
resources/lib/playback_decision.py
Normal file
458
resources/lib/playback_decision.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
107
resources/lib/plex_api/playback.py
Normal file
107
resources/lib/plex_api/playback.py
Normal 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 doesn’t 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')
|
|
@ -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 it’s 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 aren’t 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)
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue