Merge branch 'hotfixes' into translations
This commit is contained in:
commit
fe952afa3e
55 changed files with 8105 additions and 8495 deletions
27
README.md
27
README.md
|
@ -1,5 +1,5 @@
|
||||||
[![stable version](https://img.shields.io/badge/stable_version-1.8.12-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
[![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||||
[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
[![beta version](https://img.shields.io/badge/beta_version-2.0.16-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||||
|
|
||||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
[![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)
|
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||||
|
@ -15,6 +15,11 @@ PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly cu
|
||||||
|
|
||||||
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
|
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
|
||||||
|
|
||||||
|
### UPDATE YOUR PKC REPO TO RECEIVE UPDATES!
|
||||||
|
|
||||||
|
Unfortunately, the PKC Kodi repository had to move because it stopped working (thanks https://bintray.com). If you installed PKC before December 15, 2017, you need to [**MANUALLY** update the repo once](https://github.com/croneter/PlexKodiConnect/wiki/Update-PKC-Repository).
|
||||||
|
|
||||||
|
|
||||||
### Please Help Translating
|
### Please Help Translating
|
||||||
|
|
||||||
Please help translate PlexKodiConnect into your language: [Transifex.com](https://www.transifex.com/croneter/pkc)
|
Please help translate PlexKodiConnect into your language: [Transifex.com](https://www.transifex.com/croneter/pkc)
|
||||||
|
@ -69,15 +74,16 @@ PKC synchronizes your media from your Plex server to the native Kodi database. H
|
||||||
+ Chinese Simplified, thanks @everdream
|
+ Chinese Simplified, thanks @everdream
|
||||||
+ Norwegian, thanks @mjorud
|
+ Norwegian, thanks @mjorud
|
||||||
+ Portuguese, thanks @goncalo532
|
+ Portuguese, thanks @goncalo532
|
||||||
|
+ Russian, thanks @UncleStark
|
||||||
+ [Please help translating](https://www.transifex.com/croneter/pkc)
|
+ [Please help translating](https://www.transifex.com/croneter/pkc)
|
||||||
|
|
||||||
### Download and Installation
|
### Download and Installation
|
||||||
|
|
||||||
Install PKC via the PlexKodiConnect Kodi repository below (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically.
|
Install PKC via the PlexKodiConnect Kodi repository download button just below (do NOT use the standard GitHub download!). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically.
|
||||||
|
|
||||||
| Stable version | Beta version |
|
| Stable version | Beta version |
|
||||||
|----------------|--------------|
|
|----------------|--------------|
|
||||||
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) |
|
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) |
|
||||||
|
|
||||||
### Additional Artwork
|
### Additional Artwork
|
||||||
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
||||||
|
@ -92,11 +98,20 @@ PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.or
|
||||||
* If **another plugin is not working** like it's supposed to, try to use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths-Explained)
|
* If **another plugin is not working** like it's supposed to, try to use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths-Explained)
|
||||||
|
|
||||||
### Donations
|
### Donations
|
||||||
I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal if you appreciate PKC.
|
I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal, Bitcoin or Ether if you appreciate PKC.
|
||||||
**Full disclaimer:** I will see your name and address on my PayPal account. Rest assured that I will not share this with anyone.
|
**Full disclaimer:** I will see your name and address if you use PayPal. Rest assured that I will not share this with anyone.
|
||||||
|
|
||||||
[![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB)
|
[![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB)
|
||||||
|
|
||||||
|
![ETH-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F)
|
||||||
|
**Ethereum address:
|
||||||
|
0x0f57D98E08e617292D8bC0B3448dd79BF4Cf8e7F**
|
||||||
|
|
||||||
|
![BTX-Donations](https://chart.googleapis.com/chart?chs=150x150&cht=qr&chld=L0&chl=3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT)
|
||||||
|
**Bitcoin address:
|
||||||
|
3BhwvUsqAGtAZodGUx4mTP7pTECjf1AejT**
|
||||||
|
|
||||||
|
|
||||||
### Request a New Feature
|
### Request a New Feature
|
||||||
|
|
||||||
[![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect)
|
[![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect)
|
||||||
|
|
151
addon.xml
151
addon.xml
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.8.14" provider-name="croneter">
|
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.0.16" provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
<import addon="script.module.requests" version="2.3.0" />
|
<import addon="script.module.requests" version="2.9.1" />
|
||||||
|
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.0" />
|
||||||
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.1" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||||
<provides>video audio image</provides>
|
<provides>video audio image</provides>
|
||||||
|
@ -13,7 +15,7 @@
|
||||||
<item>
|
<item>
|
||||||
<label>30401</label>
|
<label>30401</label>
|
||||||
<description>30416</description>
|
<description>30416</description>
|
||||||
<visible>[!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(plexid))] + !IsEmpty(Window(10000).Property(plex_context))</visible>
|
<visible>[!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(plexid))]</visible>
|
||||||
</item>
|
</item>
|
||||||
</extension>
|
</extension>
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
|
@ -59,7 +61,148 @@
|
||||||
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
|
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
|
||||||
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
|
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
|
||||||
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
|
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
|
||||||
<news>version 1.8.14 (beta only):
|
<news>version 2.0.16 (beta only):
|
||||||
|
- Do NOT delete playstates before getting new ones from the PMS
|
||||||
|
|
||||||
|
version 2.0.15 (beta only):
|
||||||
|
- Fix Plex Companion thinking video is playing again
|
||||||
|
- Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report
|
||||||
|
- Don't clean the Kodi file table
|
||||||
|
- Only remember which player has been active if we got a Plex id
|
||||||
|
- Hopefully fix ValueError for datetime.utcnow()
|
||||||
|
|
||||||
|
version 2.0.14 (beta only):
|
||||||
|
- Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing
|
||||||
|
- Play the selected element first, then add the Kodi playqueue to the Plex playqueue
|
||||||
|
- Ensure that playstate for ended (not stopped) video is recorded correctly
|
||||||
|
- Make sure that LOCK is released after adding one element
|
||||||
|
|
||||||
|
version 2.0.13 (beta only):
|
||||||
|
- Fix resume for On Deck and browse by folder
|
||||||
|
- Fix PKC sometimes telling wrong item being played
|
||||||
|
- Don't tell PMS last item is playing if non-Plex item is played
|
||||||
|
- Fix rare KeyError for playback including trailers
|
||||||
|
- Use an empty video file to "fail" playback
|
||||||
|
- Use identical add-on paths for On Deck and browsing folders
|
||||||
|
|
||||||
|
version 2.0.12 (beta only):
|
||||||
|
- Fix resume not working for some Kodi interface languages
|
||||||
|
- Fix widget navigating to entire TV show not working
|
||||||
|
- Fix library sync crash TypeError
|
||||||
|
- Revert "Revert "Fix for "In Progress" not appearing""
|
||||||
|
- Simplify error message
|
||||||
|
|
||||||
|
version 2.0.11 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Fix playback for add-on paths
|
||||||
|
- Fix artwork for episodes for add-on paths
|
||||||
|
- Revert "Fix for "In Progress" not appearing"
|
||||||
|
- Fix playback resuming potentially too often
|
||||||
|
|
||||||
|
version 2.0.10 (beta only):
|
||||||
|
- Fix wrong item being reported using direct paths
|
||||||
|
- Direct paths: correctly clean up after context menu play
|
||||||
|
- Always resume playback if playback initiated via context menu
|
||||||
|
- Do not play trailers for resumable movies using playback via PMS
|
||||||
|
- Fix for "In Progress" widget not appearing
|
||||||
|
- Fix correctly recording ended (not stopped!) video
|
||||||
|
- Don't record last played date if state unwatched
|
||||||
|
- Clean Kodi DB more thoroughly after playback start via PMS
|
||||||
|
|
||||||
|
version 2.0.9 (beta only):
|
||||||
|
- Fix AttributeError on playback start
|
||||||
|
|
||||||
|
version 2.0.8 (beta only):
|
||||||
|
- Fix videos not being correctly marked as played
|
||||||
|
- Improve playback startup resiliance
|
||||||
|
- Fix playerstates not being copied/reset correctly
|
||||||
|
- Fix tv shows not being correctly deleted
|
||||||
|
- Fix episode rating not being correct
|
||||||
|
- Make generally sure that we're correctly deleting videos from the Kodi DB
|
||||||
|
- Fix disabling of background sync (websockets)
|
||||||
|
|
||||||
|
version 2.0.7 (beta only):
|
||||||
|
- Fix another UnicodeDecodeError for playlists
|
||||||
|
- Hardcode plugin-calls instead of using urlencode
|
||||||
|
- Fix Kodi 18 log warnings by declaring all settings variables
|
||||||
|
|
||||||
|
version 2.0.6 (beta only):
|
||||||
|
- Addon paths playback was basically broken - hope it works again!
|
||||||
|
- Fixes to add-on paths playback startup
|
||||||
|
- Fix UnicodeDecodeError for playqueue logging
|
||||||
|
|
||||||
|
version 2.0.5 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Fix art and show info not showing for addon paths
|
||||||
|
- Fix episode information not working
|
||||||
|
- Big Kodi DB overhaul - ensure video metadata updates/deletes correctly
|
||||||
|
- Artwork code overhaul
|
||||||
|
- Greatly speed up switch of PMS
|
||||||
|
- And a lot of other stuff
|
||||||
|
|
||||||
|
version 2.0.4 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Many improvements to the Kodi database handling which should get rid of some weird bugs
|
||||||
|
- Many improvements to playback startup
|
||||||
|
- Fix info screen and actors not working
|
||||||
|
- Fix Companion displaying and selecting wrong subtitle
|
||||||
|
- Don't cache subtitles if direct playing
|
||||||
|
- Wipe all existing resume point, e.g. on user switch
|
||||||
|
- Don't mess with Kodi's screensaver settings
|
||||||
|
- Inhibit idle shutdown only during initial sync
|
||||||
|
- Fix KeyError for server discovery
|
||||||
|
- Increase Python requests dependency to version 2.9.1
|
||||||
|
- Re-introduce PlexKodiConnect dependency add-ons for movies and tv shows
|
||||||
|
- And a lot of other stuff
|
||||||
|
|
||||||
|
version 2.0.3 (beta only):
|
||||||
|
- Fix Alexa playback
|
||||||
|
- Fix Kodi boot loop
|
||||||
|
- Fix playback being reported to the wrong Plex user
|
||||||
|
- Fix GB/BBFC content ratings
|
||||||
|
- Fix KeyError when browsing On Deck
|
||||||
|
- Make sure that empty XML elements get deleted
|
||||||
|
- Code optimizations
|
||||||
|
|
||||||
|
version 2.0.2 (beta only):
|
||||||
|
- Fix playback reporting not starting up correctly
|
||||||
|
- Fix playback cleanup if PKC causes stop
|
||||||
|
- Always detect if user resumes playback
|
||||||
|
- Enable resume within a playqueue
|
||||||
|
- Compare playqueue items more reliably
|
||||||
|
|
||||||
|
version 2.0.1 (beta only):
|
||||||
|
- Fix empty On Deck for tv shows
|
||||||
|
- Fix trailers not playing
|
||||||
|
|
||||||
|
version 2.0.0 (beta only):
|
||||||
|
- HUGE code overhaul - Remember that you can go back to earlier version ;-)
|
||||||
|
- Completely rewritten Plex Companion
|
||||||
|
- Completely rewritten playback startup
|
||||||
|
- Tons of fixes, see the Github changelog for more details
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
|
||||||
|
version 1.8.18:
|
||||||
|
- Russian translation, thanks @UncleStark, @xom2000, @AlexFreit
|
||||||
|
- Fix Plex context menu not showing up
|
||||||
|
- Deal better with missing stream info (e.g. channels)
|
||||||
|
- Fix AttributeError if Plex key is missing
|
||||||
|
|
||||||
|
version 1.8.17:
|
||||||
|
- Hopefully fix stable repo
|
||||||
|
- Fix subtitles not working or showing up as Unknown
|
||||||
|
- Enable channels for Plex home users
|
||||||
|
- Remove obsolete PKC settings show contextmenu
|
||||||
|
|
||||||
|
version 1.8.16:
|
||||||
|
- Add premiere dates for movies, thanks @dazedcrazy
|
||||||
|
- Fix items not getting marked as fully watched
|
||||||
|
|
||||||
|
version 1.8.15:
|
||||||
|
- version 1.8.14 for everyone
|
||||||
|
- Update translations
|
||||||
|
|
||||||
|
version 1.8.14 (beta only):
|
||||||
- Greatly speed up displaying context menu
|
- Greatly speed up displaying context menu
|
||||||
- Fix IndexError e.g. for channels if stream info missing
|
- Fix IndexError e.g. for channels if stream info missing
|
||||||
- Sleep a bit before marking item as fully watched
|
- Sleep a bit before marking item as fully watched
|
||||||
|
|
141
changelog.txt
141
changelog.txt
|
@ -1,3 +1,144 @@
|
||||||
|
version 2.0.16 (beta only):
|
||||||
|
- Do NOT delete playstates before getting new ones from the PMS
|
||||||
|
|
||||||
|
version 2.0.15 (beta only):
|
||||||
|
- Fix Plex Companion thinking video is playing again
|
||||||
|
- Warn if "Play next video automatically" is enabled, cause it breaks PKC playback report
|
||||||
|
- Don't clean the Kodi file table
|
||||||
|
- Only remember which player has been active if we got a Plex id
|
||||||
|
- Hopefully fix ValueError for datetime.utcnow()
|
||||||
|
|
||||||
|
version 2.0.14 (beta only):
|
||||||
|
- Fix resetting PKC player state - should fix PKC telling the PMS that an old, just-played item is playing
|
||||||
|
- Play the selected element first, then add the Kodi playqueue to the Plex playqueue
|
||||||
|
- Ensure that playstate for ended (not stopped) video is recorded correctly
|
||||||
|
- Make sure that LOCK is released after adding one element
|
||||||
|
|
||||||
|
version 2.0.13 (beta only):
|
||||||
|
- Fix resume for On Deck and browse by folder
|
||||||
|
- Fix PKC sometimes telling wrong item being played
|
||||||
|
- Don't tell PMS last item is playing if non-Plex item is played
|
||||||
|
- Fix rare KeyError for playback including trailers
|
||||||
|
- Use an empty video file to "fail" playback
|
||||||
|
- Use identical add-on paths for On Deck and browsing folders
|
||||||
|
|
||||||
|
version 2.0.12 (beta only):
|
||||||
|
- Fix resume not working for some Kodi interface languages
|
||||||
|
- Fix widget navigating to entire TV show not working
|
||||||
|
- Fix library sync crash TypeError
|
||||||
|
- Revert "Revert "Fix for "In Progress" not appearing""
|
||||||
|
- Simplify error message
|
||||||
|
|
||||||
|
version 2.0.11 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Fix playback for add-on paths
|
||||||
|
- Fix artwork for episodes for add-on paths
|
||||||
|
- Revert "Fix for "In Progress" not appearing"
|
||||||
|
- Fix playback resuming potentially too often
|
||||||
|
|
||||||
|
version 2.0.10 (beta only):
|
||||||
|
- Fix wrong item being reported using direct paths
|
||||||
|
- Direct paths: correctly clean up after context menu play
|
||||||
|
- Always resume playback if playback initiated via context menu
|
||||||
|
- Do not play trailers for resumable movies using playback via PMS
|
||||||
|
- Fix for "In Progress" widget not appearing
|
||||||
|
- Fix correctly recording ended (not stopped!) video
|
||||||
|
- Don't record last played date if state unwatched
|
||||||
|
- Clean Kodi DB more thoroughly after playback start via PMS
|
||||||
|
|
||||||
|
version 2.0.9 (beta only):
|
||||||
|
- Fix AttributeError on playback start
|
||||||
|
|
||||||
|
version 2.0.8 (beta only):
|
||||||
|
- Fix videos not being correctly marked as played
|
||||||
|
- Improve playback startup resiliance
|
||||||
|
- Fix playerstates not being copied/reset correctly
|
||||||
|
- Fix tv shows not being correctly deleted
|
||||||
|
- Fix episode rating not being correct
|
||||||
|
- Make generally sure that we're correctly deleting videos from the Kodi DB
|
||||||
|
- Fix disabling of background sync (websockets)
|
||||||
|
|
||||||
|
version 2.0.7 (beta only):
|
||||||
|
- Fix another UnicodeDecodeError for playlists
|
||||||
|
- Hardcode plugin-calls instead of using urlencode
|
||||||
|
- Fix Kodi 18 log warnings by declaring all settings variables
|
||||||
|
|
||||||
|
version 2.0.6 (beta only):
|
||||||
|
- Addon paths playback was basically broken - hope it works again!
|
||||||
|
- Fixes to add-on paths playback startup
|
||||||
|
- Fix UnicodeDecodeError for playqueue logging
|
||||||
|
|
||||||
|
version 2.0.5 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Fix art and show info not showing for addon paths
|
||||||
|
- Fix episode information not working
|
||||||
|
- Big Kodi DB overhaul - ensure video metadata updates/deletes correctly
|
||||||
|
- Artwork code overhaul
|
||||||
|
- Greatly speed up switch of PMS
|
||||||
|
- And a lot of other stuff
|
||||||
|
|
||||||
|
version 2.0.4 (beta only):
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
- Many improvements to the Kodi database handling which should get rid of some weird bugs
|
||||||
|
- Many improvements to playback startup
|
||||||
|
- Fix info screen and actors not working
|
||||||
|
- Fix Companion displaying and selecting wrong subtitle
|
||||||
|
- Don't cache subtitles if direct playing
|
||||||
|
- Wipe all existing resume point, e.g. on user switch
|
||||||
|
- Don't mess with Kodi's screensaver settings
|
||||||
|
- Inhibit idle shutdown only during initial sync
|
||||||
|
- Fix KeyError for server discovery
|
||||||
|
- Increase Python requests dependency to version 2.9.1
|
||||||
|
- Re-introduce PlexKodiConnect dependency add-ons for movies and tv shows
|
||||||
|
- And a lot of other stuff
|
||||||
|
|
||||||
|
version 2.0.3 (beta only):
|
||||||
|
- Fix Alexa playback
|
||||||
|
- Fix Kodi boot loop
|
||||||
|
- Fix playback being reported to the wrong Plex user
|
||||||
|
- Fix GB/BBFC content ratings
|
||||||
|
- Fix KeyError when browsing On Deck
|
||||||
|
- Make sure that empty XML elements get deleted
|
||||||
|
- Code optimizations
|
||||||
|
|
||||||
|
version 2.0.2 (beta only):
|
||||||
|
- Fix playback reporting not starting up correctly
|
||||||
|
- Fix playback cleanup if PKC causes stop
|
||||||
|
- Always detect if user resumes playback
|
||||||
|
- Enable resume within a playqueue
|
||||||
|
- Compare playqueue items more reliably
|
||||||
|
|
||||||
|
version 2.0.1 (beta only):
|
||||||
|
- Fix empty On Deck for tv shows
|
||||||
|
- Fix trailers not playing
|
||||||
|
|
||||||
|
version 2.0.0 (beta only):
|
||||||
|
- HUGE code overhaul - Remember that you can go back to earlier version ;-)
|
||||||
|
- Completely rewritten Plex Companion
|
||||||
|
- Completely rewritten playback startup
|
||||||
|
- Tons of fixes, see the Github changelog for more details
|
||||||
|
- WARNING: You will need to reset the Kodi database!
|
||||||
|
|
||||||
|
version 1.8.18:
|
||||||
|
- Russian translation, thanks @UncleStark, @xom2000, @AlexFreit
|
||||||
|
- Fix Plex context menu not showing up
|
||||||
|
- Deal better with missing stream info (e.g. channels)
|
||||||
|
- Fix AttributeError if Plex key is missing
|
||||||
|
|
||||||
|
version 1.8.17:
|
||||||
|
- Hopefully fix stable repo
|
||||||
|
- Fix subtitles not working or showing up as Unknown
|
||||||
|
- Enable channels for Plex home users
|
||||||
|
- Remove obsolete PKC settings show contextmenu
|
||||||
|
|
||||||
|
version 1.8.16:
|
||||||
|
- Add premiere dates for movies, thanks @dazedcrazy
|
||||||
|
- Fix items not getting marked as fully watched
|
||||||
|
|
||||||
|
version 1.8.15:
|
||||||
|
- version 1.8.14 for everyone
|
||||||
|
- Update translations
|
||||||
|
|
||||||
version 1.8.14 (beta only):
|
version 1.8.14 (beta only):
|
||||||
- Greatly speed up displaying context menu
|
- Greatly speed up displaying context menu
|
||||||
- Fix IndexError e.g. for channels if stream info missing
|
- Fix IndexError e.g. for channels if stream info missing
|
||||||
|
|
|
@ -1,41 +1,48 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from os import path as os_path
|
from sys import listitem
|
||||||
from sys import path as sys_path
|
from urllib import urlencode
|
||||||
|
|
||||||
from xbmcaddon import Addon
|
from xbmc import getCondVisibility, sleep
|
||||||
from xbmc import translatePath, sleep, log, LOGERROR
|
|
||||||
from xbmcgui import Window
|
from xbmcgui import Window
|
||||||
|
|
||||||
_addon = Addon(id='plugin.video.plexkodiconnect')
|
|
||||||
try:
|
|
||||||
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
|
||||||
except TypeError:
|
|
||||||
_addon_path = _addon.getAddonInfo('path').decode()
|
|
||||||
try:
|
|
||||||
_base_resource = translatePath(os_path.join(
|
|
||||||
_addon_path,
|
|
||||||
'resources',
|
|
||||||
'lib')).decode('utf-8')
|
|
||||||
except TypeError:
|
|
||||||
_base_resource = translatePath(os_path.join(
|
|
||||||
_addon_path,
|
|
||||||
'resources',
|
|
||||||
'lib')).decode()
|
|
||||||
sys_path.append(_base_resource)
|
|
||||||
|
|
||||||
from pickler import unpickle_me, pickl_window
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
win = Window(10000)
|
def _get_kodi_type():
|
||||||
while win.getProperty('plex_command'):
|
kodi_type = listitem.getVideoInfoTag().getMediaType().decode('utf-8')
|
||||||
|
if not kodi_type:
|
||||||
|
if getCondVisibility('Container.Content(albums)'):
|
||||||
|
kodi_type = "album"
|
||||||
|
elif getCondVisibility('Container.Content(artists)'):
|
||||||
|
kodi_type = "artist"
|
||||||
|
elif getCondVisibility('Container.Content(songs)'):
|
||||||
|
kodi_type = "song"
|
||||||
|
elif getCondVisibility('Container.Content(pictures)'):
|
||||||
|
kodi_type = "picture"
|
||||||
|
return kodi_type
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Grabs kodi_id and kodi_type and sends a request to our main python instance
|
||||||
|
that context menu needs to be displayed
|
||||||
|
"""
|
||||||
|
window = Window(10000)
|
||||||
|
kodi_id = listitem.getVideoInfoTag().getDbId()
|
||||||
|
if kodi_id == -1:
|
||||||
|
# There is no getDbId() method for getMusicInfoTag
|
||||||
|
# YET TO BE IMPLEMENTED - lookup ID using path
|
||||||
|
kodi_id = listitem.getMusicInfoTag().getURL()
|
||||||
|
kodi_type = _get_kodi_type()
|
||||||
|
args = {
|
||||||
|
'kodi_id': kodi_id,
|
||||||
|
'kodi_type': kodi_type
|
||||||
|
}
|
||||||
|
while window.getProperty('plex_command'):
|
||||||
sleep(20)
|
sleep(20)
|
||||||
win.setProperty('plex_command', 'CONTEXT_menu')
|
window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args))
|
||||||
while not pickl_window('plex_result'):
|
|
||||||
sleep(50)
|
|
||||||
result = unpickle_me()
|
if __name__ == "__main__":
|
||||||
if result is None:
|
main()
|
||||||
log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR)
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ sys_path.append(_base_resource)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import entrypoint
|
import entrypoint
|
||||||
from utils import window, reset, passwordsXML, language as lang, dialog, \
|
from utils import window, reset, passwords_xml, language as lang, dialog, \
|
||||||
plex_command
|
plex_command
|
||||||
from pickler import unpickle_me, pickl_window
|
from pickler import unpickle_me, pickl_window
|
||||||
from PKC_listitem import convert_PKC_to_listitem
|
from PKC_listitem import convert_PKC_to_listitem
|
||||||
|
@ -115,7 +115,7 @@ class Main():
|
||||||
entrypoint.resetAuth()
|
entrypoint.resetAuth()
|
||||||
|
|
||||||
elif mode == 'passwords':
|
elif mode == 'passwords':
|
||||||
passwordsXML()
|
passwords_xml()
|
||||||
|
|
||||||
elif mode == 'switchuser':
|
elif mode == 'switchuser':
|
||||||
entrypoint.switchPlexUser()
|
entrypoint.switchPlexUser()
|
||||||
|
|
BIN
empty_video.mp4
Normal file
BIN
empty_video.mp4
Normal file
Binary file not shown.
|
@ -23,10 +23,19 @@ msgctxt "#30000"
|
||||||
msgid "Server Address (IP)"
|
msgid "Server Address (IP)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30001"
|
||||||
|
msgid "Searching for PMS"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30002"
|
msgctxt "#30002"
|
||||||
msgid "Preferred playback method"
|
msgid "Preferred playback method"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# Warning displayed if Kodi setting is enabled. Be sure to escape the quotes again! The exact wording can be found in the Kodi settings, player settings, videos
|
||||||
|
msgctxt "#30003"
|
||||||
|
msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30004"
|
msgctxt "#30004"
|
||||||
msgid "Log level"
|
msgid "Log level"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1424,12 +1433,9 @@ msgctxt "#39030"
|
||||||
msgid "Add network credentials to allow Kodi access to your content? Note: Skipping this step may generate a message during the initial scan of your content if Kodi can't locate your content."
|
msgid "Add network credentials to allow Kodi access to your content? Note: Skipping this step may generate a message during the initial scan of your content if Kodi can't locate your content."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# Error message displayed when verifying Direct Path sync paths passed by Plex
|
||||||
msgctxt "#39031"
|
msgctxt "#39031"
|
||||||
msgid "Kodi can't locate file: "
|
msgid "Kodi cannot locate the file %s. Please verify your PKC settings. Stop syncing?"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#39032"
|
|
||||||
msgid "Please verify the path. You may need to verify your network credentials in the add-on settings or use different Plex paths. Stop syncing?"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39033"
|
msgctxt "#39033"
|
||||||
|
@ -1556,6 +1562,11 @@ msgctxt "#39064"
|
||||||
msgid "Recently Added: Also show already watched episodes"
|
msgid "Recently Added: Also show already watched episodes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC settings, Appearance Tweaks
|
||||||
|
msgctxt "#39065"
|
||||||
|
msgid "Force-refresh Kodi skin on stopping playback"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39066"
|
msgctxt "#39066"
|
||||||
msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)"
|
msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1666,10 +1677,6 @@ msgctxt "#39213"
|
||||||
msgid "is offline"
|
msgid "is offline"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39214"
|
|
||||||
msgid "Even though we signed in to plex.tv, we could not authorize for PMS"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#39215"
|
msgctxt "#39215"
|
||||||
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
|
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1851,10 +1858,6 @@ msgctxt "#39601"
|
||||||
msgid "Could not stop the database from running. Please try again later."
|
msgid "Could not stop the database from running. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39602"
|
|
||||||
msgid "Remove all cached artwork? (recommended!)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#39603"
|
msgctxt "#39603"
|
||||||
msgid "Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended and unnecessary!)"
|
msgid "Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended and unnecessary!)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,25 +1,31 @@
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
import logging
|
The Plex Companion master python file
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import Queue
|
from Queue import Empty
|
||||||
from socket import SHUT_RDWR
|
from socket import SHUT_RDWR
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
|
||||||
from xbmc import sleep, executebuiltin
|
from xbmc import sleep, executebuiltin
|
||||||
|
|
||||||
from utils import settings, thread_methods
|
from utils import settings, thread_methods, language as lang, dialog
|
||||||
from plexbmchelper import listener, plexgdm, subscribers, functions, \
|
from plexbmchelper import listener, plexgdm, subscribers, httppersist
|
||||||
httppersist, plexsettings
|
from plexbmchelper.subscribers import LOCKER
|
||||||
from PlexFunctions import ParseContainerKey, GetPlexMetadata
|
from PlexFunctions import ParseContainerKey, GetPlexMetadata, DownloadChunks
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from playlist_func import get_pms_playqueue, get_plextype_from_xml
|
from playlist_func import get_pms_playqueue, get_plextype_from_xml, \
|
||||||
|
get_playlist_details_from_xml
|
||||||
|
from playback import playback_triage, play_xml
|
||||||
|
import json_rpc as js
|
||||||
import player
|
import player
|
||||||
import variables as v
|
import variables as v
|
||||||
import state
|
import state
|
||||||
|
import playqueue as PQ
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -27,40 +33,144 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
@thread_methods(add_suspends=['PMS_STATUS'])
|
@thread_methods(add_suspends=['PMS_STATUS'])
|
||||||
class PlexCompanion(Thread):
|
class PlexCompanion(Thread):
|
||||||
"""
|
"""
|
||||||
|
Plex Companion monitoring class. Invoke only once
|
||||||
"""
|
"""
|
||||||
def __init__(self, callback=None):
|
def __init__(self):
|
||||||
log.info("----===## Starting PlexCompanion ##===----")
|
LOG.info("----===## Starting PlexCompanion ##===----")
|
||||||
if callback is not None:
|
# Init Plex Companion queue
|
||||||
self.mgr = callback
|
|
||||||
self.settings = plexsettings.getSettings()
|
|
||||||
# Start GDM for server/client discovery
|
# Start GDM for server/client discovery
|
||||||
self.client = plexgdm.plexgdm()
|
self.client = plexgdm.plexgdm()
|
||||||
self.client.clientDetails(self.settings)
|
self.client.clientDetails()
|
||||||
log.debug("Registration string is:\n%s"
|
LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
|
||||||
% self.client.getClientDetails())
|
|
||||||
# kodi player instance
|
# kodi player instance
|
||||||
self.player = player.Player()
|
self.player = player.PKC_Player()
|
||||||
|
self.httpd = False
|
||||||
|
self.subscription_manager = None
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def _getStartItem(self, string):
|
@LOCKER.lockthis
|
||||||
"""
|
def _process_alexa(self, data):
|
||||||
Grabs the Plex id from e.g. '/library/metadata/12987'
|
xml = GetPlexMetadata(data['key'])
|
||||||
|
try:
|
||||||
and returns the tuple (typus, id) where typus is either 'queueId' or
|
xml[0].attrib
|
||||||
'plexId' and id is the corresponding id as a string
|
except (AttributeError, IndexError, TypeError):
|
||||||
"""
|
LOG.error('Could not download Plex metadata for: %s', data)
|
||||||
typus = 'plexId'
|
return
|
||||||
if string.startswith('/library/metadata'):
|
api = API(xml[0])
|
||||||
try:
|
if api.plex_type() == v.PLEX_TYPE_ALBUM:
|
||||||
string = string.split('/')[3]
|
LOG.debug('Plex music album detected')
|
||||||
except IndexError:
|
PQ.init_playqueue_from_plex_children(
|
||||||
string = ''
|
api.plex_id(),
|
||||||
|
transient_token=data.get('token'))
|
||||||
|
elif data['containerKey'].startswith('/playQueues/'):
|
||||||
|
_, container_key, _ = ParseContainerKey(data['containerKey'])
|
||||||
|
xml = DownloadChunks('{server}/playQueues/%s?' % container_key)
|
||||||
|
if xml is None:
|
||||||
|
# "Play error"
|
||||||
|
dialog('notification', lang(29999), lang(30128), icon='{error}')
|
||||||
|
return
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||||
|
playqueue.clear()
|
||||||
|
get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
playqueue.plex_transient_token = data.get('token')
|
||||||
|
if data.get('offset') != '0':
|
||||||
|
offset = float(data['offset']) / 1000.0
|
||||||
|
else:
|
||||||
|
offset = None
|
||||||
|
play_xml(playqueue, xml, offset)
|
||||||
else:
|
else:
|
||||||
log.error('Unknown string! %s' % string)
|
state.PLEX_TRANSIENT_TOKEN = data.get('token')
|
||||||
return typus, string
|
if data.get('offset') != '0':
|
||||||
|
state.RESUMABLE = True
|
||||||
|
state.RESUME_PLAYBACK = True
|
||||||
|
playback_triage(api.plex_id(), api.plex_type(), resolve=False)
|
||||||
|
|
||||||
def processTasks(self, task):
|
@staticmethod
|
||||||
|
def _process_node(data):
|
||||||
|
"""
|
||||||
|
E.g. watch later initiated by Companion. Basically navigating Plex
|
||||||
|
"""
|
||||||
|
state.PLEX_TRANSIENT_TOKEN = data.get('key')
|
||||||
|
params = {
|
||||||
|
'mode': 'plex_node',
|
||||||
|
'key': '{server}%s' % data.get('key'),
|
||||||
|
'offset': data.get('offset'),
|
||||||
|
'play_directly': 'true'
|
||||||
|
}
|
||||||
|
executebuiltin('RunPlugin(plugin://%s?%s)'
|
||||||
|
% (v.ADDON_ID, urlencode(params)))
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
|
def _process_playlist(self, data):
|
||||||
|
# Get the playqueue ID
|
||||||
|
_, container_key, query = ParseContainerKey(data['containerKey'])
|
||||||
|
try:
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
|
||||||
|
except KeyError:
|
||||||
|
# E.g. Plex web does not supply the media type
|
||||||
|
# Still need to figure out the type (video vs. music vs. pix)
|
||||||
|
xml = GetPlexMetadata(data['key'])
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (AttributeError, IndexError, TypeError):
|
||||||
|
LOG.error('Could not download Plex metadata')
|
||||||
|
return
|
||||||
|
api = API(xml[0])
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||||
|
PQ.update_playqueue_from_PMS(
|
||||||
|
playqueue,
|
||||||
|
playqueue_id=container_key,
|
||||||
|
repeat=query.get('repeat'),
|
||||||
|
offset=data.get('offset'),
|
||||||
|
transient_token=data.get('token'))
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
|
def _process_streams(self, data):
|
||||||
|
"""
|
||||||
|
Plex Companion client adjusted audio or subtitle stream
|
||||||
|
"""
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
|
||||||
|
pos = js.get_position(playqueue.playlistid)
|
||||||
|
if 'audioStreamID' in data:
|
||||||
|
index = playqueue.items[pos].kodi_stream_index(
|
||||||
|
data['audioStreamID'], 'audio')
|
||||||
|
self.player.setAudioStream(index)
|
||||||
|
elif 'subtitleStreamID' in data:
|
||||||
|
if data['subtitleStreamID'] == '0':
|
||||||
|
self.player.showSubtitles(False)
|
||||||
|
else:
|
||||||
|
index = playqueue.items[pos].kodi_stream_index(
|
||||||
|
data['subtitleStreamID'], 'subtitle')
|
||||||
|
self.player.setSubtitleStream(index)
|
||||||
|
else:
|
||||||
|
LOG.error('Unknown setStreams command: %s', data)
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
|
def _process_refresh(self, data):
|
||||||
|
"""
|
||||||
|
example data: {'playQueueID': '8475', 'commandID': '11'}
|
||||||
|
"""
|
||||||
|
xml = get_pms_playqueue(data['playQueueID'])
|
||||||
|
if xml is None:
|
||||||
|
return
|
||||||
|
if len(xml) == 0:
|
||||||
|
LOG.debug('Empty playqueue received - clearing playqueue')
|
||||||
|
plex_type = get_plextype_from_xml(xml)
|
||||||
|
if plex_type is None:
|
||||||
|
return
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
||||||
|
playqueue.clear()
|
||||||
|
return
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
||||||
|
PQ.update_playqueue_from_PMS(playqueue, data['playQueueID'])
|
||||||
|
|
||||||
|
def _process_tasks(self, task):
|
||||||
"""
|
"""
|
||||||
Processes tasks picked up e.g. by Companion listener, e.g.
|
Processes tasks picked up e.g. by Companion listener, e.g.
|
||||||
{'action': 'playlist',
|
{'action': 'playlist',
|
||||||
|
@ -75,105 +185,26 @@ class PlexCompanion(Thread):
|
||||||
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
|
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
|
||||||
'type': 'video'}}
|
'type': 'video'}}
|
||||||
"""
|
"""
|
||||||
log.debug('Processing: %s' % task)
|
LOG.debug('Processing: %s', task)
|
||||||
data = task['data']
|
data = task['data']
|
||||||
|
|
||||||
# Get the token of the user flinging media (might be different one)
|
|
||||||
token = data.get('token')
|
|
||||||
if task['action'] == 'alexa':
|
if task['action'] == 'alexa':
|
||||||
# e.g. Alexa
|
self._process_alexa(data)
|
||||||
xml = GetPlexMetadata(data['key'])
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except (AttributeError, IndexError, TypeError):
|
|
||||||
log.error('Could not download Plex metadata')
|
|
||||||
return
|
|
||||||
api = API(xml[0])
|
|
||||||
if api.getType() == v.PLEX_TYPE_ALBUM:
|
|
||||||
log.debug('Plex music album detected')
|
|
||||||
queue = self.mgr.playqueue.init_playqueue_from_plex_children(
|
|
||||||
api.getRatingKey())
|
|
||||||
queue.plex_transient_token = token
|
|
||||||
else:
|
|
||||||
state.PLEX_TRANSIENT_TOKEN = token
|
|
||||||
params = {
|
|
||||||
'mode': 'plex_node',
|
|
||||||
'key': '{server}%s' % data.get('key'),
|
|
||||||
'view_offset': data.get('offset'),
|
|
||||||
'play_directly': 'true',
|
|
||||||
'node': 'false'
|
|
||||||
}
|
|
||||||
executebuiltin('RunPlugin(plugin://%s?%s)'
|
|
||||||
% (v.ADDON_ID, urlencode(params)))
|
|
||||||
|
|
||||||
elif (task['action'] == 'playlist' and
|
elif (task['action'] == 'playlist' and
|
||||||
data.get('address') == 'node.plexapp.com'):
|
data.get('address') == 'node.plexapp.com'):
|
||||||
# E.g. watch later initiated by Companion
|
self._process_node(data)
|
||||||
state.PLEX_TRANSIENT_TOKEN = token
|
|
||||||
params = {
|
|
||||||
'mode': 'plex_node',
|
|
||||||
'key': '{server}%s' % data.get('key'),
|
|
||||||
'view_offset': data.get('offset'),
|
|
||||||
'play_directly': 'true'
|
|
||||||
}
|
|
||||||
executebuiltin('RunPlugin(plugin://%s?%s)'
|
|
||||||
% (v.ADDON_ID, urlencode(params)))
|
|
||||||
|
|
||||||
elif task['action'] == 'playlist':
|
elif task['action'] == 'playlist':
|
||||||
# Get the playqueue ID
|
self._process_playlist(data)
|
||||||
try:
|
|
||||||
typus, ID, query = ParseContainerKey(data['containerKey'])
|
|
||||||
except Exception as e:
|
|
||||||
log.error('Exception while processing: %s' % e)
|
|
||||||
import traceback
|
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
|
|
||||||
except KeyError:
|
|
||||||
# E.g. Plex web does not supply the media type
|
|
||||||
# Still need to figure out the type (video vs. music vs. pix)
|
|
||||||
xml = GetPlexMetadata(data['key'])
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except (AttributeError, IndexError, TypeError):
|
|
||||||
log.error('Could not download Plex metadata')
|
|
||||||
return
|
|
||||||
api = API(xml[0])
|
|
||||||
playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
|
|
||||||
self.mgr.playqueue.update_playqueue_from_PMS(
|
|
||||||
playqueue,
|
|
||||||
ID,
|
|
||||||
repeat=query.get('repeat'),
|
|
||||||
offset=data.get('offset'))
|
|
||||||
playqueue.plex_transient_token = token
|
|
||||||
|
|
||||||
elif task['action'] == 'refreshPlayQueue':
|
elif task['action'] == 'refreshPlayQueue':
|
||||||
# example data: {'playQueueID': '8475', 'commandID': '11'}
|
self._process_refresh(data)
|
||||||
xml = get_pms_playqueue(data['playQueueID'])
|
elif task['action'] == 'setStreams':
|
||||||
if xml is None:
|
self._process_streams(data)
|
||||||
return
|
|
||||||
if len(xml) == 0:
|
|
||||||
log.debug('Empty playqueue received - clearing playqueue')
|
|
||||||
plex_type = get_plextype_from_xml(xml)
|
|
||||||
if plex_type is None:
|
|
||||||
return
|
|
||||||
playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
|
||||||
playqueue.clear()
|
|
||||||
return
|
|
||||||
playqueue = self.mgr.playqueue.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
|
||||||
self.mgr.playqueue.update_playqueue_from_PMS(
|
|
||||||
playqueue,
|
|
||||||
data['playQueueID'])
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# Ensure that sockets will be closed no matter what
|
"""
|
||||||
|
Ensure that sockets will be closed no matter what
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.__run()
|
self._run()
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
self.httpd.socket.shutdown(SHUT_RDWR)
|
self.httpd.socket.shutdown(SHUT_RDWR)
|
||||||
|
@ -184,24 +215,20 @@ class PlexCompanion(Thread):
|
||||||
self.httpd.socket.close()
|
self.httpd.socket.close()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
log.info("----===## Plex Companion stopped ##===----")
|
LOG.info("----===## Plex Companion stopped ##===----")
|
||||||
|
|
||||||
def __run(self):
|
def _run(self):
|
||||||
self.httpd = False
|
|
||||||
httpd = self.httpd
|
httpd = self.httpd
|
||||||
# Cache for quicker while loops
|
# Cache for quicker while loops
|
||||||
client = self.client
|
client = self.client
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
|
|
||||||
# Start up instances
|
# Start up instances
|
||||||
requestMgr = httppersist.RequestMgr()
|
request_mgr = httppersist.RequestMgr()
|
||||||
jsonClass = functions.jsonClass(requestMgr, self.settings)
|
subscription_manager = subscribers.SubscriptionMgr(request_mgr,
|
||||||
subscriptionManager = subscribers.SubscriptionManager(
|
self.player)
|
||||||
jsonClass, requestMgr, self.player, self.mgr)
|
self.subscription_manager = subscription_manager
|
||||||
|
|
||||||
queue = Queue.Queue(maxsize=100)
|
|
||||||
self.queue = queue
|
|
||||||
|
|
||||||
if settings('plexCompanion') == 'true':
|
if settings('plexCompanion') == 'true':
|
||||||
# Start up httpd
|
# Start up httpd
|
||||||
|
@ -210,82 +237,74 @@ class PlexCompanion(Thread):
|
||||||
try:
|
try:
|
||||||
httpd = listener.ThreadedHTTPServer(
|
httpd = listener.ThreadedHTTPServer(
|
||||||
client,
|
client,
|
||||||
subscriptionManager,
|
subscription_manager,
|
||||||
jsonClass,
|
('', v.COMPANION_PORT),
|
||||||
self.settings,
|
|
||||||
queue,
|
|
||||||
('', self.settings['myport']),
|
|
||||||
listener.MyHandler)
|
listener.MyHandler)
|
||||||
httpd.timeout = 0.95
|
httpd.timeout = 0.95
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
log.error("Unable to start PlexCompanion. Traceback:")
|
LOG.error("Unable to start PlexCompanion. Traceback:")
|
||||||
import traceback
|
import traceback
|
||||||
log.error(traceback.print_exc())
|
LOG.error(traceback.print_exc())
|
||||||
|
|
||||||
sleep(3000)
|
sleep(3000)
|
||||||
|
|
||||||
if start_count == 3:
|
if start_count == 3:
|
||||||
log.error("Error: Unable to start web helper.")
|
LOG.error("Error: Unable to start web helper.")
|
||||||
httpd = False
|
httpd = False
|
||||||
break
|
break
|
||||||
|
|
||||||
start_count += 1
|
start_count += 1
|
||||||
else:
|
else:
|
||||||
log.info('User deactivated Plex Companion')
|
LOG.info('User deactivated Plex Companion')
|
||||||
|
|
||||||
client.start_all()
|
client.start_all()
|
||||||
|
|
||||||
message_count = 0
|
message_count = 0
|
||||||
if httpd:
|
if httpd:
|
||||||
t = Thread(target=httpd.handle_request)
|
thread = Thread(target=httpd.handle_request)
|
||||||
|
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
# If we are not authorized, sleep
|
# If we are not authorized, sleep
|
||||||
# Otherwise, we trigger a download which leads to a
|
# Otherwise, we trigger a download which leads to a
|
||||||
# re-authorizations
|
# re-authorizations
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
break
|
break
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
message_count += 1
|
message_count += 1
|
||||||
if httpd:
|
if httpd:
|
||||||
if not t.isAlive():
|
if not thread.isAlive():
|
||||||
# Use threads cause the method will stall
|
# Use threads cause the method will stall
|
||||||
t = Thread(target=httpd.handle_request)
|
thread = Thread(target=httpd.handle_request)
|
||||||
t.start()
|
thread.start()
|
||||||
|
|
||||||
if message_count == 3000:
|
if message_count == 3000:
|
||||||
message_count = 0
|
message_count = 0
|
||||||
if client.check_client_registration():
|
if client.check_client_registration():
|
||||||
log.debug("Client is still registered")
|
LOG.debug('Client is still registered')
|
||||||
else:
|
else:
|
||||||
log.debug("Client is no longer registered. "
|
LOG.debug('Client is no longer registered. Plex '
|
||||||
"Plex Companion still running on port %s"
|
'Companion still running on port %s',
|
||||||
% self.settings['myport'])
|
v.COMPANION_PORT)
|
||||||
client.register_as_client()
|
client.register_as_client()
|
||||||
# Get and set servers
|
# Get and set servers
|
||||||
if message_count % 30 == 0:
|
if message_count % 30 == 0:
|
||||||
subscriptionManager.serverlist = client.getServerList()
|
subscription_manager.serverlist = client.getServerList()
|
||||||
subscriptionManager.notify()
|
subscription_manager.notify()
|
||||||
if not httpd:
|
if not httpd:
|
||||||
message_count = 0
|
message_count = 0
|
||||||
except:
|
except:
|
||||||
log.warn("Error in loop, continuing anyway. Traceback:")
|
LOG.warn("Error in loop, continuing anyway. Traceback:")
|
||||||
import traceback
|
import traceback
|
||||||
log.warn(traceback.format_exc())
|
LOG.warn(traceback.format_exc())
|
||||||
# See if there's anything we need to process
|
# See if there's anything we need to process
|
||||||
try:
|
try:
|
||||||
task = queue.get(block=False)
|
task = state.COMPANION_QUEUE.get(block=False)
|
||||||
except Queue.Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Got instructions, process them
|
# Got instructions, process them
|
||||||
self.processTasks(task)
|
self._process_tasks(task)
|
||||||
queue.task_done()
|
state.COMPANION_QUEUE.task_done()
|
||||||
# Don't sleep
|
# Don't sleep
|
||||||
continue
|
continue
|
||||||
sleep(50)
|
sleep(50)
|
||||||
|
subscription_manager.signal_stop()
|
||||||
client.stop_all()
|
client.stop_all()
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from urllib import urlencode
|
from urllib import urlencode, quote_plus
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from urlparse import urlparse, parse_qsl
|
from urlparse import urlparse, parse_qsl
|
||||||
import re
|
from re import compile as re_compile
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from time import time
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
import downloadutils
|
from xbmc import sleep
|
||||||
from utils import settings
|
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
from utils import settings, try_encode, try_decode
|
||||||
from variables import PLEX_TO_KODI_TIMEFACTOR
|
from variables import PLEX_TO_KODI_TIMEFACTOR
|
||||||
|
import plex_tv
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
log = getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
CONTAINERSIZE = int(settings('limitindex'))
|
CONTAINERSIZE = int(settings('limitindex'))
|
||||||
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
|
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
|
||||||
|
|
||||||
|
# For discovery of PMS in the local LAN
|
||||||
|
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
|
||||||
|
PLEX_GDM_PORT = 32414
|
||||||
|
PLEX_GDM_MSG = 'M-SEARCH * HTTP/1.0'
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,30 +86,372 @@ def GetMethodFromPlexType(plexType):
|
||||||
return methods[plexType]
|
return methods[plexType]
|
||||||
|
|
||||||
|
|
||||||
def XbmcItemtypes():
|
def GetPlexLoginFromSettings():
|
||||||
return ['photo', 'video', 'audio']
|
|
||||||
|
|
||||||
|
|
||||||
def PlexItemtypes():
|
|
||||||
return ['photo', 'video', 'audio']
|
|
||||||
|
|
||||||
|
|
||||||
def PlexLibraryItemtypes():
|
|
||||||
return ['movie', 'show']
|
|
||||||
# later add: 'artist', 'photo'
|
|
||||||
|
|
||||||
|
|
||||||
def EmbyItemtypes():
|
|
||||||
return ['Movie', 'Series', 'Season', 'Episode']
|
|
||||||
|
|
||||||
|
|
||||||
def SelectStreams(url, args):
|
|
||||||
"""
|
"""
|
||||||
Does a PUT request to tell the PMS what audio and subtitle streams we have
|
Returns a dict:
|
||||||
chosen.
|
'plexLogin': settings('plexLogin'),
|
||||||
|
'plexToken': settings('plexToken'),
|
||||||
|
'plexhome': settings('plexhome'),
|
||||||
|
'plexid': settings('plexid'),
|
||||||
|
'myplexlogin': settings('myplexlogin'),
|
||||||
|
'plexAvatar': settings('plexAvatar'),
|
||||||
|
'plexHomeSize': settings('plexHomeSize')
|
||||||
|
|
||||||
|
Returns strings or unicode
|
||||||
|
|
||||||
|
Returns empty strings '' for a setting if not found.
|
||||||
|
|
||||||
|
myplexlogin is 'true' if user opted to log into plex.tv (the default)
|
||||||
|
plexhome is 'true' if plex home is used (the default)
|
||||||
"""
|
"""
|
||||||
downloadutils.DownloadUtils().downloadUrl(
|
return {
|
||||||
url + '?' + urlencode(args), action_type='PUT')
|
'plexLogin': settings('plexLogin'),
|
||||||
|
'plexToken': settings('plexToken'),
|
||||||
|
'plexhome': settings('plexhome'),
|
||||||
|
'plexid': settings('plexid'),
|
||||||
|
'myplexlogin': settings('myplexlogin'),
|
||||||
|
'plexAvatar': settings('plexAvatar'),
|
||||||
|
'plexHomeSize': settings('plexHomeSize')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_connection(url, token=None, verifySSL=None):
|
||||||
|
"""
|
||||||
|
Checks connection to a Plex server, available at url. Can also be used
|
||||||
|
to check for connection with plex.tv.
|
||||||
|
|
||||||
|
Override SSL to skip the check by setting verifySSL=False
|
||||||
|
if 'None', SSL will be checked (standard requests setting)
|
||||||
|
if 'True', SSL settings from file settings are used (False/True)
|
||||||
|
|
||||||
|
Input:
|
||||||
|
url URL to Plex server (e.g. https://192.168.1.1:32400)
|
||||||
|
token appropriate token to access server. If None is passed,
|
||||||
|
the current token is used
|
||||||
|
Output:
|
||||||
|
False if server could not be reached or timeout occured
|
||||||
|
200 if connection was successfull
|
||||||
|
int or other HTML status codes as received from the server
|
||||||
|
"""
|
||||||
|
# Add '/clients' to URL because then an authentication is necessary
|
||||||
|
# If a plex.tv URL was passed, this does not work.
|
||||||
|
header_options = None
|
||||||
|
if token is not None:
|
||||||
|
header_options = {'X-Plex-Token': token}
|
||||||
|
if verifySSL is True:
|
||||||
|
verifySSL = None if settings('sslverify') == 'true' else False
|
||||||
|
if 'plex.tv' in url:
|
||||||
|
url = 'https://plex.tv/api/home/users'
|
||||||
|
else:
|
||||||
|
url = url + '/library/onDeck'
|
||||||
|
LOG.debug("Checking connection to server %s with verifySSL=%s",
|
||||||
|
url, verifySSL)
|
||||||
|
answer = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
headerOptions=header_options,
|
||||||
|
verifySSL=verifySSL,
|
||||||
|
timeout=10)
|
||||||
|
if answer is None:
|
||||||
|
LOG.debug("Could not connect to %s", url)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
# xml received?
|
||||||
|
answer.attrib
|
||||||
|
except AttributeError:
|
||||||
|
if answer is True:
|
||||||
|
# Maybe no xml but connection was successful nevertheless
|
||||||
|
answer = 200
|
||||||
|
else:
|
||||||
|
# Success - we downloaded an xml!
|
||||||
|
answer = 200
|
||||||
|
# We could connect but maybe were not authenticated. No worries
|
||||||
|
LOG.debug("Checking connection successfull. Answer: %s", answer)
|
||||||
|
return answer
|
||||||
|
|
||||||
|
|
||||||
|
def discover_pms(token=None):
|
||||||
|
"""
|
||||||
|
Optional parameter:
|
||||||
|
token token for plex.tv
|
||||||
|
|
||||||
|
Returns a list of available PMS to connect to, one entry is the dict:
|
||||||
|
{
|
||||||
|
'machineIdentifier' [str] unique identifier of the PMS
|
||||||
|
'name' [str] name of the PMS
|
||||||
|
'token' [str] token needed to access that PMS
|
||||||
|
'ownername' [str] name of the owner of this PMS or None if
|
||||||
|
the owner itself supplied tries to connect
|
||||||
|
'product' e.g. 'Plex Media Server' or None
|
||||||
|
'version' e.g. '1.11.2.4772-3e...' or None
|
||||||
|
'device': e.g. 'PC' or 'Windows' or None
|
||||||
|
'platform': e.g. 'Windows', 'Android' or None
|
||||||
|
'local' [bool] True if plex.tv supplied
|
||||||
|
'publicAddressMatches'='1'
|
||||||
|
or if found using Plex GDM in the local LAN
|
||||||
|
'owned' [bool] True if it's the owner's PMS
|
||||||
|
'relay' [bool] True if plex.tv supplied 'relay'='1'
|
||||||
|
'presence' [bool] True if plex.tv supplied 'presence'='1'
|
||||||
|
'httpsRequired' [bool] True if plex.tv supplied
|
||||||
|
'httpsRequired'='1'
|
||||||
|
'scheme' [str] either 'http' or 'https'
|
||||||
|
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
|
||||||
|
'port': [str] Port of the PMS, e.g. '32400'
|
||||||
|
'baseURL': [str] <scheme>://<ip>:<port> of the PMS
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
LOG.info('Start discovery of Plex Media Servers')
|
||||||
|
# Look first for local PMS in the LAN
|
||||||
|
local_pms_list = _plex_gdm()
|
||||||
|
LOG.debug('PMS found in the local LAN using Plex GDM: %s', local_pms_list)
|
||||||
|
# Get PMS from plex.tv
|
||||||
|
if token:
|
||||||
|
LOG.info('Checking with plex.tv for more PMS to connect to')
|
||||||
|
plex_pms_list = _pms_list_from_plex_tv(token)
|
||||||
|
LOG.debug('PMS found on plex.tv: %s', plex_pms_list)
|
||||||
|
else:
|
||||||
|
LOG.info('No plex token supplied, only checked LAN for available PMS')
|
||||||
|
plex_pms_list = []
|
||||||
|
|
||||||
|
# See if we found a PMS both locally and using plex.tv. If so, use local
|
||||||
|
# connection data
|
||||||
|
all_pms = []
|
||||||
|
for pms in local_pms_list:
|
||||||
|
for i, plex_pms in enumerate(plex_pms_list):
|
||||||
|
if pms['machineIdentifier'] == plex_pms['machineIdentifier']:
|
||||||
|
# Update with GDM data - potentially more reliable than plex.tv
|
||||||
|
LOG.debug('Found this PMS also in the LAN: %s', plex_pms)
|
||||||
|
plex_pms['ip'] = pms['ip']
|
||||||
|
plex_pms['port'] = pms['port']
|
||||||
|
plex_pms['local'] = True
|
||||||
|
# Use all the other data we know from plex.tv
|
||||||
|
pms = plex_pms
|
||||||
|
# Remove this particular pms since we already know it
|
||||||
|
plex_pms_list.pop(i)
|
||||||
|
break
|
||||||
|
https = _pms_https_enabled('%s:%s' % (pms['ip'], pms['port']))
|
||||||
|
if https is None:
|
||||||
|
# Error contacting url. Skip and ignore this PMS for now
|
||||||
|
continue
|
||||||
|
elif https is True:
|
||||||
|
pms['scheme'] = 'https'
|
||||||
|
pms['baseURL'] = 'https://%s:%s' % (pms['ip'], pms['port'])
|
||||||
|
else:
|
||||||
|
pms['scheme'] = 'http'
|
||||||
|
pms['baseURL'] = 'http://%s:%s' % (pms['ip'], pms['port'])
|
||||||
|
all_pms.append(pms)
|
||||||
|
# Now add the remaining PMS from plex.tv (where we already checked connect.)
|
||||||
|
for plex_pms in plex_pms_list:
|
||||||
|
all_pms.append(plex_pms)
|
||||||
|
LOG.debug('Found the following PMS in total: %s', all_pms)
|
||||||
|
return all_pms
|
||||||
|
|
||||||
|
|
||||||
|
def _plex_gdm():
|
||||||
|
"""
|
||||||
|
PlexGDM - looks for PMS in the local LAN and returns a list of the PMS found
|
||||||
|
"""
|
||||||
|
# Import here because we might not need to do gdm because we already
|
||||||
|
# connected to a PMS successfully in the past
|
||||||
|
import struct
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# setup socket for discovery -> multicast message
|
||||||
|
gdm = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
gdm.settimeout(2.0)
|
||||||
|
# Set the time-to-live for messages to 2 for local network
|
||||||
|
ttl = struct.pack('b', 2)
|
||||||
|
gdm.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
|
||||||
|
|
||||||
|
return_data = []
|
||||||
|
try:
|
||||||
|
# Send data to the multicast group
|
||||||
|
gdm.sendto(PLEX_GDM_MSG, (PLEX_GDM_IP, PLEX_GDM_PORT))
|
||||||
|
|
||||||
|
# Look for responses from all recipients
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data, server = gdm.recvfrom(1024)
|
||||||
|
return_data.append({'from': server, 'data': data})
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
# Probably error: (101, 'Network is unreachable')
|
||||||
|
LOG.error(e)
|
||||||
|
import traceback
|
||||||
|
LOG.error("Traceback:\n%s", traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
gdm.close()
|
||||||
|
LOG.debug('Plex GDM returned the data: %s', return_data)
|
||||||
|
pms_list = []
|
||||||
|
for response in return_data:
|
||||||
|
# Check if we had a positive HTTP response
|
||||||
|
if '200 OK' not in response['data']:
|
||||||
|
continue
|
||||||
|
pms = {
|
||||||
|
'ip': response['from'][0],
|
||||||
|
'scheme': None,
|
||||||
|
'local': True, # Since we found it using GDM
|
||||||
|
'product': None,
|
||||||
|
'baseURL': None,
|
||||||
|
'name': None,
|
||||||
|
'version': None,
|
||||||
|
'token': None,
|
||||||
|
'ownername': None,
|
||||||
|
'device': None,
|
||||||
|
'platform': None,
|
||||||
|
'owned': None,
|
||||||
|
'relay': None,
|
||||||
|
'presence': True, # Since we're talking to the PMS
|
||||||
|
'httpsRequired': None,
|
||||||
|
}
|
||||||
|
for line in response['data'].split('\n'):
|
||||||
|
if 'Content-Type:' in line:
|
||||||
|
pms['product'] = try_decode(line.split(':')[1].strip())
|
||||||
|
elif 'Host:' in line:
|
||||||
|
pms['baseURL'] = line.split(':')[1].strip()
|
||||||
|
elif 'Name:' in line:
|
||||||
|
pms['name'] = try_decode(line.split(':')[1].strip())
|
||||||
|
elif 'Port:' in line:
|
||||||
|
pms['port'] = line.split(':')[1].strip()
|
||||||
|
elif 'Resource-Identifier:' in line:
|
||||||
|
pms['machineIdentifier'] = line.split(':')[1].strip()
|
||||||
|
elif 'Version:' in line:
|
||||||
|
pms['version'] = line.split(':')[1].strip()
|
||||||
|
pms_list.append(pms)
|
||||||
|
return pms_list
|
||||||
|
|
||||||
|
|
||||||
|
def _pms_list_from_plex_tv(token):
|
||||||
|
"""
|
||||||
|
get Plex media Server List from plex.tv/pms/resources
|
||||||
|
"""
|
||||||
|
xml = DU().downloadUrl('https://plex.tv/api/resources',
|
||||||
|
authenticate=False,
|
||||||
|
parameters={'includeHttps': 1},
|
||||||
|
headerOptions={'X-Plex-Token': token})
|
||||||
|
try:
|
||||||
|
xml.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Could not get list of PMS from plex.tv')
|
||||||
|
return
|
||||||
|
|
||||||
|
from Queue import Queue
|
||||||
|
queue = Queue()
|
||||||
|
thread_queue = []
|
||||||
|
|
||||||
|
max_age_in_seconds = 2*60*60*24
|
||||||
|
for device in xml.findall('Device'):
|
||||||
|
if 'server' not in device.get('provides'):
|
||||||
|
# No PMS - skip
|
||||||
|
continue
|
||||||
|
if device.find('Connection') is None:
|
||||||
|
# no valid connection - skip
|
||||||
|
continue
|
||||||
|
# check MyPlex data age - skip if >2 days
|
||||||
|
info_age = time() - int(device.get('lastSeenAt'))
|
||||||
|
if info_age > max_age_in_seconds:
|
||||||
|
LOG.debug("Skip server %s not seen for 2 days", device.get('name'))
|
||||||
|
continue
|
||||||
|
pms = {
|
||||||
|
'machineIdentifier': device.get('clientIdentifier'),
|
||||||
|
'name': device.get('name'),
|
||||||
|
'token': device.get('accessToken'),
|
||||||
|
'ownername': device.get('sourceTitle'),
|
||||||
|
'product': device.get('product'), # e.g. 'Plex Media Server'
|
||||||
|
'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e...'
|
||||||
|
'device': device.get('device'), # e.g. 'PC' or 'Windows'
|
||||||
|
'platform': device.get('platform'), # e.g. 'Windows', 'Android'
|
||||||
|
'local': device.get('publicAddressMatches') == '1',
|
||||||
|
'owned': device.get('owned') == '1',
|
||||||
|
'relay': device.get('relay') == '1',
|
||||||
|
'presence': device.get('presence') == '1',
|
||||||
|
'httpsRequired': device.get('httpsRequired') == '1',
|
||||||
|
'connections': []
|
||||||
|
}
|
||||||
|
# Try a local connection first, no matter what plex.tv tells us
|
||||||
|
for connection in device.findall('Connection'):
|
||||||
|
if connection.get('local') == '1':
|
||||||
|
pms['connections'].append(connection)
|
||||||
|
# Then try non-local
|
||||||
|
for connection in device.findall('Connection'):
|
||||||
|
if connection.get('local') != '1':
|
||||||
|
pms['connections'].append(connection)
|
||||||
|
# Spawn threads to ping each PMS simultaneously
|
||||||
|
thread = Thread(target=_poke_pms, args=(pms, queue))
|
||||||
|
thread_queue.append(thread)
|
||||||
|
|
||||||
|
max_threads = 5
|
||||||
|
threads = []
|
||||||
|
# poke PMS, own thread for each PMS
|
||||||
|
while True:
|
||||||
|
# Remove finished threads
|
||||||
|
for thread in threads:
|
||||||
|
if not thread.isAlive():
|
||||||
|
threads.remove(thread)
|
||||||
|
if len(threads) < max_threads:
|
||||||
|
try:
|
||||||
|
thread = thread_queue.pop()
|
||||||
|
except IndexError:
|
||||||
|
# We have done our work
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
thread.start()
|
||||||
|
threads.append(thread)
|
||||||
|
else:
|
||||||
|
sleep(50)
|
||||||
|
# wait for requests being answered
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
# declare new PMSs
|
||||||
|
pms_list = []
|
||||||
|
while not queue.empty():
|
||||||
|
pms = queue.get()
|
||||||
|
del pms['connections']
|
||||||
|
pms_list.append(pms)
|
||||||
|
queue.task_done()
|
||||||
|
return pms_list
|
||||||
|
|
||||||
|
|
||||||
|
def _poke_pms(pms, queue):
|
||||||
|
data = pms['connections'][0].attrib
|
||||||
|
if data['local'] == '1':
|
||||||
|
protocol = data['protocol']
|
||||||
|
address = data['address']
|
||||||
|
port = data['port']
|
||||||
|
url = '%s://%s:%s' % (protocol, address, port)
|
||||||
|
else:
|
||||||
|
url = data['uri']
|
||||||
|
if url.count(':') == 1:
|
||||||
|
url = '%s:%s' % (url, data['port'])
|
||||||
|
protocol, address, port = url.split(':', 2)
|
||||||
|
address = address.replace('/', '')
|
||||||
|
xml = DU().downloadUrl('%s/identity' % url,
|
||||||
|
authenticate=False,
|
||||||
|
headerOptions={'X-Plex-Token': pms['token']},
|
||||||
|
verifySSL=False,
|
||||||
|
timeout=10)
|
||||||
|
try:
|
||||||
|
xml.attrib['machineIdentifier']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
# No connection, delete the one we just tested
|
||||||
|
del pms['connections'][0]
|
||||||
|
if pms['connections']:
|
||||||
|
# Still got connections left, try them
|
||||||
|
return _poke_pms(pms, queue)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Connection successful - correct pms?
|
||||||
|
if xml.get('machineIdentifier') == pms['machineIdentifier']:
|
||||||
|
# process later
|
||||||
|
pms['baseURL'] = url
|
||||||
|
pms['scheme'] = protocol
|
||||||
|
pms['ip'] = address
|
||||||
|
pms['port'] = port
|
||||||
|
queue.put(pms)
|
||||||
|
return
|
||||||
|
LOG.info('Found a pms at %s, but the expected machineIdentifier of '
|
||||||
|
'%s did not match the one we found: %s',
|
||||||
|
url, pms['uuid'], xml.get('machineIdentifier'))
|
||||||
|
|
||||||
|
|
||||||
def GetPlexMetadata(key):
|
def GetPlexMetadata(key):
|
||||||
|
@ -128,7 +480,7 @@ def GetPlexMetadata(key):
|
||||||
# 'includeConcerts': 1
|
# 'includeConcerts': 1
|
||||||
}
|
}
|
||||||
url = url + '?' + urlencode(arguments)
|
url = url + '?' + urlencode(arguments)
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(url)
|
xml = DU().downloadUrl(url)
|
||||||
if xml == 401:
|
if xml == 401:
|
||||||
# Either unauthorized (taken care of by doUtils) or PMS under strain
|
# Either unauthorized (taken care of by doUtils) or PMS under strain
|
||||||
return 401
|
return 401
|
||||||
|
@ -137,7 +489,7 @@ def GetPlexMetadata(key):
|
||||||
xml.attrib
|
xml.attrib
|
||||||
# Nope we did not receive a valid XML
|
# Nope we did not receive a valid XML
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error("Error retrieving metadata for %s" % url)
|
LOG.error("Error retrieving metadata for %s", url)
|
||||||
xml = None
|
xml = None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
@ -179,22 +531,21 @@ def DownloadChunks(url):
|
||||||
"""
|
"""
|
||||||
xml = None
|
xml = None
|
||||||
pos = 0
|
pos = 0
|
||||||
errorCounter = 0
|
error_counter = 0
|
||||||
while errorCounter < 10:
|
while error_counter < 10:
|
||||||
args = {
|
args = {
|
||||||
'X-Plex-Container-Size': CONTAINERSIZE,
|
'X-Plex-Container-Size': CONTAINERSIZE,
|
||||||
'X-Plex-Container-Start': pos
|
'X-Plex-Container-Start': pos
|
||||||
}
|
}
|
||||||
xmlpart = downloadutils.DownloadUtils().downloadUrl(
|
xmlpart = DU().downloadUrl(url + urlencode(args))
|
||||||
url + urlencode(args))
|
|
||||||
# If something went wrong - skip in the hope that it works next time
|
# If something went wrong - skip in the hope that it works next time
|
||||||
try:
|
try:
|
||||||
xmlpart.attrib
|
xmlpart.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error('Error while downloading chunks: %s'
|
LOG.error('Error while downloading chunks: %s',
|
||||||
% (url + urlencode(args)))
|
url + urlencode(args))
|
||||||
pos += CONTAINERSIZE
|
pos += CONTAINERSIZE
|
||||||
errorCounter += 1
|
error_counter += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Very first run: starting xml (to retain data in xml's root!)
|
# Very first run: starting xml (to retain data in xml's root!)
|
||||||
|
@ -212,8 +563,8 @@ def DownloadChunks(url):
|
||||||
if len(xmlpart) < CONTAINERSIZE:
|
if len(xmlpart) < CONTAINERSIZE:
|
||||||
break
|
break
|
||||||
pos += CONTAINERSIZE
|
pos += CONTAINERSIZE
|
||||||
if errorCounter == 10:
|
if error_counter == 10:
|
||||||
log.error('Fatal error while downloading chunks for %s' % url)
|
LOG.error('Fatal error while downloading chunks for %s', url)
|
||||||
return None
|
return None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
@ -261,8 +612,7 @@ def get_plex_sections():
|
||||||
"""
|
"""
|
||||||
Returns all Plex sections (libraries) of the PMS as an etree xml
|
Returns all Plex sections (libraries) of the PMS as an etree xml
|
||||||
"""
|
"""
|
||||||
return downloadutils.DownloadUtils().downloadUrl(
|
return DU().downloadUrl('{server}/library/sections')
|
||||||
'{server}/library/sections')
|
|
||||||
|
|
||||||
|
|
||||||
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
|
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
|
||||||
|
@ -281,26 +631,16 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
|
||||||
}
|
}
|
||||||
if trailers is True:
|
if trailers is True:
|
||||||
args['extrasPrefixCount'] = settings('trailerNumber')
|
args['extrasPrefixCount'] = settings('trailerNumber')
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST")
|
||||||
url + '?' + urlencode(args), action_type="POST")
|
|
||||||
try:
|
try:
|
||||||
xml[0].tag
|
xml[0].tag
|
||||||
except (IndexError, TypeError, AttributeError):
|
except (IndexError, TypeError, AttributeError):
|
||||||
log.error("Error retrieving metadata for %s" % url)
|
LOG.error("Error retrieving metadata for %s", url)
|
||||||
return None
|
return None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def getPlexRepeat(kodiRepeat):
|
def _pms_https_enabled(url):
|
||||||
plexRepeat = {
|
|
||||||
'off': '0',
|
|
||||||
'one': '1',
|
|
||||||
'all': '2' # does this work?!?
|
|
||||||
}
|
|
||||||
return plexRepeat.get(kodiRepeat)
|
|
||||||
|
|
||||||
|
|
||||||
def PMSHttpsEnabled(url):
|
|
||||||
"""
|
"""
|
||||||
Returns True if the PMS can talk https, False otherwise.
|
Returns True if the PMS can talk https, False otherwise.
|
||||||
None if error occured, e.g. the connection timed out
|
None if error occured, e.g. the connection timed out
|
||||||
|
@ -312,21 +652,20 @@ def PMSHttpsEnabled(url):
|
||||||
|
|
||||||
Prefers HTTPS over HTTP
|
Prefers HTTPS over HTTP
|
||||||
"""
|
"""
|
||||||
doUtils = downloadutils.DownloadUtils().downloadUrl
|
res = DU().downloadUrl('https://%s/identity' % url,
|
||||||
res = doUtils('https://%s/identity' % url,
|
authenticate=False,
|
||||||
authenticate=False,
|
verifySSL=False)
|
||||||
verifySSL=False)
|
|
||||||
try:
|
try:
|
||||||
res.attrib
|
res.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Might have SSL deactivated. Try with http
|
# Might have SSL deactivated. Try with http
|
||||||
res = doUtils('http://%s/identity' % url,
|
res = DU().downloadUrl('http://%s/identity' % url,
|
||||||
authenticate=False,
|
authenticate=False,
|
||||||
verifySSL=False)
|
verifySSL=False)
|
||||||
try:
|
try:
|
||||||
res.attrib
|
res.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error("Could not contact PMS %s" % url)
|
LOG.error("Could not contact PMS %s", url)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
# Received a valid XML. Server wants to talk HTTP
|
# Received a valid XML. Server wants to talk HTTP
|
||||||
|
@ -342,17 +681,17 @@ def GetMachineIdentifier(url):
|
||||||
|
|
||||||
Returns None if something went wrong
|
Returns None if something went wrong
|
||||||
"""
|
"""
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url,
|
xml = DU().downloadUrl('%s/identity' % url,
|
||||||
authenticate=False,
|
authenticate=False,
|
||||||
verifySSL=False,
|
verifySSL=False,
|
||||||
timeout=10)
|
timeout=10)
|
||||||
try:
|
try:
|
||||||
machineIdentifier = xml.attrib['machineIdentifier']
|
machineIdentifier = xml.attrib['machineIdentifier']
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
log.error('Could not get the PMS machineIdentifier for %s' % url)
|
LOG.error('Could not get the PMS machineIdentifier for %s', url)
|
||||||
return None
|
return None
|
||||||
log.debug('Found machineIdentifier %s for the PMS %s'
|
LOG.debug('Found machineIdentifier %s for the PMS %s',
|
||||||
% (machineIdentifier, url))
|
machineIdentifier, url)
|
||||||
return machineIdentifier
|
return machineIdentifier
|
||||||
|
|
||||||
|
|
||||||
|
@ -372,9 +711,8 @@ def GetPMSStatus(token):
|
||||||
or an empty dict.
|
or an empty dict.
|
||||||
"""
|
"""
|
||||||
answer = {}
|
answer = {}
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
xml = DU().downloadUrl('{server}/status/sessions',
|
||||||
'{server}/status/sessions',
|
headerOptions={'X-Plex-Token': token})
|
||||||
headerOptions={'X-Plex-Token': token})
|
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -412,8 +750,8 @@ def scrobble(ratingKey, state):
|
||||||
url = "{server}/:/unscrobble?" + urlencode(args)
|
url = "{server}/:/unscrobble?" + urlencode(args)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
downloadutils.DownloadUtils().downloadUrl(url)
|
DU().downloadUrl(url)
|
||||||
log.info("Toggled watched state for Plex item %s" % ratingKey)
|
LOG.info("Toggled watched state for Plex item %s", ratingKey)
|
||||||
|
|
||||||
|
|
||||||
def delete_item_from_pms(plexid):
|
def delete_item_from_pms(plexid):
|
||||||
|
@ -423,24 +761,76 @@ def delete_item_from_pms(plexid):
|
||||||
|
|
||||||
Returns True if successful, False otherwise
|
Returns True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
if downloadutils.DownloadUtils().downloadUrl(
|
if DU().downloadUrl('{server}/library/metadata/%s' % plexid,
|
||||||
'{server}/library/metadata/%s' % plexid,
|
action_type="DELETE") is True:
|
||||||
action_type="DELETE") is True:
|
LOG.info('Successfully deleted Plex id %s from the PMS', plexid)
|
||||||
log.info('Successfully deleted Plex id %s from the PMS' % plexid)
|
|
||||||
return True
|
return True
|
||||||
else:
|
LOG.error('Could not delete Plex id %s from the PMS', plexid)
|
||||||
log.error('Could not delete Plex id %s from the PMS' % plexid)
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_PMS_settings(url, token):
|
def get_PMS_settings(url, token):
|
||||||
"""
|
"""
|
||||||
Retrieve the PMS' settings via <url>/:/
|
Retrieve the PMS' settings via <url>/:/prefs
|
||||||
|
|
||||||
Call with url: scheme://ip:port
|
Call with url: scheme://ip:port
|
||||||
"""
|
"""
|
||||||
return downloadutils.DownloadUtils().downloadUrl(
|
return DU().downloadUrl(
|
||||||
'%s/:/prefs' % url,
|
'%s/:/prefs' % url,
|
||||||
authenticate=False,
|
authenticate=False,
|
||||||
verifySSL=False,
|
verifySSL=False,
|
||||||
headerOptions={'X-Plex-Token': token} if token else None)
|
headerOptions={'X-Plex-Token': token} if token else None)
|
||||||
|
|
||||||
|
|
||||||
|
def GetUserArtworkURL(username):
|
||||||
|
"""
|
||||||
|
Returns the URL for the user's Avatar. Or False if something went
|
||||||
|
wrong.
|
||||||
|
"""
|
||||||
|
users = plex_tv.list_home_users(settings('plexToken'))
|
||||||
|
url = ''
|
||||||
|
# If an error is encountered, set to False
|
||||||
|
if not users:
|
||||||
|
LOG.info("Couldnt get user from plex.tv. No URL for user avatar")
|
||||||
|
return False
|
||||||
|
for user in users:
|
||||||
|
if username in user['title']:
|
||||||
|
url = user['thumb']
|
||||||
|
LOG.debug("Avatar url for user %s is: %s", username, url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def transcode_image_path(key, AuthToken, path, width, height):
|
||||||
|
"""
|
||||||
|
Transcode Image support
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
key
|
||||||
|
AuthToken
|
||||||
|
path - source path of current XML: path[srcXML]
|
||||||
|
width
|
||||||
|
height
|
||||||
|
result:
|
||||||
|
final path to image file
|
||||||
|
"""
|
||||||
|
# external address - can we get a transcoding request for external images?
|
||||||
|
if key.startswith('http://') or key.startswith('https://'):
|
||||||
|
path = key
|
||||||
|
elif key.startswith('/'): # internal full path.
|
||||||
|
path = 'http://127.0.0.1:32400' + key
|
||||||
|
else: # internal path, add-on
|
||||||
|
path = 'http://127.0.0.1:32400' + path + '/' + key
|
||||||
|
path = try_encode(path)
|
||||||
|
# This is bogus (note the extra path component) but ATV is stupid when it
|
||||||
|
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
|
||||||
|
# lenient...
|
||||||
|
transcode_path = ('/photo/:/transcode/%sx%s/%s'
|
||||||
|
% (width, height, quote_plus(path)))
|
||||||
|
args = {
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'url': path
|
||||||
|
}
|
||||||
|
if AuthToken:
|
||||||
|
args['X-Plex-Token'] = AuthToken
|
||||||
|
return transcode_path + '?' + urlencode(args)
|
||||||
|
|
|
@ -1,121 +1,28 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
from json import dumps, loads
|
from Queue import Queue, Empty
|
||||||
import requests
|
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from urllib import quote_plus, unquote
|
from urllib import quote_plus, unquote
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue, Empty
|
from os import makedirs
|
||||||
|
import requests
|
||||||
|
|
||||||
from xbmc import executeJSONRPC, sleep, translatePath
|
from xbmc import sleep, translatePath
|
||||||
from xbmcvfs import exists
|
from xbmcvfs import exists
|
||||||
|
|
||||||
from utils import window, settings, language as lang, kodiSQL, tryEncode, \
|
from utils import window, settings, language as lang, kodi_sql, try_encode, \
|
||||||
thread_methods, dialog, exists_dir, tryDecode
|
thread_methods, dialog, exists_dir, try_decode
|
||||||
|
import state
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
# Disable annoying requests warnings
|
# Disable annoying requests warnings
|
||||||
import requests.packages.urllib3
|
|
||||||
requests.packages.urllib3.disable_warnings()
|
requests.packages.urllib3.disable_warnings()
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
ARTWORK_QUEUE = Queue()
|
ARTWORK_QUEUE = Queue()
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
def setKodiWebServerDetails():
|
|
||||||
"""
|
|
||||||
Get the Kodi webserver details - used to set the texture cache
|
|
||||||
"""
|
|
||||||
xbmc_port = None
|
|
||||||
xbmc_username = None
|
|
||||||
xbmc_password = None
|
|
||||||
web_query = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.GetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserver"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_query))
|
|
||||||
result = loads(result)
|
|
||||||
try:
|
|
||||||
xbmc_webserver_enabled = result['result']['value']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
xbmc_webserver_enabled = False
|
|
||||||
if not xbmc_webserver_enabled:
|
|
||||||
# Enable the webserver, it is disabled
|
|
||||||
web_port = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.SetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserverport",
|
|
||||||
"value": 8080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_port))
|
|
||||||
xbmc_port = 8080
|
|
||||||
web_user = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.SetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserver",
|
|
||||||
"value": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_user))
|
|
||||||
xbmc_username = "kodi"
|
|
||||||
# Webserver already enabled
|
|
||||||
web_port = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.GetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserverport"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_port))
|
|
||||||
result = loads(result)
|
|
||||||
try:
|
|
||||||
xbmc_port = result['result']['value']
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
pass
|
|
||||||
web_user = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.GetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserverusername"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_user))
|
|
||||||
result = loads(result)
|
|
||||||
try:
|
|
||||||
xbmc_username = result['result']['value']
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
pass
|
|
||||||
web_pass = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Settings.GetSettingValue",
|
|
||||||
"params": {
|
|
||||||
"setting": "services.webserverpassword"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = executeJSONRPC(dumps(web_pass))
|
|
||||||
result = loads(result)
|
|
||||||
try:
|
|
||||||
xbmc_password = result['result']['value']
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
return (xbmc_port, xbmc_username, xbmc_password)
|
|
||||||
|
|
||||||
|
|
||||||
def double_urlencode(text):
|
def double_urlencode(text):
|
||||||
|
@ -130,8 +37,6 @@ def double_urldecode(text):
|
||||||
'DB_SCAN',
|
'DB_SCAN',
|
||||||
'STOP_SYNC'])
|
'STOP_SYNC'])
|
||||||
class Image_Cache_Thread(Thread):
|
class Image_Cache_Thread(Thread):
|
||||||
xbmc_host = 'localhost'
|
|
||||||
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()
|
|
||||||
sleep_between = 50
|
sleep_between = 50
|
||||||
# Potentially issues with limited number of threads
|
# Potentially issues with limited number of threads
|
||||||
# Hence let Kodi wait till download is successful
|
# Hence let Kodi wait till download is successful
|
||||||
|
@ -142,17 +47,17 @@ class Image_Cache_Thread(Thread):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
queue = self.queue
|
queue = self.queue
|
||||||
sleep_between = self.sleep_between
|
sleep_between = self.sleep_between
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
# Set in service.py
|
# Set in service.py
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
log.info("---===### Stopped Image_Cache_Thread ###===---")
|
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
|
||||||
return
|
return
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
|
@ -165,43 +70,45 @@ class Image_Cache_Thread(Thread):
|
||||||
try:
|
try:
|
||||||
requests.head(
|
requests.head(
|
||||||
url="http://%s:%s/image/image://%s"
|
url="http://%s:%s/image/image://%s"
|
||||||
% (self.xbmc_host, self.xbmc_port, url),
|
% (state.WEBSERVER_HOST,
|
||||||
auth=(self.xbmc_username, self.xbmc_password),
|
state.WEBSERVER_PORT,
|
||||||
|
url),
|
||||||
|
auth=(state.WEBSERVER_USERNAME,
|
||||||
|
state.WEBSERVER_PASSWORD),
|
||||||
timeout=self.timeout)
|
timeout=self.timeout)
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
# We don't need the result, only trigger Kodi to start the
|
# We don't need the result, only trigger Kodi to start the
|
||||||
# download. All is well
|
# download. All is well
|
||||||
break
|
break
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
# Kodi terminated
|
# Kodi terminated
|
||||||
break
|
break
|
||||||
# Server thinks its a DOS attack, ('error 10053')
|
# Server thinks its a DOS attack, ('error 10053')
|
||||||
# Wait before trying again
|
# Wait before trying again
|
||||||
if sleeptime > 5:
|
if sleeptime > 5:
|
||||||
log.error('Repeatedly got ConnectionError for url %s'
|
LOG.error('Repeatedly got ConnectionError for url %s',
|
||||||
% double_urldecode(url))
|
double_urldecode(url))
|
||||||
break
|
break
|
||||||
log.debug('Were trying too hard to download art, server '
|
LOG.debug('Were trying too hard to download art, server '
|
||||||
'over-loaded. Sleep %s seconds before trying '
|
'over-loaded. Sleep %s seconds before trying '
|
||||||
'again to download %s'
|
'again to download %s',
|
||||||
% (2**sleeptime, double_urldecode(url)))
|
2**sleeptime, double_urldecode(url))
|
||||||
sleep((2**sleeptime)*1000)
|
sleep((2**sleeptime)*1000)
|
||||||
sleeptime += 1
|
sleeptime += 1
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Unknown exception for url %s: %s'
|
LOG.error('Unknown exception for url %s: %s'.
|
||||||
% (double_urldecode(url), e))
|
double_urldecode(url), e)
|
||||||
import traceback
|
import traceback
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
LOG.error("Traceback:\n%s", traceback.format_exc())
|
||||||
break
|
break
|
||||||
# We did not even get a timeout
|
# We did not even get a timeout
|
||||||
break
|
break
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
log.debug('Cached art: %s' % double_urldecode(url))
|
|
||||||
# Sleep for a bit to reduce CPU strain
|
# Sleep for a bit to reduce CPU strain
|
||||||
sleep(sleep_between)
|
sleep(sleep_between)
|
||||||
log.info("---===### Stopped Image_Cache_Thread ###===---")
|
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
|
||||||
|
|
||||||
|
|
||||||
class Artwork():
|
class Artwork():
|
||||||
|
@ -217,18 +124,19 @@ class Artwork():
|
||||||
if not dialog('yesno', "Image Texture Cache", lang(39250)):
|
if not dialog('yesno', "Image Texture Cache", lang(39250)):
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("Doing Image Cache Sync")
|
LOG.info("Doing Image Cache Sync")
|
||||||
|
|
||||||
# ask to rest all existing or not
|
# ask to rest all existing or not
|
||||||
if dialog('yesno', "Image Texture Cache", lang(39251)):
|
if dialog('yesno', "Image Texture Cache", lang(39251)):
|
||||||
log.info("Resetting all cache data first")
|
LOG.info("Resetting all cache data first")
|
||||||
# Remove all existing textures first
|
# Remove all existing textures first
|
||||||
path = tryDecode(translatePath("special://thumbnails/"))
|
path = try_decode(translatePath("special://thumbnails/"))
|
||||||
if exists_dir(path):
|
if exists_dir(path):
|
||||||
rmtree(path, ignore_errors=True)
|
rmtree(path, ignore_errors=True)
|
||||||
|
self.restore_cache_directories()
|
||||||
|
|
||||||
# remove all existing data from texture DB
|
# remove all existing data from texture DB
|
||||||
connection = kodiSQL('texture')
|
connection = kodi_sql('texture')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
|
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
|
||||||
cursor.execute(query, ('table', ))
|
cursor.execute(query, ('table', ))
|
||||||
|
@ -241,191 +149,120 @@ class Artwork():
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
# Cache all entries in video DB
|
# Cache all entries in video DB
|
||||||
connection = kodiSQL('video')
|
connection = kodi_sql('video')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
# dont include actors
|
# dont include actors
|
||||||
query = "SELECT url FROM art WHERE media_type != ?"
|
query = "SELECT url FROM art WHERE media_type != ?"
|
||||||
cursor.execute(query, ('actor', ))
|
cursor.execute(query, ('actor', ))
|
||||||
result = cursor.fetchall()
|
result = cursor.fetchall()
|
||||||
total = len(result)
|
total = len(result)
|
||||||
log.info("Image cache sync about to process %s video images" % total)
|
LOG.info("Image cache sync about to process %s video images", total)
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
for url in result:
|
for url in result:
|
||||||
self.cacheTexture(url[0])
|
self.cache_texture(url[0])
|
||||||
# Cache all entries in music DB
|
# Cache all entries in music DB
|
||||||
connection = kodiSQL('music')
|
connection = kodi_sql('music')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute("SELECT url FROM art")
|
cursor.execute("SELECT url FROM art")
|
||||||
result = cursor.fetchall()
|
result = cursor.fetchall()
|
||||||
total = len(result)
|
total = len(result)
|
||||||
log.info("Image cache sync about to process %s music images" % total)
|
LOG.info("Image cache sync about to process %s music images", total)
|
||||||
connection.close()
|
connection.close()
|
||||||
for url in result:
|
for url in result:
|
||||||
self.cacheTexture(url[0])
|
self.cache_texture(url[0])
|
||||||
|
|
||||||
def cacheTexture(self, url):
|
def cache_texture(self, url):
|
||||||
# Cache a single image url to the texture cache
|
'''
|
||||||
|
Cache a single image url to the texture cache
|
||||||
|
'''
|
||||||
if url and self.enableTextureCache:
|
if url and self.enableTextureCache:
|
||||||
self.queue.put(double_urlencode(tryEncode(url)))
|
self.queue.put(double_urlencode(try_encode(url)))
|
||||||
|
|
||||||
def addArtwork(self, artwork, kodiId, mediaType, cursor):
|
def modify_artwork(self, artworks, kodi_id, kodi_type, cursor):
|
||||||
# Kodi conversion table
|
"""
|
||||||
kodiart = {
|
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||||
'Primary': ["thumb", "poster"],
|
"""
|
||||||
'Banner': "banner",
|
for kodi_art, url in artworks.iteritems():
|
||||||
'Logo': "clearlogo",
|
self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor)
|
||||||
'Art': "clearart",
|
|
||||||
'Thumb': "landscape",
|
|
||||||
'Disc': "discart",
|
|
||||||
'Backdrop': "fanart",
|
|
||||||
'BoxRear': "poster"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Artwork is a dictionary
|
def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor):
|
||||||
for art in artwork:
|
"""
|
||||||
if art == "Backdrop":
|
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
|
||||||
# Backdrop entry is a list
|
Kodi art table for item kodi_id/kodi_type. Will also cache everything
|
||||||
# Process extra fanart for artwork downloader (fanart, fanart1,
|
except actor portraits.
|
||||||
# fanart2...)
|
"""
|
||||||
backdrops = artwork[art]
|
query = '''
|
||||||
backdropsNumber = len(backdrops)
|
SELECT url FROM art
|
||||||
|
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||||
query = ' '.join((
|
LIMIT 1
|
||||||
"SELECT url",
|
'''
|
||||||
"FROM art",
|
cursor.execute(query, (kodi_id, kodi_type, kodi_art,))
|
||||||
"WHERE media_id = ?",
|
|
||||||
"AND media_type = ?",
|
|
||||||
"AND type LIKE ?"
|
|
||||||
))
|
|
||||||
cursor.execute(query, (kodiId, mediaType, "fanart%",))
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
|
|
||||||
if len(rows) > backdropsNumber:
|
|
||||||
# More backdrops in database. Delete extra fanart.
|
|
||||||
query = ' '.join((
|
|
||||||
"DELETE FROM art",
|
|
||||||
"WHERE media_id = ?",
|
|
||||||
"AND media_type = ?",
|
|
||||||
"AND type LIKE ?"
|
|
||||||
))
|
|
||||||
cursor.execute(query, (kodiId, mediaType, "fanart_",))
|
|
||||||
|
|
||||||
# Process backdrops and extra fanart
|
|
||||||
index = ""
|
|
||||||
for backdrop in backdrops:
|
|
||||||
self.addOrUpdateArt(
|
|
||||||
imageUrl=backdrop,
|
|
||||||
kodiId=kodiId,
|
|
||||||
mediaType=mediaType,
|
|
||||||
imageType="%s%s" % ("fanart", index),
|
|
||||||
cursor=cursor)
|
|
||||||
|
|
||||||
if backdropsNumber > 1:
|
|
||||||
try: # Will only fail on the first try, str to int.
|
|
||||||
index += 1
|
|
||||||
except TypeError:
|
|
||||||
index = 1
|
|
||||||
|
|
||||||
elif art == "Primary":
|
|
||||||
# Primary art is processed as thumb and poster for Kodi.
|
|
||||||
for artType in kodiart[art]:
|
|
||||||
self.addOrUpdateArt(
|
|
||||||
imageUrl=artwork[art],
|
|
||||||
kodiId=kodiId,
|
|
||||||
mediaType=mediaType,
|
|
||||||
imageType=artType,
|
|
||||||
cursor=cursor)
|
|
||||||
|
|
||||||
elif kodiart.get(art):
|
|
||||||
# Process the rest artwork type that Kodi can use
|
|
||||||
self.addOrUpdateArt(
|
|
||||||
imageUrl=artwork[art],
|
|
||||||
kodiId=kodiId,
|
|
||||||
mediaType=mediaType,
|
|
||||||
imageType=kodiart[art],
|
|
||||||
cursor=cursor)
|
|
||||||
|
|
||||||
def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor):
|
|
||||||
if not imageUrl:
|
|
||||||
# Possible that the imageurl is an empty string
|
|
||||||
return
|
|
||||||
|
|
||||||
query = ' '.join((
|
|
||||||
"SELECT url",
|
|
||||||
"FROM art",
|
|
||||||
"WHERE media_id = ?",
|
|
||||||
"AND media_type = ?",
|
|
||||||
"AND type = ?"
|
|
||||||
))
|
|
||||||
cursor.execute(query, (kodiId, mediaType, imageType,))
|
|
||||||
try:
|
try:
|
||||||
# Update the artwork
|
# Update the artwork
|
||||||
url = cursor.fetchone()[0]
|
old_url = cursor.fetchone()[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Add the artwork
|
# Add the artwork
|
||||||
log.debug("Adding Art Link for kodiId: %s (%s)"
|
LOG.debug('Adding Art Link for %s kodi_id %s, kodi_type %s: %s',
|
||||||
% (kodiId, imageUrl))
|
kodi_art, kodi_id, kodi_type, url)
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT INTO art(media_id, media_type, type, url)
|
INSERT INTO art(media_id, media_type, type, url)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
cursor.execute(query, (kodi_id, kodi_type, kodi_art, url))
|
||||||
cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
|
|
||||||
else:
|
else:
|
||||||
if url == imageUrl:
|
if url == old_url:
|
||||||
# Only cache artwork if it changed
|
# Only cache artwork if it changed
|
||||||
return
|
return
|
||||||
# Only for the main backdrop, poster
|
self.delete_cached_artwork(old_url)
|
||||||
if (window('plex_initialScan') != "true" and
|
LOG.debug("Updating Art url for %s kodi_id %s, kodi_type %s to %s",
|
||||||
imageType in ("fanart", "poster")):
|
kodi_art, kodi_id, kodi_type, url)
|
||||||
# Delete current entry before updating with the new one
|
query = '''
|
||||||
self.deleteCachedArtwork(url)
|
UPDATE art SET url = ?
|
||||||
log.debug("Updating Art url for %s kodiId %s %s -> (%s)"
|
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||||
% (imageType, kodiId, url, imageUrl))
|
'''
|
||||||
query = ' '.join((
|
cursor.execute(query, (url, kodi_id, kodi_type, kodi_art))
|
||||||
"UPDATE art",
|
|
||||||
"SET url = ?",
|
|
||||||
"WHERE media_id = ?",
|
|
||||||
"AND media_type = ?",
|
|
||||||
"AND type = ?"
|
|
||||||
))
|
|
||||||
cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
|
|
||||||
|
|
||||||
# Cache fanart and poster in Kodi texture cache
|
# Cache fanart and poster in Kodi texture cache
|
||||||
if mediaType != 'actor':
|
if kodi_type != 'actor':
|
||||||
self.cacheTexture(imageUrl)
|
self.cache_texture(url)
|
||||||
|
|
||||||
def deleteArtwork(self, kodiId, mediaType, cursor):
|
def delete_artwork(self, kodiId, mediaType, cursor):
|
||||||
query = ' '.join((
|
query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?'
|
||||||
"SELECT url",
|
|
||||||
"FROM art",
|
|
||||||
"WHERE media_id = ?",
|
|
||||||
"AND media_type = ?"
|
|
||||||
))
|
|
||||||
cursor.execute(query, (kodiId, mediaType,))
|
cursor.execute(query, (kodiId, mediaType,))
|
||||||
rows = cursor.fetchall()
|
for row in cursor.fetchall():
|
||||||
for row in rows:
|
self.delete_cached_artwork(row[0])
|
||||||
self.deleteCachedArtwork(row[0])
|
|
||||||
|
|
||||||
def deleteCachedArtwork(self, url):
|
@staticmethod
|
||||||
# Only necessary to remove and apply a new backdrop or poster
|
def delete_cached_artwork(url):
|
||||||
connection = kodiSQL('texture')
|
"""
|
||||||
|
Deleted the cached artwork with path url (if it exists)
|
||||||
|
"""
|
||||||
|
connection = kodi_sql('texture')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT cachedurl FROM texture WHERE url = ?",
|
cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1",
|
||||||
(url,))
|
(url,))
|
||||||
cachedurl = cursor.fetchone()[0]
|
cachedurl = cursor.fetchone()[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.info("Could not find cached url.")
|
# Could not find cached url
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
# Delete thumbnail as well as the entry
|
# Delete thumbnail as well as the entry
|
||||||
path = translatePath("special://thumbnails/%s" % cachedurl)
|
path = translatePath("special://thumbnails/%s" % cachedurl)
|
||||||
log.debug("Deleting cached thumbnail: %s" % path)
|
LOG.debug("Deleting cached thumbnail: %s", path)
|
||||||
if exists(path):
|
if exists(path):
|
||||||
rmtree(tryDecode(path), ignore_errors=True)
|
rmtree(try_decode(path), ignore_errors=True)
|
||||||
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
|
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
|
||||||
connection.commit()
|
connection.commit()
|
||||||
finally:
|
finally:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def restore_cache_directories():
|
||||||
|
LOG.info("Restoring cache directories...")
|
||||||
|
paths = ("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||||
|
"a", "b", "c", "d", "e", "f",
|
||||||
|
"Video", "plex")
|
||||||
|
for path in paths:
|
||||||
|
makedirs(try_decode(translatePath("special://thumbnails/%s"
|
||||||
|
% path)))
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
|
|
||||||
from utils import window, settings
|
from utils import window, settings
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def getXArgsDeviceInfo(options=None):
|
def getXArgsDeviceInfo(options=None, include_token=True):
|
||||||
"""
|
"""
|
||||||
Returns a dictionary that can be used as headers for GET and POST
|
Returns a dictionary that can be used as headers for GET and POST
|
||||||
requests. An authentication option is NOT yet added.
|
requests. An authentication option is NOT yet added.
|
||||||
|
@ -21,6 +21,8 @@ def getXArgsDeviceInfo(options=None):
|
||||||
Inputs:
|
Inputs:
|
||||||
options: dictionary of options that will override the
|
options: dictionary of options that will override the
|
||||||
standard header options otherwise set.
|
standard header options otherwise set.
|
||||||
|
include_token: set to False if you don't want to include the Plex token
|
||||||
|
(e.g. for Companion communication)
|
||||||
Output:
|
Output:
|
||||||
header dictionary
|
header dictionary
|
||||||
"""
|
"""
|
||||||
|
@ -41,7 +43,7 @@ def getXArgsDeviceInfo(options=None):
|
||||||
'X-Plex-Client-Identifier': getDeviceId(),
|
'X-Plex-Client-Identifier': getDeviceId(),
|
||||||
'X-Plex-Provides': 'client,controller,player,pubsub-player',
|
'X-Plex-Provides': 'client,controller,player,pubsub-player',
|
||||||
}
|
}
|
||||||
if window('pms_token'):
|
if include_token and window('pms_token'):
|
||||||
xargs['X-Plex-Token'] = window('pms_token')
|
xargs['X-Plex-Token'] = window('pms_token')
|
||||||
if options is not None:
|
if options is not None:
|
||||||
xargs.update(options)
|
xargs.update(options)
|
||||||
|
@ -57,24 +59,27 @@ def getDeviceId(reset=False):
|
||||||
If id does not exist, create one and save in Kodi settings file.
|
If id does not exist, create one and save in Kodi settings file.
|
||||||
"""
|
"""
|
||||||
if reset is True:
|
if reset is True:
|
||||||
|
v.PKC_MACHINE_IDENTIFIER = None
|
||||||
window('plex_client_Id', clear=True)
|
window('plex_client_Id', clear=True)
|
||||||
settings('plex_client_Id', value="")
|
settings('plex_client_Id', value="")
|
||||||
|
|
||||||
clientId = window('plex_client_Id')
|
client_id = v.PKC_MACHINE_IDENTIFIER
|
||||||
if clientId:
|
if client_id:
|
||||||
return clientId
|
return client_id
|
||||||
|
|
||||||
clientId = settings('plex_client_Id')
|
client_id = settings('plex_client_Id')
|
||||||
# Because Kodi appears to cache file settings!!
|
# Because Kodi appears to cache file settings!!
|
||||||
if clientId != "" and reset is False:
|
if client_id != "" and reset is False:
|
||||||
window('plex_client_Id', value=clientId)
|
v.PKC_MACHINE_IDENTIFIER = client_id
|
||||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
window('plex_client_Id', value=client_id)
|
||||||
return clientId
|
log.info("Unique device Id plex_client_Id loaded: %s", client_id)
|
||||||
|
return client_id
|
||||||
|
|
||||||
log.info("Generating a new deviceid.")
|
log.info("Generating a new deviceid.")
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
clientId = str(uuid4())
|
client_id = str(uuid4())
|
||||||
settings('plex_client_Id', value=clientId)
|
settings('plex_client_Id', value=client_id)
|
||||||
window('plex_client_Id', value=clientId)
|
v.PKC_MACHINE_IDENTIFIER = client_id
|
||||||
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
|
window('plex_client_Id', value=client_id)
|
||||||
return clientId
|
log.info("Unique device Id plex_client_Id generated: %s", client_id)
|
||||||
|
return client_id
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
import logging
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
from xbmc import sleep
|
from xbmc import sleep
|
||||||
|
|
||||||
|
@ -10,8 +9,7 @@ from utils import window, thread_methods
|
||||||
import state
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = logging.getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,17 +21,11 @@ class Monitor_Window(Thread):
|
||||||
|
|
||||||
Adjusts state.py accordingly
|
Adjusts state.py accordingly
|
||||||
"""
|
"""
|
||||||
# Borg - multiple instances, shared state
|
|
||||||
def __init__(self, callback=None):
|
|
||||||
self.mgr = callback
|
|
||||||
self.playback_queue = Queue()
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
queue = self.playback_queue
|
queue = state.COMMAND_PIPELINE_QUEUE
|
||||||
log.info("----===## Starting Kodi_Play_Client ##===----")
|
LOG.info("----===## Starting Kodi_Play_Client ##===----")
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
if window('plex_command'):
|
if window('plex_command'):
|
||||||
value = window('plex_command')
|
value = window('plex_command')
|
||||||
window('plex_command', clear=True)
|
window('plex_command', clear=True)
|
||||||
|
@ -62,12 +54,15 @@ class Monitor_Window(Thread):
|
||||||
value.replace('PLEX_USERNAME-', '') or None
|
value.replace('PLEX_USERNAME-', '') or None
|
||||||
elif value.startswith('RUN_LIB_SCAN-'):
|
elif value.startswith('RUN_LIB_SCAN-'):
|
||||||
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
||||||
elif value == 'CONTEXT_menu':
|
elif value.startswith('CONTEXT_menu?'):
|
||||||
queue.put('dummy?mode=context_menu')
|
queue.put('dummy?mode=context_menu&%s'
|
||||||
|
% value.replace('CONTEXT_menu?', ''))
|
||||||
|
elif value.startswith('NAVIGATE'):
|
||||||
|
queue.put(value.replace('NAVIGATE-', ''))
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('%s not implemented' % value)
|
raise NotImplementedError('%s not implemented' % value)
|
||||||
else:
|
else:
|
||||||
sleep(50)
|
sleep(50)
|
||||||
# Put one last item into the queue to let playback_starter end
|
# Put one last item into the queue to let playback_starter end
|
||||||
queue.put(None)
|
queue.put(None)
|
||||||
log.info("----===## Kodi_Play_Client stopped ##===----")
|
LOG.info("----===## Kodi_Play_Client stopped ##===----")
|
||||||
|
|
|
@ -1,192 +1,121 @@
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
import logging
|
Processes Plex companion inputs from the plexbmchelper to Kodi commands
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from xbmc import Player
|
from xbmc import Player
|
||||||
|
|
||||||
from utils import JSONRPC
|
|
||||||
from variables import ALEXA_TO_COMPANION
|
from variables import ALEXA_TO_COMPANION
|
||||||
from playqueue import Playqueue
|
import playqueue as PQ
|
||||||
from PlexFunctions import GetPlexKeyNumber
|
from PlexFunctions import GetPlexKeyNumber
|
||||||
|
import json_rpc as js
|
||||||
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def getPlayers():
|
def skip_to(params):
|
||||||
info = JSONRPC("Player.GetActivePlayers").execute()['result'] or []
|
|
||||||
ret = {}
|
|
||||||
for player in info:
|
|
||||||
player['playerid'] = int(player['playerid'])
|
|
||||||
ret[player['type']] = player
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def getPlayerIds():
|
|
||||||
ret = []
|
|
||||||
for player in getPlayers().values():
|
|
||||||
ret.append(player['playerid'])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def getPlaylistId(typus):
|
|
||||||
"""
|
"""
|
||||||
typus: one of the Kodi types, e.g. audio or video
|
Skip to a specific playlist position.
|
||||||
|
|
||||||
Returns None if nothing was found
|
Does not seem to be implemented yet by Plex!
|
||||||
"""
|
"""
|
||||||
for playlist in getPlaylists():
|
playqueue_item_id = params.get('playQueueItemID')
|
||||||
if playlist.get('type') == typus:
|
_, plex_id = GetPlexKeyNumber(params.get('key'))
|
||||||
return playlist.get('playlistid')
|
LOG.debug('Skipping to playQueueItemID %s, plex_id %s',
|
||||||
|
playqueue_item_id, plex_id)
|
||||||
|
|
||||||
def getPlaylists():
|
|
||||||
"""
|
|
||||||
Returns a list, e.g.
|
|
||||||
[
|
|
||||||
{u'playlistid': 0, u'type': u'audio'},
|
|
||||||
{u'playlistid': 1, u'type': u'video'},
|
|
||||||
{u'playlistid': 2, u'type': u'picture'}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
return JSONRPC('Playlist.GetPlaylists').execute()
|
|
||||||
|
|
||||||
|
|
||||||
def millisToTime(t):
|
|
||||||
millis = int(t)
|
|
||||||
seconds = millis / 1000
|
|
||||||
minutes = seconds / 60
|
|
||||||
hours = minutes / 60
|
|
||||||
seconds = seconds % 60
|
|
||||||
minutes = minutes % 60
|
|
||||||
millis = millis % 1000
|
|
||||||
return {'hours': hours,
|
|
||||||
'minutes': minutes,
|
|
||||||
'seconds': seconds,
|
|
||||||
'milliseconds': millis}
|
|
||||||
|
|
||||||
|
|
||||||
def skipTo(params):
|
|
||||||
# Does not seem to be implemented yet
|
|
||||||
playQueueItemID = params.get('playQueueItemID', 'not available')
|
|
||||||
library, plex_id = GetPlexKeyNumber(params.get('key'))
|
|
||||||
log.debug('Skipping to playQueueItemID %s, plex_id %s'
|
|
||||||
% (playQueueItemID, plex_id))
|
|
||||||
found = True
|
found = True
|
||||||
playqueues = Playqueue()
|
for player in js.get_players().values():
|
||||||
for (player, ID) in getPlayers().iteritems():
|
playqueue = PQ.PLAYQUEUES[player['playerid']]
|
||||||
playqueue = playqueues.get_playqueue_from_type(player)
|
|
||||||
for i, item in enumerate(playqueue.items):
|
for i, item in enumerate(playqueue.items):
|
||||||
if item.ID == playQueueItemID or item.plex_id == plex_id:
|
if item.id == playqueue_item_id:
|
||||||
|
found = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
log.debug('Item not found to skip to')
|
for i, item in enumerate(playqueue.items):
|
||||||
found = False
|
if item.plex_id == plex_id:
|
||||||
if found:
|
found = True
|
||||||
|
break
|
||||||
|
if found is True:
|
||||||
Player().play(playqueue.kodi_pl, None, False, i)
|
Player().play(playqueue.kodi_pl, None, False, i)
|
||||||
|
else:
|
||||||
|
LOG.error('Item not found to skip to')
|
||||||
|
|
||||||
|
|
||||||
def convert_alexa_to_companion(dictionary):
|
def convert_alexa_to_companion(dictionary):
|
||||||
|
"""
|
||||||
|
The params passed by Alexa must first be converted to Companion talk
|
||||||
|
"""
|
||||||
for key in dictionary:
|
for key in dictionary:
|
||||||
if key in ALEXA_TO_COMPANION:
|
if key in ALEXA_TO_COMPANION:
|
||||||
dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key]
|
dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key]
|
||||||
del dictionary[key]
|
del dictionary[key]
|
||||||
|
|
||||||
|
|
||||||
def process_command(request_path, params, queue=None):
|
def process_command(request_path, params):
|
||||||
"""
|
"""
|
||||||
queue: Queue() of PlexCompanion.py
|
queue: Queue() of PlexCompanion.py
|
||||||
"""
|
"""
|
||||||
if params.get('deviceName') == 'Alexa':
|
if params.get('deviceName') == 'Alexa':
|
||||||
convert_alexa_to_companion(params)
|
convert_alexa_to_companion(params)
|
||||||
log.debug('Received request_path: %s, params: %s' % (request_path, params))
|
LOG.debug('Received request_path: %s, params: %s', request_path, params)
|
||||||
if "/playMedia" in request_path:
|
if request_path == 'player/playback/playMedia':
|
||||||
# We need to tell service.py
|
# We need to tell service.py
|
||||||
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
||||||
queue.put({
|
state.COMPANION_QUEUE.put({
|
||||||
'action': action,
|
'action': action,
|
||||||
'data': params
|
'data': params
|
||||||
})
|
})
|
||||||
|
|
||||||
elif request_path == 'player/playback/refreshPlayQueue':
|
elif request_path == 'player/playback/refreshPlayQueue':
|
||||||
queue.put({
|
state.COMPANION_QUEUE.put({
|
||||||
'action': 'refreshPlayQueue',
|
'action': 'refreshPlayQueue',
|
||||||
'data': params
|
'data': params
|
||||||
})
|
})
|
||||||
|
|
||||||
elif request_path == "player/playback/setParameters":
|
elif request_path == "player/playback/setParameters":
|
||||||
if 'volume' in params:
|
if 'volume' in params:
|
||||||
volume = int(params['volume'])
|
js.set_volume(int(params['volume']))
|
||||||
log.debug("Adjusting the volume to %s" % volume)
|
|
||||||
JSONRPC('Application.SetVolume').execute({"volume": volume})
|
|
||||||
else:
|
else:
|
||||||
log.error('Unknown parameters: %s' % params)
|
LOG.error('Unknown parameters: %s', params)
|
||||||
|
|
||||||
elif request_path == "player/playback/play":
|
elif request_path == "player/playback/play":
|
||||||
for playerid in getPlayerIds():
|
js.play()
|
||||||
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
|
|
||||||
"play": True})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/pause":
|
elif request_path == "player/playback/pause":
|
||||||
for playerid in getPlayerIds():
|
js.pause()
|
||||||
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
|
|
||||||
"play": False})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/stop":
|
elif request_path == "player/playback/stop":
|
||||||
for playerid in getPlayerIds():
|
js.stop()
|
||||||
JSONRPC("Player.Stop").execute({"playerid": playerid})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/seekTo":
|
elif request_path == "player/playback/seekTo":
|
||||||
for playerid in getPlayerIds():
|
js.seek_to(int(params.get('offset', 0)))
|
||||||
JSONRPC("Player.Seek").execute(
|
|
||||||
{"playerid": playerid,
|
|
||||||
"value": millisToTime(params.get('offset', 0))})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/stepForward":
|
elif request_path == "player/playback/stepForward":
|
||||||
for playerid in getPlayerIds():
|
js.smallforward()
|
||||||
JSONRPC("Player.Seek").execute({"playerid": playerid,
|
|
||||||
"value": "smallforward"})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/stepBack":
|
elif request_path == "player/playback/stepBack":
|
||||||
for playerid in getPlayerIds():
|
js.smallbackward()
|
||||||
JSONRPC("Player.Seek").execute({"playerid": playerid,
|
|
||||||
"value": "smallbackward"})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/skipNext":
|
elif request_path == "player/playback/skipNext":
|
||||||
for playerid in getPlayerIds():
|
js.skipnext()
|
||||||
JSONRPC("Player.GoTo").execute({"playerid": playerid,
|
|
||||||
"to": "next"})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/skipPrevious":
|
elif request_path == "player/playback/skipPrevious":
|
||||||
for playerid in getPlayerIds():
|
js.skipprevious()
|
||||||
JSONRPC("Player.GoTo").execute({"playerid": playerid,
|
|
||||||
"to": "previous"})
|
|
||||||
|
|
||||||
elif request_path == "player/playback/skipTo":
|
elif request_path == "player/playback/skipTo":
|
||||||
skipTo(params)
|
skip_to(params)
|
||||||
|
|
||||||
elif request_path == "player/navigation/moveUp":
|
elif request_path == "player/navigation/moveUp":
|
||||||
JSONRPC("Input.Up").execute()
|
js.input_up()
|
||||||
|
|
||||||
elif request_path == "player/navigation/moveDown":
|
elif request_path == "player/navigation/moveDown":
|
||||||
JSONRPC("Input.Down").execute()
|
js.input_down()
|
||||||
|
|
||||||
elif request_path == "player/navigation/moveLeft":
|
elif request_path == "player/navigation/moveLeft":
|
||||||
JSONRPC("Input.Left").execute()
|
js.input_left()
|
||||||
|
|
||||||
elif request_path == "player/navigation/moveRight":
|
elif request_path == "player/navigation/moveRight":
|
||||||
JSONRPC("Input.Right").execute()
|
js.input_right()
|
||||||
|
|
||||||
elif request_path == "player/navigation/select":
|
elif request_path == "player/navigation/select":
|
||||||
JSONRPC("Input.Select").execute()
|
js.input_select()
|
||||||
|
|
||||||
elif request_path == "player/navigation/home":
|
elif request_path == "player/navigation/home":
|
||||||
JSONRPC("Input.Home").execute()
|
js.input_home()
|
||||||
|
|
||||||
elif request_path == "player/navigation/back":
|
elif request_path == "player/navigation/back":
|
||||||
JSONRPC("Input.Back").execute()
|
js.input_back()
|
||||||
|
elif request_path == "player/playback/setStreams":
|
||||||
|
state.COMPANION_QUEUE.put({
|
||||||
|
'action': 'setStreams',
|
||||||
|
'data': params
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
log.error('Unknown request path: %s' % request_path)
|
LOG.error('Unknown request path: %s', request_path)
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
import logging
|
from xbmc import getInfoLabel, sleep, executebuiltin
|
||||||
|
from xbmcaddon import Addon
|
||||||
import xbmc
|
|
||||||
import xbmcaddon
|
|
||||||
|
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
from utils import window, settings, dialog, language as lang, kodiSQL
|
from utils import window, settings, dialog, language as lang
|
||||||
from dialogs import context
|
from dialogs import context
|
||||||
from PlexFunctions import delete_item_from_pms
|
from PlexFunctions import delete_item_from_pms
|
||||||
|
import playqueue as PQ
|
||||||
import variables as v
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
OPTIONS = {
|
OPTIONS = {
|
||||||
'Refresh': lang(30410),
|
'Refresh': lang(30410),
|
||||||
|
@ -32,81 +32,67 @@ OPTIONS = {
|
||||||
|
|
||||||
|
|
||||||
class ContextMenu(object):
|
class ContextMenu(object):
|
||||||
|
"""
|
||||||
|
Class initiated if user opens "Plex options" on a PLEX item using the Kodi
|
||||||
|
context menu
|
||||||
|
"""
|
||||||
_selected_option = None
|
_selected_option = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, kodi_id=None, kodi_type=None):
|
||||||
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8')
|
"""
|
||||||
self.item_type = self._get_item_type()
|
Simply instantiate with ContextMenu() - no need to call any methods
|
||||||
self.item_id = self._get_item_id(self.kodi_id, self.item_type)
|
"""
|
||||||
|
self.kodi_id = kodi_id
|
||||||
log.info("Found item_id: %s item_type: %s"
|
self.kodi_type = kodi_type
|
||||||
% (self.item_id, self.item_type))
|
self.plex_id = self._get_plex_id(self.kodi_id, self.kodi_type)
|
||||||
|
if self.kodi_type:
|
||||||
if not self.item_id:
|
self.plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[self.kodi_type]
|
||||||
|
else:
|
||||||
|
self.plex_type = None
|
||||||
|
LOG.debug("Found plex_id: %s plex_type: %s",
|
||||||
|
self.plex_id, self.plex_type)
|
||||||
|
if not self.plex_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._select_menu():
|
if self._select_menu():
|
||||||
self._action_menu()
|
self._action_menu()
|
||||||
|
|
||||||
if self._selected_option in (OPTIONS['Delete'],
|
if self._selected_option in (OPTIONS['Delete'],
|
||||||
OPTIONS['Refresh']):
|
OPTIONS['Refresh']):
|
||||||
log.info("refreshing container")
|
LOG.info("refreshing container")
|
||||||
xbmc.sleep(500)
|
sleep(500)
|
||||||
xbmc.executebuiltin('Container.Refresh')
|
executebuiltin('Container.Refresh')
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def _get_item_type(cls):
|
def _get_plex_id(kodi_id, kodi_type):
|
||||||
item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8')
|
plex_id = getInfoLabel('ListItem.Property(plexid)') or None
|
||||||
if not item_type:
|
if not plex_id and kodi_id and kodi_type:
|
||||||
if xbmc.getCondVisibility('Container.Content(albums)'):
|
|
||||||
item_type = "album"
|
|
||||||
elif xbmc.getCondVisibility('Container.Content(artists)'):
|
|
||||||
item_type = "artist"
|
|
||||||
elif xbmc.getCondVisibility('Container.Content(songs)'):
|
|
||||||
item_type = "song"
|
|
||||||
elif xbmc.getCondVisibility('Container.Content(pictures)'):
|
|
||||||
item_type = "picture"
|
|
||||||
else:
|
|
||||||
log.info("item_type is unknown")
|
|
||||||
return item_type
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_item_id(cls, kodi_id, item_type):
|
|
||||||
item_id = xbmc.getInfoLabel('ListItem.Property(plexid)')
|
|
||||||
if not item_id and kodi_id and item_type:
|
|
||||||
with plexdb.Get_Plex_DB() as plexcursor:
|
with plexdb.Get_Plex_DB() as plexcursor:
|
||||||
item = plexcursor.getItem_byKodiId(kodi_id, item_type)
|
item = plexcursor.getItem_byKodiId(kodi_id, kodi_type)
|
||||||
try:
|
try:
|
||||||
item_id = item[0]
|
plex_id = item[0]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error('Could not get the Plex id for context menu')
|
LOG.info('Could not get the Plex id for context menu')
|
||||||
return item_id
|
return plex_id
|
||||||
|
|
||||||
def _select_menu(self):
|
def _select_menu(self):
|
||||||
# Display select dialog
|
"""
|
||||||
|
Display select dialog
|
||||||
|
"""
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
# if user uses direct paths, give option to initiate playback via PMS
|
# if user uses direct paths, give option to initiate playback via PMS
|
||||||
if (window('useDirectPaths') == 'true' and
|
if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES:
|
||||||
self.item_type in v.KODI_VIDEOTYPES):
|
|
||||||
options.append(OPTIONS['PMS_Play'])
|
options.append(OPTIONS['PMS_Play'])
|
||||||
|
if self.kodi_type in v.KODI_VIDEOTYPES:
|
||||||
if self.item_type in v.KODI_VIDEOTYPES:
|
|
||||||
options.append(OPTIONS['Transcode'])
|
options.append(OPTIONS['Transcode'])
|
||||||
|
# userdata = self.api.userdata()
|
||||||
# userdata = self.api.getUserData()
|
|
||||||
# if userdata['Favorite']:
|
# if userdata['Favorite']:
|
||||||
# # Remove from emby favourites
|
# # Remove from emby favourites
|
||||||
# options.append(OPTIONS['RemoveFav'])
|
# options.append(OPTIONS['RemoveFav'])
|
||||||
# else:
|
# else:
|
||||||
# # Add to emby favourites
|
# # Add to emby favourites
|
||||||
# options.append(OPTIONS['AddFav'])
|
# options.append(OPTIONS['AddFav'])
|
||||||
|
# if self.kodi_type == "song":
|
||||||
# if self.item_type == "song":
|
|
||||||
# # Set custom song rating
|
# # Set custom song rating
|
||||||
# options.append(OPTIONS['RateSong'])
|
# options.append(OPTIONS['RateSong'])
|
||||||
|
|
||||||
# Refresh item
|
# Refresh item
|
||||||
# options.append(OPTIONS['Refresh'])
|
# options.append(OPTIONS['Refresh'])
|
||||||
# Delete item, only if the Plex Home main user is logged in
|
# Delete item, only if the Plex Home main user is logged in
|
||||||
|
@ -115,103 +101,64 @@ class ContextMenu(object):
|
||||||
options.append(OPTIONS['Delete'])
|
options.append(OPTIONS['Delete'])
|
||||||
# Addon settings
|
# Addon settings
|
||||||
options.append(OPTIONS['Addon'])
|
options.append(OPTIONS['Addon'])
|
||||||
|
|
||||||
context_menu = context.ContextMenu(
|
context_menu = context.ContextMenu(
|
||||||
"script-emby-context.xml",
|
"script-emby-context.xml",
|
||||||
xbmcaddon.Addon(
|
Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
|
||||||
'plugin.video.plexkodiconnect').getAddonInfo('path'),
|
"default",
|
||||||
"default", "1080i")
|
"1080i")
|
||||||
context_menu.set_options(options)
|
context_menu.set_options(options)
|
||||||
context_menu.doModal()
|
context_menu.doModal()
|
||||||
|
|
||||||
if context_menu.is_selected():
|
if context_menu.is_selected():
|
||||||
self._selected_option = context_menu.get_selected()
|
self._selected_option = context_menu.get_selected()
|
||||||
|
|
||||||
return self._selected_option
|
return self._selected_option
|
||||||
|
|
||||||
def _action_menu(self):
|
def _action_menu(self):
|
||||||
|
"""
|
||||||
|
Do whatever the user selected to do
|
||||||
|
"""
|
||||||
selected = self._selected_option
|
selected = self._selected_option
|
||||||
|
|
||||||
if selected == OPTIONS['Transcode']:
|
if selected == OPTIONS['Transcode']:
|
||||||
window('plex_forcetranscode', value='true')
|
state.FORCE_TRANSCODE = True
|
||||||
self._PMS_play()
|
self._PMS_play()
|
||||||
|
|
||||||
elif selected == OPTIONS['PMS_Play']:
|
elif selected == OPTIONS['PMS_Play']:
|
||||||
self._PMS_play()
|
self._PMS_play()
|
||||||
|
|
||||||
# elif selected == OPTIONS['Refresh']:
|
# elif selected == OPTIONS['Refresh']:
|
||||||
# self.emby.refreshItem(self.item_id)
|
# self.emby.refreshItem(self.item_id)
|
||||||
|
|
||||||
# elif selected == OPTIONS['AddFav']:
|
# elif selected == OPTIONS['AddFav']:
|
||||||
# self.emby.updateUserRating(self.item_id, favourite=True)
|
# self.emby.updateUserRating(self.item_id, favourite=True)
|
||||||
|
|
||||||
# elif selected == OPTIONS['RemoveFav']:
|
# elif selected == OPTIONS['RemoveFav']:
|
||||||
# self.emby.updateUserRating(self.item_id, favourite=False)
|
# self.emby.updateUserRating(self.item_id, favourite=False)
|
||||||
|
|
||||||
# elif selected == OPTIONS['RateSong']:
|
# elif selected == OPTIONS['RateSong']:
|
||||||
# self._rate_song()
|
# self._rate_song()
|
||||||
|
|
||||||
elif selected == OPTIONS['Addon']:
|
elif selected == OPTIONS['Addon']:
|
||||||
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||||
|
|
||||||
elif selected == OPTIONS['Delete']:
|
elif selected == OPTIONS['Delete']:
|
||||||
self._delete_item()
|
self._delete_item()
|
||||||
|
|
||||||
def _rate_song(self):
|
|
||||||
|
|
||||||
conn = kodiSQL('music')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
query = "SELECT rating FROM song WHERE idSong = ?"
|
|
||||||
cursor.execute(query, (self.kodi_id,))
|
|
||||||
try:
|
|
||||||
value = cursor.fetchone()[0]
|
|
||||||
current_value = int(round(float(value), 0))
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
new_value = dialog("numeric", 0, lang(30411), str(current_value))
|
|
||||||
if new_value > -1:
|
|
||||||
|
|
||||||
new_value = int(new_value)
|
|
||||||
if new_value > 5:
|
|
||||||
new_value = 5
|
|
||||||
|
|
||||||
if settings('enableUpdateSongRating') == "true":
|
|
||||||
musicutils.updateRatingToFile(new_value, self.api.get_file_path())
|
|
||||||
|
|
||||||
query = "UPDATE song SET rating = ? WHERE idSong = ?"
|
|
||||||
cursor.execute(query, (new_value, self.kodi_id,))
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def _delete_item(self):
|
def _delete_item(self):
|
||||||
|
"""
|
||||||
|
Delete item on PMS
|
||||||
|
"""
|
||||||
delete = True
|
delete = True
|
||||||
if settings('skipContextMenu') != "true":
|
if settings('skipContextMenu') != "true":
|
||||||
|
if not dialog("yesno", heading="{plex}", line1=lang(33041)):
|
||||||
if not dialog("yesno", heading=lang(29999), line1=lang(33041)):
|
LOG.info("User skipped deletion for: %s", self.plex_id)
|
||||||
log.info("User skipped deletion for: %s", self.item_id)
|
|
||||||
delete = False
|
delete = False
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
log.info("Deleting Plex item with id %s", self.item_id)
|
LOG.info("Deleting Plex item with id %s", self.plex_id)
|
||||||
if delete_item_from_pms(self.item_id) is False:
|
if delete_item_from_pms(self.plex_id) is False:
|
||||||
dialog("ok", heading="{plex}", line1=lang(30414))
|
dialog("ok", heading="{plex}", line1=lang(30414))
|
||||||
|
|
||||||
def _PMS_play(self):
|
def _PMS_play(self):
|
||||||
"""
|
"""
|
||||||
For using direct paths: Initiates playback using the PMS
|
For using direct paths: Initiates playback using the PMS
|
||||||
"""
|
"""
|
||||||
window('plex_contextplay', value='true')
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
params = {
|
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
||||||
'filename': '/library/metadata/%s' % self.item_id,
|
playqueue.clear()
|
||||||
'id': self.item_id,
|
state.CONTEXT_MENU_PLAY = True
|
||||||
'dbid': self.kodi_id,
|
handle = ('plugin://%s/?plex_id=%s&plex_type=%s&mode=play'
|
||||||
'mode': "play"
|
% (v.ADDON_TYPE[self.plex_type],
|
||||||
}
|
self.plex_id,
|
||||||
from urllib import urlencode
|
self.plex_type))
|
||||||
handle = ("plugin://plugin.video.plexkodiconnect/movies?%s"
|
executebuiltin('RunPlugin(%s)' % handle)
|
||||||
% urlencode(params))
|
|
||||||
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
import logging
|
from os.path import join
|
||||||
import os
|
|
||||||
|
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcaddon
|
from xbmcaddon import Addon
|
||||||
|
|
||||||
from utils import window
|
from utils import window
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
addon = xbmcaddon.Addon('plugin.video.plexkodiconnect')
|
ADDON = Addon('plugin.video.plexkodiconnect')
|
||||||
|
|
||||||
ACTION_PARENT_DIR = 9
|
ACTION_PARENT_DIR = 9
|
||||||
ACTION_PREVIOUS_MENU = 10
|
ACTION_PREVIOUS_MENU = 10
|
||||||
|
@ -27,16 +25,16 @@ USER_IMAGE = 150
|
||||||
|
|
||||||
|
|
||||||
class ContextMenu(xbmcgui.WindowXMLDialog):
|
class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||||
|
|
||||||
_options = []
|
|
||||||
selected_option = None
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._options = []
|
||||||
|
self.selected_option = None
|
||||||
|
self.list_ = None
|
||||||
|
self.background = None
|
||||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
def set_options(self, options=[]):
|
def set_options(self, options=None):
|
||||||
|
if not options:
|
||||||
|
options = []
|
||||||
self._options = options
|
self._options = options
|
||||||
|
|
||||||
def is_selected(self):
|
def is_selected(self):
|
||||||
|
@ -46,17 +44,13 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||||
return self.selected_option
|
return self.selected_option
|
||||||
|
|
||||||
def onInit(self):
|
def onInit(self):
|
||||||
|
|
||||||
if window('PlexUserImage'):
|
if window('PlexUserImage'):
|
||||||
self.getControl(USER_IMAGE).setImage(window('PlexUserImage'))
|
self.getControl(USER_IMAGE).setImage(window('PlexUserImage'))
|
||||||
|
|
||||||
height = 479 + (len(self._options) * 55)
|
height = 479 + (len(self._options) * 55)
|
||||||
log.info("options: %s", self._options)
|
LOG.debug("options: %s", self._options)
|
||||||
self.list_ = self.getControl(LIST)
|
self.list_ = self.getControl(LIST)
|
||||||
|
|
||||||
for option in self._options:
|
for option in self._options:
|
||||||
self.list_.addItem(self._add_listitem(option))
|
self.list_.addItem(self._add_listitem(option))
|
||||||
|
|
||||||
self.background = self._add_editcontrol(730, height, 30, 450)
|
self.background = self._add_editcontrol(730, height, 30, 450)
|
||||||
self.setFocus(self.list_)
|
self.setFocus(self.list_)
|
||||||
|
|
||||||
|
@ -64,27 +58,23 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||||
|
|
||||||
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
|
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
|
||||||
|
|
||||||
if self.getFocusId() == LIST:
|
if self.getFocusId() == LIST:
|
||||||
option = self.list_.getSelectedItem()
|
option = self.list_.getSelectedItem()
|
||||||
self.selected_option = option.getLabel()
|
self.selected_option = option.getLabel()
|
||||||
log.info('option selected: %s', self.selected_option)
|
LOG.info('option selected: %s', self.selected_option)
|
||||||
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _add_editcontrol(self, x, y, height, width, password=0):
|
def _add_editcontrol(self, x, y, height, width, password=None):
|
||||||
|
media = join(ADDON.getAddonInfo('path'),
|
||||||
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
|
'resources', 'skins', 'default', 'media')
|
||||||
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
||||||
filename=os.path.join(media, "white.png"),
|
filename=join(media, "white.png"),
|
||||||
aspectRatio=0,
|
aspectRatio=0,
|
||||||
colorDiffuse="ff111111")
|
colorDiffuse="ff111111")
|
||||||
control.setPosition(x, y)
|
control.setPosition(x, y)
|
||||||
control.setHeight(height)
|
control.setHeight(height)
|
||||||
control.setWidth(width)
|
control.setWidth(width)
|
||||||
|
|
||||||
self.addControl(control)
|
self.addControl(control)
|
||||||
return control
|
return control
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
import requests
|
||||||
|
|
||||||
from utils import settings, window, language as lang, dialog
|
from utils import window, language as lang, dialog
|
||||||
import clientinfo as client
|
import clientinfo as client
|
||||||
|
|
||||||
import state
|
import state
|
||||||
|
@ -17,7 +16,7 @@ import state
|
||||||
import requests.packages.urllib3
|
import requests.packages.urllib3
|
||||||
requests.packages.urllib3.disable_warnings()
|
requests.packages.urllib3.disable_warnings()
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -47,17 +46,7 @@ class DownloadUtils():
|
||||||
Reserved for userclient only
|
Reserved for userclient only
|
||||||
"""
|
"""
|
||||||
self.server = server
|
self.server = server
|
||||||
log.debug("Set server: %s" % server)
|
LOG.debug("Set server: %s", server)
|
||||||
|
|
||||||
def setToken(self, token):
|
|
||||||
"""
|
|
||||||
Reserved for userclient only
|
|
||||||
"""
|
|
||||||
self.token = token
|
|
||||||
if token == '':
|
|
||||||
log.debug('Set token: empty token!')
|
|
||||||
else:
|
|
||||||
log.debug("Set token: xxxxxxx")
|
|
||||||
|
|
||||||
def setSSL(self, verifySSL=None, certificate=None):
|
def setSSL(self, verifySSL=None, certificate=None):
|
||||||
"""
|
"""
|
||||||
|
@ -68,15 +57,15 @@ class DownloadUtils():
|
||||||
certificate must be path to certificate or 'None'
|
certificate must be path to certificate or 'None'
|
||||||
"""
|
"""
|
||||||
if verifySSL is None:
|
if verifySSL is None:
|
||||||
verifySSL = settings('sslverify')
|
verifySSL = state.VERIFY_SSL_CERT
|
||||||
if certificate is None:
|
if certificate is None:
|
||||||
certificate = settings('sslcert')
|
certificate = state.SSL_CERT_PATH
|
||||||
log.debug("Verify SSL certificates set to: %s" % verifySSL)
|
# Set the session's parameters
|
||||||
log.debug("SSL client side certificate set to: %s" % certificate)
|
self.s.verify = verifySSL
|
||||||
if verifySSL != 'true':
|
if certificate:
|
||||||
self.s.verify = False
|
|
||||||
if certificate != 'None':
|
|
||||||
self.s.cert = certificate
|
self.s.cert = certificate
|
||||||
|
LOG.debug("Verify SSL certificates set to: %s", verifySSL)
|
||||||
|
LOG.debug("SSL client side certificate set to: %s", certificate)
|
||||||
|
|
||||||
def startSession(self, reset=False):
|
def startSession(self, reset=False):
|
||||||
"""
|
"""
|
||||||
|
@ -95,7 +84,6 @@ class DownloadUtils():
|
||||||
|
|
||||||
# Set other stuff
|
# Set other stuff
|
||||||
self.setServer(window('pms_server'))
|
self.setServer(window('pms_server'))
|
||||||
self.setToken(window('pms_token'))
|
|
||||||
|
|
||||||
# Counters to declare PMS dead or unauthorized
|
# Counters to declare PMS dead or unauthorized
|
||||||
# Use window variables because start of movies will be called with a
|
# Use window variables because start of movies will be called with a
|
||||||
|
@ -108,18 +96,18 @@ class DownloadUtils():
|
||||||
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
|
|
||||||
log.info("Requests session started on: %s" % self.server)
|
LOG.info("Requests session started on: %s", self.server)
|
||||||
|
|
||||||
def stopSession(self):
|
def stopSession(self):
|
||||||
try:
|
try:
|
||||||
self.s.close()
|
self.s.close()
|
||||||
except:
|
except:
|
||||||
log.info("Requests session already closed")
|
LOG.info("Requests session already closed")
|
||||||
try:
|
try:
|
||||||
del self.s
|
del self.s
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
log.info('Request session stopped')
|
LOG.info('Request session stopped')
|
||||||
|
|
||||||
def getHeader(self, options=None):
|
def getHeader(self, options=None):
|
||||||
header = client.getXArgsDeviceInfo()
|
header = client.getXArgsDeviceInfo()
|
||||||
|
@ -142,7 +130,8 @@ class DownloadUtils():
|
||||||
|
|
||||||
def downloadUrl(self, url, action_type="GET", postBody=None,
|
def downloadUrl(self, url, action_type="GET", postBody=None,
|
||||||
parameters=None, authenticate=True, headerOptions=None,
|
parameters=None, authenticate=True, headerOptions=None,
|
||||||
verifySSL=True, timeout=None, return_response=False):
|
verifySSL=True, timeout=None, return_response=False,
|
||||||
|
headerOverride=None):
|
||||||
"""
|
"""
|
||||||
Override SSL check with verifySSL=False
|
Override SSL check with verifySSL=False
|
||||||
|
|
||||||
|
@ -164,7 +153,7 @@ class DownloadUtils():
|
||||||
try:
|
try:
|
||||||
s = self.s
|
s = self.s
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.info("Request session does not exist: start one")
|
LOG.info("Request session does not exist: start one")
|
||||||
self.startSession()
|
self.startSession()
|
||||||
s = self.s
|
s = self.s
|
||||||
# Replace for the real values
|
# Replace for the real values
|
||||||
|
@ -173,9 +162,13 @@ class DownloadUtils():
|
||||||
# User is not (yet) authenticated. Used to communicate with
|
# User is not (yet) authenticated. Used to communicate with
|
||||||
# plex.tv and to check for PMS servers
|
# plex.tv and to check for PMS servers
|
||||||
s = requests
|
s = requests
|
||||||
headerOptions = self.getHeader(options=headerOptions)
|
if not headerOverride:
|
||||||
if settings('sslcert') != 'None':
|
headerOptions = self.getHeader(options=headerOptions)
|
||||||
kwargs['cert'] = settings('sslcert')
|
else:
|
||||||
|
headerOptions = headerOverride
|
||||||
|
kwargs['verify'] = state.VERIFY_SSL_CERT
|
||||||
|
if state.SSL_CERT_PATH:
|
||||||
|
kwargs['cert'] = state.SSL_CERT_PATH
|
||||||
|
|
||||||
# Set the variables we were passed (fallback to request session
|
# Set the variables we were passed (fallback to request session
|
||||||
# otherwise - faster)
|
# otherwise - faster)
|
||||||
|
@ -196,39 +189,39 @@ class DownloadUtils():
|
||||||
r = self._doDownload(s, action_type, **kwargs)
|
r = self._doDownload(s, action_type, **kwargs)
|
||||||
|
|
||||||
# THE EXCEPTIONS
|
# THE EXCEPTIONS
|
||||||
|
except requests.exceptions.SSLError as e:
|
||||||
|
LOG.warn("Invalid SSL certificate for: %s", url)
|
||||||
|
LOG.warn(e)
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
# Connection error
|
# Connection error
|
||||||
log.warn("Server unreachable at: %s" % url)
|
LOG.warn("Server unreachable at: %s", url)
|
||||||
log.warn(e)
|
LOG.warn(e)
|
||||||
|
|
||||||
except requests.exceptions.Timeout as e:
|
except requests.exceptions.Timeout as e:
|
||||||
log.warn("Server timeout at: %s" % url)
|
LOG.warn("Server timeout at: %s", url)
|
||||||
log.warn(e)
|
LOG.warn(e)
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
log.warn('HTTP Error at %s' % url)
|
LOG.warn('HTTP Error at %s', url)
|
||||||
log.warn(e)
|
LOG.warn(e)
|
||||||
|
|
||||||
except requests.exceptions.SSLError as e:
|
|
||||||
log.warn("Invalid SSL certificate for: %s" % url)
|
|
||||||
log.warn(e)
|
|
||||||
|
|
||||||
except requests.exceptions.TooManyRedirects as e:
|
except requests.exceptions.TooManyRedirects as e:
|
||||||
log.warn("Too many redirects connecting to: %s" % url)
|
LOG.warn("Too many redirects connecting to: %s", url)
|
||||||
log.warn(e)
|
LOG.warn(e)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
log.warn("Unknown error connecting to: %s" % url)
|
LOG.warn("Unknown error connecting to: %s", url)
|
||||||
log.warn(e)
|
LOG.warn(e)
|
||||||
|
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
log.info('SystemExit detected, aborting download')
|
LOG.info('SystemExit detected, aborting download')
|
||||||
self.stopSession()
|
self.stopSession()
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.warn('Unknown error while downloading. Traceback:')
|
LOG.warn('Unknown error while downloading. Traceback:')
|
||||||
import traceback
|
import traceback
|
||||||
log.warn(traceback.format_exc())
|
LOG.warn(traceback.format_exc())
|
||||||
|
|
||||||
# THE RESPONSE #####
|
# THE RESPONSE #####
|
||||||
else:
|
else:
|
||||||
|
@ -250,19 +243,19 @@ class DownloadUtils():
|
||||||
# Called when checking a connect - no need for rash action
|
# Called when checking a connect - no need for rash action
|
||||||
return 401
|
return 401
|
||||||
r.encoding = 'utf-8'
|
r.encoding = 'utf-8'
|
||||||
log.warn('HTTP error 401 from PMS %s' % url)
|
LOG.warn('HTTP error 401 from PMS %s', url)
|
||||||
log.info(r.text)
|
LOG.info(r.text)
|
||||||
if '401 Unauthorized' in r.text:
|
if '401 Unauthorized' in r.text:
|
||||||
# Truly unauthorized
|
# Truly unauthorized
|
||||||
window('countUnauthorized',
|
window('countUnauthorized',
|
||||||
value=str(int(window('countUnauthorized')) + 1))
|
value=str(int(window('countUnauthorized')) + 1))
|
||||||
if (int(window('countUnauthorized')) >=
|
if (int(window('countUnauthorized')) >=
|
||||||
self.unauthorizedAttempts):
|
self.unauthorizedAttempts):
|
||||||
log.warn('We seem to be truly unauthorized for PMS'
|
LOG.warn('We seem to be truly unauthorized for PMS'
|
||||||
' %s ' % url)
|
' %s ', url)
|
||||||
if state.PMS_STATUS not in ('401', 'Auth'):
|
if state.PMS_STATUS not in ('401', 'Auth'):
|
||||||
# Tell userclient token has been revoked.
|
# Tell userclient token has been revoked.
|
||||||
log.debug('Setting PMS server status to '
|
LOG.debug('Setting PMS server status to '
|
||||||
'unauthorized')
|
'unauthorized')
|
||||||
state.PMS_STATUS = '401'
|
state.PMS_STATUS = '401'
|
||||||
window('plex_serverStatus', value="401")
|
window('plex_serverStatus', value="401")
|
||||||
|
@ -272,7 +265,7 @@ class DownloadUtils():
|
||||||
icon='{error}')
|
icon='{error}')
|
||||||
else:
|
else:
|
||||||
# there might be other 401 where e.g. PMS under strain
|
# there might be other 401 where e.g. PMS under strain
|
||||||
log.info('PMS might only be under strain')
|
LOG.info('PMS might only be under strain')
|
||||||
return 401
|
return 401
|
||||||
|
|
||||||
elif r.status_code in (200, 201):
|
elif r.status_code in (200, 201):
|
||||||
|
@ -300,21 +293,19 @@ class DownloadUtils():
|
||||||
# update
|
# update
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.warn("Unable to convert the response for: "
|
LOG.warn("Unable to convert the response for: "
|
||||||
"%s" % url)
|
"%s", url)
|
||||||
log.warn("Received headers were: %s" % r.headers)
|
LOG.warn("Received headers were: %s", r.headers)
|
||||||
log.warn('Received text:')
|
LOG.warn('Received text: %s', r.text)
|
||||||
log.warn(r.text)
|
|
||||||
return True
|
return True
|
||||||
elif r.status_code == 403:
|
elif r.status_code == 403:
|
||||||
# E.g. deleting a PMS item
|
# E.g. deleting a PMS item
|
||||||
log.warn('PMS sent 403: Forbidden error for url %s' % url)
|
LOG.warn('PMS sent 403: Forbidden error for url %s', url)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
log.warn('Unknown answer from PMS %s with status code %s. '
|
|
||||||
'Message:' % (url, r.status_code))
|
|
||||||
r.encoding = 'utf-8'
|
r.encoding = 'utf-8'
|
||||||
log.warn(r.text)
|
LOG.warn('Unknown answer from PMS %s with status code %s. ',
|
||||||
|
url, r.status_code)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# And now deal with the consequences of the exceptions
|
# And now deal with the consequences of the exceptions
|
||||||
|
@ -324,8 +315,8 @@ class DownloadUtils():
|
||||||
window('countError',
|
window('countError',
|
||||||
value=str(int(window('countError')) + 1))
|
value=str(int(window('countError')) + 1))
|
||||||
if int(window('countError')) >= self.connectionAttempts:
|
if int(window('countError')) >= self.connectionAttempts:
|
||||||
log.warn('Failed to connect to %s too many times. '
|
LOG.warn('Failed to connect to %s too many times. '
|
||||||
'Declare PMS dead' % url)
|
'Declare PMS dead', url)
|
||||||
window('plex_online', value="false")
|
window('plex_online', value="false")
|
||||||
except:
|
except:
|
||||||
# 'countError' not yet set
|
# 'countError' not yet set
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from os import walk, makedirs
|
from os import walk, makedirs
|
||||||
from os.path import basename, join
|
from os.path import basename, join
|
||||||
|
@ -11,17 +11,18 @@ import xbmcplugin
|
||||||
from xbmc import sleep, executebuiltin, translatePath
|
from xbmc import sleep, executebuiltin, translatePath
|
||||||
from xbmcgui import ListItem
|
from xbmcgui import ListItem
|
||||||
|
|
||||||
from utils import window, settings, language as lang, dialog, tryEncode, \
|
from utils import window, settings, language as lang, dialog, try_encode, \
|
||||||
CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode
|
catch_exceptions, exists_dir, plex_command, try_decode
|
||||||
import downloadutils
|
import downloadutils
|
||||||
|
|
||||||
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
|
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
|
||||||
GetMachineIdentifier
|
GetMachineIdentifier
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
|
import json_rpc as js
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
HANDLE = int(argv[1])
|
HANDLE = int(argv[1])
|
||||||
|
@ -39,7 +40,7 @@ def chooseServer():
|
||||||
|
|
||||||
import initialsetup
|
import initialsetup
|
||||||
setup = initialsetup.InitialSetup()
|
setup = initialsetup.InitialSetup()
|
||||||
server = setup.PickPMS(showDialog=True)
|
server = setup.pick_pms(showDialog=True)
|
||||||
if server is None:
|
if server is None:
|
||||||
log.error('We did not connect to a new PMS, aborting')
|
log.error('We did not connect to a new PMS, aborting')
|
||||||
plex_command('SUSPEND_USER_CLIENT', 'False')
|
plex_command('SUSPEND_USER_CLIENT', 'False')
|
||||||
|
@ -47,16 +48,14 @@ def chooseServer():
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("User chose server %s" % server['name'])
|
log.info("User chose server %s" % server['name'])
|
||||||
setup.WritePMStoSettings(server)
|
setup.write_pms_to_settings(server)
|
||||||
|
|
||||||
if not __LogOut():
|
if not __LogOut():
|
||||||
return
|
return
|
||||||
|
|
||||||
from utils import deletePlaylists, deleteNodes
|
from utils import wipe_database
|
||||||
# First remove playlists
|
# Wipe Kodi and Plex database as well as playlists and video nodes
|
||||||
deletePlaylists()
|
wipe_database()
|
||||||
# Remove video nodes
|
|
||||||
deleteNodes()
|
|
||||||
|
|
||||||
# Log in again
|
# Log in again
|
||||||
__LogIn()
|
__LogIn()
|
||||||
|
@ -86,7 +85,7 @@ def togglePlexTV():
|
||||||
else:
|
else:
|
||||||
log.info('Login to plex.tv')
|
log.info('Login to plex.tv')
|
||||||
import initialsetup
|
import initialsetup
|
||||||
initialsetup.InitialSetup().PlexTVSignIn()
|
initialsetup.InitialSetup().plex_tv_sign_in()
|
||||||
dialog('notification',
|
dialog('notification',
|
||||||
lang(29999),
|
lang(29999),
|
||||||
lang(39221),
|
lang(39221),
|
||||||
|
@ -174,10 +173,10 @@ def switchPlexUser():
|
||||||
return
|
return
|
||||||
|
|
||||||
# First remove playlists of old user
|
# First remove playlists of old user
|
||||||
from utils import deletePlaylists, deleteNodes
|
from utils import delete_playlists, delete_nodes
|
||||||
deletePlaylists()
|
delete_playlists()
|
||||||
# Remove video nodes
|
# Remove video nodes
|
||||||
deleteNodes()
|
delete_nodes()
|
||||||
__LogIn()
|
__LogIn()
|
||||||
|
|
||||||
|
|
||||||
|
@ -193,48 +192,41 @@ def GetSubFolders(nodeindex):
|
||||||
|
|
||||||
|
|
||||||
##### LISTITEM SETUP FOR VIDEONODES #####
|
##### LISTITEM SETUP FOR VIDEONODES #####
|
||||||
def createListItem(item, appendShowTitle=False, appendSxxExx=False):
|
def createListItem(item, append_show_title=False, append_sxxexx=False):
|
||||||
|
log.debug('createListItem called with append_show_title %s, append_sxxexx '
|
||||||
|
'%s, item: %s', append_show_title, append_sxxexx, item)
|
||||||
title = item['title']
|
title = item['title']
|
||||||
li = ListItem(title)
|
li = ListItem(title)
|
||||||
li.setProperty('IsPlayable', "true")
|
li.setProperty('IsPlayable', 'true')
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'duration': str(item['runtime']/60),
|
'duration': str(item['runtime']/60),
|
||||||
'Plot': item['plot'],
|
'Plot': item['plot'],
|
||||||
'Playcount': item['playcount']
|
'Playcount': item['playcount']
|
||||||
}
|
}
|
||||||
|
|
||||||
if "episode" in item:
|
if 'episode' in item:
|
||||||
episode = item['episode']
|
episode = item['episode']
|
||||||
metadata['Episode'] = episode
|
metadata['Episode'] = episode
|
||||||
|
if 'season' in item:
|
||||||
if "season" in item:
|
|
||||||
season = item['season']
|
season = item['season']
|
||||||
metadata['Season'] = season
|
metadata['Season'] = season
|
||||||
|
|
||||||
if season and episode:
|
if season and episode:
|
||||||
li.setProperty('episodeno', "s%.2de%.2d" % (season, episode))
|
li.setProperty('episodeno', 's%.2de%.2d' % (season, episode))
|
||||||
if appendSxxExx is True:
|
if append_sxxexx is True:
|
||||||
title = "S%.2dE%.2d - %s" % (season, episode, title)
|
title = 'S%.2dE%.2d - %s' % (season, episode, title)
|
||||||
|
if 'firstaired' in item:
|
||||||
if "firstaired" in item:
|
|
||||||
metadata['Premiered'] = item['firstaired']
|
metadata['Premiered'] = item['firstaired']
|
||||||
|
if 'showtitle' in item:
|
||||||
if "showtitle" in item:
|
|
||||||
metadata['TVshowTitle'] = item['showtitle']
|
metadata['TVshowTitle'] = item['showtitle']
|
||||||
if appendShowTitle is True:
|
if append_show_title is True:
|
||||||
title = item['showtitle'] + ' - ' + title
|
title = item['showtitle'] + ' - ' + title
|
||||||
|
if 'rating' in item:
|
||||||
if "rating" in item:
|
metadata['Rating'] = str(round(float(item['rating']), 1))
|
||||||
metadata['Rating'] = str(round(float(item['rating']),1))
|
if 'director' in item:
|
||||||
|
metadata['Director'] = item['director']
|
||||||
if "director" in item:
|
if 'writer' in item:
|
||||||
metadata['Director'] = " / ".join(item['director'])
|
metadata['Writer'] = item['writer']
|
||||||
|
if 'cast' in item:
|
||||||
if "writer" in item:
|
|
||||||
metadata['Writer'] = " / ".join(item['writer'])
|
|
||||||
|
|
||||||
if "cast" in item:
|
|
||||||
cast = []
|
cast = []
|
||||||
castandrole = []
|
castandrole = []
|
||||||
for person in item['cast']:
|
for person in item['cast']:
|
||||||
|
@ -245,16 +237,17 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
|
||||||
metadata['CastAndRole'] = castandrole
|
metadata['CastAndRole'] = castandrole
|
||||||
|
|
||||||
metadata['Title'] = title
|
metadata['Title'] = title
|
||||||
|
metadata['mediatype'] = 'episode'
|
||||||
|
metadata['dbid'] = str(item['episodeid'])
|
||||||
li.setLabel(title)
|
li.setLabel(title)
|
||||||
|
li.setInfo(type='Video', infoLabels=metadata)
|
||||||
|
|
||||||
li.setInfo(type="Video", infoLabels=metadata)
|
|
||||||
li.setProperty('resumetime', str(item['resume']['position']))
|
li.setProperty('resumetime', str(item['resume']['position']))
|
||||||
li.setProperty('totaltime', str(item['resume']['total']))
|
li.setProperty('totaltime', str(item['resume']['total']))
|
||||||
li.setArt(item['art'])
|
li.setArt(item['art'])
|
||||||
li.setThumbnailImage(item['art'].get('thumb',''))
|
li.setThumbnailImage(item['art'].get('thumb', ''))
|
||||||
li.setArt({'icon': 'DefaultTVShows.png'})
|
li.setArt({'icon': 'DefaultTVShows.png'})
|
||||||
li.setProperty('dbid', str(item['episodeid']))
|
li.setProperty('fanart_image', item['art'].get('tvshow.fanart', ''))
|
||||||
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
|
|
||||||
try:
|
try:
|
||||||
li.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
|
li.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -263,12 +256,10 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
|
||||||
for key, value in item['streamdetails'].iteritems():
|
for key, value in item['streamdetails'].iteritems():
|
||||||
for stream in value:
|
for stream in value:
|
||||||
li.addStreamInfo(key, stream)
|
li.addStreamInfo(key, stream)
|
||||||
|
|
||||||
return li
|
return li
|
||||||
|
|
||||||
##### GET NEXTUP EPISODES FOR TAGNAME #####
|
##### GET NEXTUP EPISODES FOR TAGNAME #####
|
||||||
def getNextUpEpisodes(tagname, limit):
|
def getNextUpEpisodes(tagname, limit):
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
# if the addon is called with nextup parameter,
|
# if the addon is called with nextup parameter,
|
||||||
# we return the nextepisodes list of the given tagname
|
# we return the nextepisodes list of the given tagname
|
||||||
|
@ -283,68 +274,50 @@ def getNextUpEpisodes(tagname, limit):
|
||||||
]},
|
]},
|
||||||
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
||||||
}
|
}
|
||||||
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
|
for item in js.get_tv_shows(params):
|
||||||
|
if settings('ignoreSpecialsNextEpisodes') == "true":
|
||||||
# If we found any, find the oldest unwatched show for each one.
|
params = {
|
||||||
try:
|
'tvshowid': item['tvshowid'],
|
||||||
items = result['result']['tvshows']
|
'sort': {'method': "episode"},
|
||||||
except (KeyError, TypeError):
|
'filter': {
|
||||||
pass
|
'and': [
|
||||||
else:
|
{'operator': "lessthan",
|
||||||
for item in items:
|
'field': "playcount",
|
||||||
if settings('ignoreSpecialsNextEpisodes') == "true":
|
'value': "1"},
|
||||||
params = {
|
{'operator': "greaterthan",
|
||||||
'tvshowid': item['tvshowid'],
|
'field': "season",
|
||||||
'sort': {'method': "episode"},
|
'value': "0"}]},
|
||||||
'filter': {
|
'properties': [
|
||||||
'and': [
|
"title", "playcount", "season", "episode", "showtitle",
|
||||||
{'operator': "lessthan",
|
"plot", "file", "rating", "resume", "tvshowid", "art",
|
||||||
'field': "playcount",
|
"streamdetails", "firstaired", "runtime", "writer",
|
||||||
'value': "1"},
|
"dateadded", "lastplayed"
|
||||||
{'operator': "greaterthan",
|
],
|
||||||
'field': "season",
|
'limits': {"end": 1}
|
||||||
'value': "0"}]},
|
}
|
||||||
'properties': [
|
else:
|
||||||
"title", "playcount", "season", "episode", "showtitle",
|
params = {
|
||||||
"plot", "file", "rating", "resume", "tvshowid", "art",
|
'tvshowid': item['tvshowid'],
|
||||||
"streamdetails", "firstaired", "runtime", "writer",
|
'sort': {'method': "episode"},
|
||||||
"dateadded", "lastplayed"
|
'filter': {
|
||||||
],
|
'operator': "lessthan",
|
||||||
'limits': {"end": 1}
|
'field': "playcount",
|
||||||
}
|
'value': "1"},
|
||||||
else:
|
'properties': [
|
||||||
params = {
|
"title", "playcount", "season", "episode", "showtitle",
|
||||||
'tvshowid': item['tvshowid'],
|
"plot", "file", "rating", "resume", "tvshowid", "art",
|
||||||
'sort': {'method': "episode"},
|
"streamdetails", "firstaired", "runtime", "writer",
|
||||||
'filter': {
|
"dateadded", "lastplayed"
|
||||||
'operator': "lessthan",
|
],
|
||||||
'field': "playcount",
|
'limits': {"end": 1}
|
||||||
'value': "1"},
|
}
|
||||||
'properties': [
|
for episode in js.get_episodes(params):
|
||||||
"title", "playcount", "season", "episode", "showtitle",
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
"plot", "file", "rating", "resume", "tvshowid", "art",
|
url=episode['file'],
|
||||||
"streamdetails", "firstaired", "runtime", "writer",
|
listitem=createListItem(episode))
|
||||||
"dateadded", "lastplayed"
|
count += 1
|
||||||
],
|
if count == limit:
|
||||||
'limits': {"end": 1}
|
break
|
||||||
}
|
|
||||||
|
|
||||||
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
|
|
||||||
try:
|
|
||||||
episodes = result['result']['episodes']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for episode in episodes:
|
|
||||||
li = createListItem(episode)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
|
||||||
url=episode['file'],
|
|
||||||
listitem=li)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
xbmcplugin.endOfDirectory(handle=HANDLE)
|
||||||
|
|
||||||
|
|
||||||
|
@ -364,42 +337,26 @@ def getInProgressEpisodes(tagname, limit):
|
||||||
]},
|
]},
|
||||||
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
|
||||||
}
|
}
|
||||||
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
|
for item in js.get_tv_shows(params):
|
||||||
# If we found any, find the oldest unwatched show for each one.
|
params = {
|
||||||
try:
|
'tvshowid': item['tvshowid'],
|
||||||
items = result['result']['tvshows']
|
'sort': {'method': "episode"},
|
||||||
except (KeyError, TypeError):
|
'filter': {
|
||||||
pass
|
'operator': "true",
|
||||||
else:
|
'field': "inprogress",
|
||||||
for item in items:
|
'value': ""},
|
||||||
params = {
|
'properties': ["title", "playcount", "season", "episode",
|
||||||
'tvshowid': item['tvshowid'],
|
"showtitle", "plot", "file", "rating", "resume",
|
||||||
'sort': {'method': "episode"},
|
"tvshowid", "art", "cast", "streamdetails", "firstaired",
|
||||||
'filter': {
|
"runtime", "writer", "dateadded", "lastplayed"]
|
||||||
'operator': "true",
|
}
|
||||||
'field': "inprogress",
|
for episode in js.get_episodes(params):
|
||||||
'value': ""},
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
'properties': ["title", "playcount", "season", "episode",
|
url=episode['file'],
|
||||||
"showtitle", "plot", "file", "rating", "resume",
|
listitem=createListItem(episode))
|
||||||
"tvshowid", "art", "cast", "streamdetails", "firstaired",
|
count += 1
|
||||||
"runtime", "writer", "dateadded", "lastplayed"]
|
if count == limit:
|
||||||
}
|
break
|
||||||
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
|
|
||||||
try:
|
|
||||||
episodes = result['result']['episodes']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for episode in episodes:
|
|
||||||
li = createListItem(episode)
|
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
|
||||||
url=episode['file'],
|
|
||||||
listitem=li)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
xbmcplugin.endOfDirectory(handle=HANDLE)
|
||||||
|
|
||||||
##### GET RECENT EPISODES FOR TAGNAME #####
|
##### GET RECENT EPISODES FOR TAGNAME #####
|
||||||
|
@ -409,25 +366,16 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit):
|
||||||
# if the addon is called with recentepisodes parameter,
|
# if the addon is called with recentepisodes parameter,
|
||||||
# we return the recentepisodes list of the given tagname
|
# we return the recentepisodes list of the given tagname
|
||||||
xbmcplugin.setContent(HANDLE, 'episodes')
|
xbmcplugin.setContent(HANDLE, 'episodes')
|
||||||
appendShowTitle = settings('RecentTvAppendShow') == 'true'
|
append_show_title = settings('RecentTvAppendShow') == 'true'
|
||||||
appendSxxExx = settings('RecentTvAppendSeason') == 'true'
|
append_sxxexx = settings('RecentTvAppendSeason') == 'true'
|
||||||
# First we get a list of all the TV shows - filtered by tag
|
# First we get a list of all the TV shows - filtered by tag
|
||||||
|
allshowsIds = set()
|
||||||
params = {
|
params = {
|
||||||
'sort': {'order': "descending", 'method': "dateadded"},
|
'sort': {'order': "descending", 'method': "dateadded"},
|
||||||
'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
|
'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
|
||||||
}
|
}
|
||||||
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
|
for tv_show in js.get_tv_shows(params):
|
||||||
# If we found any, find the oldest unwatched show for each one.
|
allshowsIds.add(tv_show['tvshowid'])
|
||||||
try:
|
|
||||||
items = result['result'][mediatype]
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
# No items, empty folder
|
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
|
||||||
return
|
|
||||||
|
|
||||||
allshowsIds = set()
|
|
||||||
for item in items:
|
|
||||||
allshowsIds.add(item['tvshowid'])
|
|
||||||
params = {
|
params = {
|
||||||
'sort': {'order': "descending", 'method': "dateadded"},
|
'sort': {'order': "descending", 'method': "dateadded"},
|
||||||
'properties': ["title", "playcount", "season", "episode", "showtitle",
|
'properties': ["title", "playcount", "season", "episode", "showtitle",
|
||||||
|
@ -442,26 +390,18 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit):
|
||||||
'field': "playcount",
|
'field': "playcount",
|
||||||
'value': "1"
|
'value': "1"
|
||||||
}
|
}
|
||||||
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
|
for episode in js.get_episodes(params):
|
||||||
try:
|
if episode['tvshowid'] in allshowsIds:
|
||||||
episodes = result['result']['episodes']
|
listitem = createListItem(episode,
|
||||||
except (KeyError, TypeError):
|
append_show_title=append_show_title,
|
||||||
pass
|
append_sxxexx=append_sxxexx)
|
||||||
else:
|
xbmcplugin.addDirectoryItem(
|
||||||
for episode in episodes:
|
handle=HANDLE,
|
||||||
if episode['tvshowid'] in allshowsIds:
|
url=episode['file'],
|
||||||
li = createListItem(episode,
|
listitem=listitem)
|
||||||
appendShowTitle=appendShowTitle,
|
count += 1
|
||||||
appendSxxExx=appendSxxExx)
|
if count == limit:
|
||||||
xbmcplugin.addDirectoryItem(
|
break
|
||||||
handle=HANDLE,
|
|
||||||
url=episode['file'],
|
|
||||||
listitem=li)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if count == limit:
|
|
||||||
break
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
xbmcplugin.endOfDirectory(handle=HANDLE)
|
||||||
|
|
||||||
|
|
||||||
|
@ -506,14 +446,14 @@ def getVideoFiles(plexId, params):
|
||||||
if exists_dir(path):
|
if exists_dir(path):
|
||||||
for root, dirs, files in walk(path):
|
for root, dirs, files in walk(path):
|
||||||
for directory in dirs:
|
for directory in dirs:
|
||||||
item_path = tryEncode(join(root, directory))
|
item_path = try_encode(join(root, directory))
|
||||||
li = ListItem(item_path, path=item_path)
|
li = ListItem(item_path, path=item_path)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=item_path,
|
url=item_path,
|
||||||
listitem=li,
|
listitem=li,
|
||||||
isFolder=True)
|
isFolder=True)
|
||||||
for file in files:
|
for file in files:
|
||||||
item_path = tryEncode(join(root, file))
|
item_path = try_encode(join(root, file))
|
||||||
li = ListItem(item_path, path=item_path)
|
li = ListItem(item_path, path=item_path)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=file,
|
url=file,
|
||||||
|
@ -524,7 +464,7 @@ def getVideoFiles(plexId, params):
|
||||||
xbmcplugin.endOfDirectory(HANDLE)
|
xbmcplugin.endOfDirectory(HANDLE)
|
||||||
|
|
||||||
|
|
||||||
@CatchExceptions(warnuser=False)
|
@catch_exceptions(warnuser=False)
|
||||||
def getExtraFanArt(plexid, plexPath):
|
def getExtraFanArt(plexid, plexPath):
|
||||||
"""
|
"""
|
||||||
Get extrafanart for listitem
|
Get extrafanart for listitem
|
||||||
|
@ -541,7 +481,7 @@ def getExtraFanArt(plexid, plexPath):
|
||||||
|
|
||||||
# We need to store the images locally for this to work
|
# We need to store the images locally for this to work
|
||||||
# because of the caching system in xbmc
|
# because of the caching system in xbmc
|
||||||
fanartDir = tryDecode(translatePath(
|
fanartDir = try_decode(translatePath(
|
||||||
"special://thumbnails/plex/%s/" % plexid))
|
"special://thumbnails/plex/%s/" % plexid))
|
||||||
if not exists_dir(fanartDir):
|
if not exists_dir(fanartDir):
|
||||||
# Download the images to the cache directory
|
# Download the images to the cache directory
|
||||||
|
@ -552,22 +492,22 @@ def getExtraFanArt(plexid, plexPath):
|
||||||
return xbmcplugin.endOfDirectory(HANDLE)
|
return xbmcplugin.endOfDirectory(HANDLE)
|
||||||
|
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
backdrops = api.getAllArtwork()['Backdrop']
|
backdrops = api.artwork()['Backdrop']
|
||||||
for count, backdrop in enumerate(backdrops):
|
for count, backdrop in enumerate(backdrops):
|
||||||
# Same ordering as in artwork
|
# Same ordering as in artwork
|
||||||
fanartFile = tryEncode(join(fanartDir, "fanart%.3d.jpg" % count))
|
fanartFile = try_encode(join(fanartDir, "fanart%.3d.jpg" % count))
|
||||||
li = ListItem("%.3d" % count, path=fanartFile)
|
li = ListItem("%.3d" % count, path=fanartFile)
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=HANDLE,
|
handle=HANDLE,
|
||||||
url=fanartFile,
|
url=fanartFile,
|
||||||
listitem=li)
|
listitem=li)
|
||||||
copyfile(backdrop, tryDecode(fanartFile))
|
copyfile(backdrop, try_decode(fanartFile))
|
||||||
else:
|
else:
|
||||||
log.info("Found cached backdrop.")
|
log.info("Found cached backdrop.")
|
||||||
# Use existing cached images
|
# Use existing cached images
|
||||||
for root, dirs, files in walk(fanartDir):
|
for root, dirs, files in walk(fanartDir):
|
||||||
for file in files:
|
for file in files:
|
||||||
fanartFile = tryEncode(join(root, file))
|
fanartFile = try_encode(join(root, file))
|
||||||
li = ListItem(file, path=fanartFile)
|
li = ListItem(file, path=fanartFile)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=fanartFile,
|
url=fanartFile,
|
||||||
|
@ -587,8 +527,8 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
limit: Max. number of items to retrieve, e.g. 50
|
limit: Max. number of items to retrieve, e.g. 50
|
||||||
"""
|
"""
|
||||||
xbmcplugin.setContent(HANDLE, 'episodes')
|
xbmcplugin.setContent(HANDLE, 'episodes')
|
||||||
appendShowTitle = settings('OnDeckTvAppendShow') == 'true'
|
append_show_title = settings('OnDeckTvAppendShow') == 'true'
|
||||||
appendSxxExx = settings('OnDeckTvAppendSeason') == 'true'
|
append_sxxexx = settings('OnDeckTvAppendSeason') == 'true'
|
||||||
directpaths = settings('useDirectPaths') == 'true'
|
directpaths = settings('useDirectPaths') == 'true'
|
||||||
if settings('OnDeckTVextended') == 'false':
|
if settings('OnDeckTVextended') == 'false':
|
||||||
# Chances are that this view is used on Kodi startup
|
# Chances are that this view is used on Kodi startup
|
||||||
|
@ -609,19 +549,20 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
limitcounter = 0
|
limitcounter = 0
|
||||||
for item in xml:
|
for item in xml:
|
||||||
api = API(item)
|
api = API(item)
|
||||||
listitem = api.CreateListItemFromPlexItem(
|
listitem = api.create_listitem(
|
||||||
appendShowTitle=appendShowTitle,
|
append_show_title=append_show_title,
|
||||||
appendSxxExx=appendSxxExx)
|
append_sxxexx=append_sxxexx)
|
||||||
if directpaths:
|
if directpaths:
|
||||||
url = api.getFilePath()
|
url = api.file_path()
|
||||||
else:
|
else:
|
||||||
params = {
|
url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||||
'mode': "play",
|
% (v.ADDON_ID,
|
||||||
'id': api.getRatingKey(),
|
api.plex_id(),
|
||||||
'dbid': listitem.getProperty('dbid')
|
api.plex_type(),
|
||||||
}
|
api.file_name(force_first_media=True)))
|
||||||
url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \
|
if api.resume_point():
|
||||||
% urlencode(params)
|
listitem.setProperty('resumetime',
|
||||||
|
str(api.resume_point()))
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=HANDLE,
|
handle=HANDLE,
|
||||||
url=url,
|
url=url,
|
||||||
|
@ -644,11 +585,8 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
{'operator': "is", 'field': "tag", 'value': "%s" % tagname}
|
{'operator': "is", 'field': "tag", 'value': "%s" % tagname}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
|
items = js.get_tv_shows(params)
|
||||||
# If we found any, find the oldest unwatched show for each one.
|
if not items:
|
||||||
try:
|
|
||||||
items = result['result'][mediatype]
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
# Now items retrieved - empty directory
|
# Now items retrieved - empty directory
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
xbmcplugin.endOfDirectory(handle=HANDLE)
|
||||||
return
|
return
|
||||||
|
@ -689,33 +627,26 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
count = 0
|
count = 0
|
||||||
for item in items:
|
for item in items:
|
||||||
inprog_params['tvshowid'] = item['tvshowid']
|
inprog_params['tvshowid'] = item['tvshowid']
|
||||||
result = JSONRPC('VideoLibrary.GetEpisodes').execute(inprog_params)
|
episodes = js.get_episodes(inprog_params)
|
||||||
try:
|
if not episodes:
|
||||||
episodes = result['result']['episodes']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
# No, there are no episodes not yet finished. Get "next up"
|
# No, there are no episodes not yet finished. Get "next up"
|
||||||
params['tvshowid'] = item['tvshowid']
|
params['tvshowid'] = item['tvshowid']
|
||||||
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
|
episodes = js.get_episodes(params)
|
||||||
try:
|
if not episodes:
|
||||||
episodes = result['result']['episodes']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
# Also no episodes currently coming up
|
# Also no episodes currently coming up
|
||||||
continue
|
continue
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
# There will always be only 1 episode ('limit=1')
|
# There will always be only 1 episode ('limit=1')
|
||||||
li = createListItem(episode,
|
listitem = createListItem(episode,
|
||||||
appendShowTitle=appendShowTitle,
|
append_show_title=append_show_title,
|
||||||
appendSxxExx=appendSxxExx)
|
append_sxxexx=append_sxxexx)
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
handle=HANDLE,
|
url=episode['file'],
|
||||||
url=episode['file'],
|
listitem=listitem,
|
||||||
listitem=li,
|
isFolder=False)
|
||||||
isFolder=False)
|
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
if count >= limit:
|
if count >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(handle=HANDLE)
|
xbmcplugin.endOfDirectory(handle=HANDLE)
|
||||||
|
|
||||||
|
|
||||||
|
@ -752,10 +683,6 @@ def channels():
|
||||||
"""
|
"""
|
||||||
Listing for Plex Channels
|
Listing for Plex Channels
|
||||||
"""
|
"""
|
||||||
if window('plex_restricteduser') == 'true':
|
|
||||||
log.error('No Plex Channels - restricted user')
|
|
||||||
return xbmcplugin.endOfDirectory(HANDLE, False)
|
|
||||||
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all')
|
xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all')
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
xml[0].attrib
|
||||||
|
@ -888,25 +815,28 @@ def __build_folder(xml_element, plex_section_id=None):
|
||||||
|
|
||||||
def __build_item(xml_element):
|
def __build_item(xml_element):
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
listitem = api.CreateListItemFromPlexItem()
|
listitem = api.create_listitem()
|
||||||
if (api.getKey().startswith('/system/services') or
|
resume = api.resume_point()
|
||||||
api.getKey().startswith('http')):
|
if resume:
|
||||||
|
listitem.setProperty('resumetime', str(resume))
|
||||||
|
if (api.path_and_plex_id().startswith('/system/services') or
|
||||||
|
api.path_and_plex_id().startswith('http')):
|
||||||
params = {
|
params = {
|
||||||
'mode': 'plex_node',
|
'mode': 'plex_node',
|
||||||
'key': xml_element.attrib.get('key'),
|
'key': xml_element.attrib.get('key'),
|
||||||
'view_offset': xml_element.attrib.get('viewOffset', '0'),
|
'offset': xml_element.attrib.get('viewOffset', '0'),
|
||||||
}
|
}
|
||||||
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
||||||
elif api.getType() == v.PLEX_TYPE_PHOTO:
|
elif api.plex_type() == v.PLEX_TYPE_PHOTO:
|
||||||
url = api.get_picture_path()
|
url = api.get_picture_path()
|
||||||
else:
|
else:
|
||||||
params = {
|
url = 'plugin://%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s' \
|
||||||
'mode': 'play',
|
% (v.ADDON_TYPE[api.plex_type()],
|
||||||
'filename': api.getKey(),
|
api.plex_id(),
|
||||||
'id': api.getRatingKey(),
|
api.plex_type(),
|
||||||
'dbid': listitem.getProperty('dbid')
|
api.file_name(force_first_media=True))
|
||||||
}
|
if api.resume_point():
|
||||||
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
listitem.setProperty('resumetime', str(api.resume_point()))
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=url,
|
url=url,
|
||||||
listitem=listitem)
|
listitem=listitem)
|
||||||
|
|
|
@ -1,107 +1,217 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
|
from Queue import Queue
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
import logging
|
from xbmc import executebuiltin, translatePath
|
||||||
import xbmc
|
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
from utils import settings, window, language as lang, tryEncode, \
|
from utils import settings, window, language as lang, try_encode, try_decode, \
|
||||||
advancedsettings_xml
|
XmlKodiSetting, reboot_kodi, dialog
|
||||||
import downloadutils
|
|
||||||
from userclient import UserClient
|
|
||||||
|
|
||||||
from PlexAPI import PlexAPI
|
|
||||||
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
|
|
||||||
import state
|
|
||||||
from migration import check_migration
|
from migration import check_migration
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
from userclient import UserClient
|
||||||
|
from clientinfo import getDeviceId
|
||||||
|
import PlexFunctions as PF
|
||||||
|
import plex_tv
|
||||||
|
import json_rpc as js
|
||||||
|
import playqueue as PQ
|
||||||
|
from videonodes import VideoNodes
|
||||||
|
import state
|
||||||
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class InitialSetup():
|
WINDOW_PROPERTIES = (
|
||||||
|
"plex_online", "plex_serverStatus", "plex_onWake", "plex_kodiScan",
|
||||||
|
"plex_shouldStop", "plex_dbScan", "plex_initialScan",
|
||||||
|
"plex_customplayqueue", "plex_playbackProps", "pms_token", "plex_token",
|
||||||
|
"pms_server", "plex_machineIdentifier", "plex_servername",
|
||||||
|
"plex_authenticated", "PlexUserImage", "useDirectPaths", "countError",
|
||||||
|
"countUnauthorized", "plex_restricteduser", "plex_allows_mediaDeletion",
|
||||||
|
"plex_command", "plex_result", "plex_force_transcode_pix"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reload_pkc():
|
||||||
|
"""
|
||||||
|
Will reload state.py entirely and then initiate some values from the Kodi
|
||||||
|
settings file
|
||||||
|
"""
|
||||||
|
LOG.info('Start (re-)loading PKC settings')
|
||||||
|
# Reset state.py
|
||||||
|
reload(state)
|
||||||
|
# Reset window props
|
||||||
|
for prop in WINDOW_PROPERTIES:
|
||||||
|
window(prop, clear=True)
|
||||||
|
# Clear video nodes properties
|
||||||
|
VideoNodes().clearProperties()
|
||||||
|
|
||||||
|
# Initializing
|
||||||
|
state.VERIFY_SSL_CERT = settings('sslverify') == 'true'
|
||||||
|
state.SSL_CERT_PATH = settings('sslcert') \
|
||||||
|
if settings('sslcert') != 'None' else None
|
||||||
|
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
|
||||||
|
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
||||||
|
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
|
||||||
|
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
|
||||||
|
state.BACKGROUND_SYNC_DISABLED = settings(
|
||||||
|
'enableBackgroundSync') == 'false'
|
||||||
|
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||||
|
settings('backgroundsync_saftyMargin'))
|
||||||
|
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
|
||||||
|
state.REMAP_PATH = settings('remapSMB') == 'true'
|
||||||
|
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
||||||
|
state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number')
|
||||||
|
state.FORCE_RELOAD_SKIN = settings('forceReloadSkinOnPlaybackStop') == 'true'
|
||||||
|
# Init some Queues()
|
||||||
|
state.COMMAND_PIPELINE_QUEUE = Queue()
|
||||||
|
state.COMPANION_QUEUE = Queue(maxsize=100)
|
||||||
|
state.WEBSOCKET_QUEUE = Queue()
|
||||||
|
set_replace_paths()
|
||||||
|
set_webserver()
|
||||||
|
# To detect Kodi profile switches
|
||||||
|
window('plex_kodiProfile',
|
||||||
|
value=try_decode(translatePath("special://profile")))
|
||||||
|
getDeviceId()
|
||||||
|
# Initialize the PKC playqueues
|
||||||
|
PQ.init_playqueues()
|
||||||
|
LOG.info('Done (re-)loading PKC settings')
|
||||||
|
|
||||||
|
|
||||||
|
def set_replace_paths():
|
||||||
|
"""
|
||||||
|
Sets our values for direct paths correctly (including using lower-case
|
||||||
|
protocols like smb:// and NOT SMB://)
|
||||||
|
"""
|
||||||
|
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
|
||||||
|
for arg in ('Org', 'New'):
|
||||||
|
key = 'remapSMB%s%s' % (typus, arg)
|
||||||
|
value = settings(key)
|
||||||
|
if '://' in value:
|
||||||
|
protocol = value.split('://', 1)[0]
|
||||||
|
value = value.replace(protocol, protocol.lower())
|
||||||
|
setattr(state, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def set_webserver():
|
||||||
|
"""
|
||||||
|
Set the Kodi webserver details - used to set the texture cache
|
||||||
|
"""
|
||||||
|
if js.get_setting('services.webserver') in (None, False):
|
||||||
|
# Enable the webserver, it is disabled
|
||||||
|
js.set_setting('services.webserver', True)
|
||||||
|
# Set standard port and username
|
||||||
|
# set_setting('services.webserverport', 8080)
|
||||||
|
# set_setting('services.webserverusername', 'kodi')
|
||||||
|
# Webserver already enabled
|
||||||
|
state.WEBSERVER_PORT = js.get_setting('services.webserverport')
|
||||||
|
state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername')
|
||||||
|
state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword')
|
||||||
|
|
||||||
|
|
||||||
|
def _write_pms_settings(url, token):
|
||||||
|
"""
|
||||||
|
Sets certain settings for server by asking for the PMS' settings
|
||||||
|
Call with url: scheme://ip:port
|
||||||
|
"""
|
||||||
|
xml = PF.get_PMS_settings(url, token)
|
||||||
|
try:
|
||||||
|
xml.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Could not get PMS settings for %s', url)
|
||||||
|
return
|
||||||
|
for entry in xml:
|
||||||
|
if entry.attrib.get('id', '') == 'allowMediaDeletion':
|
||||||
|
settings('plex_allows_mediaDeletion',
|
||||||
|
value=entry.attrib.get('value', 'true'))
|
||||||
|
window('plex_allows_mediaDeletion',
|
||||||
|
value=entry.attrib.get('value', 'true'))
|
||||||
|
|
||||||
|
|
||||||
|
class InitialSetup(object):
|
||||||
|
"""
|
||||||
|
Will load Plex PMS settings (e.g. address) and token
|
||||||
|
Will ask the user initial questions on first PKC boot
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
log.debug('Entering initialsetup class')
|
LOG.debug('Entering initialsetup class')
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
|
||||||
self.plx = PlexAPI()
|
|
||||||
self.dialog = xbmcgui.Dialog()
|
|
||||||
|
|
||||||
self.server = UserClient().getServer()
|
self.server = UserClient().getServer()
|
||||||
self.serverid = settings('plex_machineIdentifier')
|
self.serverid = settings('plex_machineIdentifier')
|
||||||
# Get Plex credentials from settings file, if they exist
|
# Get Plex credentials from settings file, if they exist
|
||||||
plexdict = self.plx.GetPlexLoginFromSettings()
|
plexdict = PF.GetPlexLoginFromSettings()
|
||||||
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
||||||
self.plexLogin = plexdict['plexLogin']
|
self.plex_login = plexdict['plexLogin']
|
||||||
self.plexToken = plexdict['plexToken']
|
self.plex_token = plexdict['plexToken']
|
||||||
self.plexid = plexdict['plexid']
|
self.plexid = plexdict['plexid']
|
||||||
# Token for the PMS, not plex.tv
|
# Token for the PMS, not plex.tv
|
||||||
self.pms_token = settings('accessToken')
|
self.pms_token = settings('accessToken')
|
||||||
if self.plexToken:
|
if self.plex_token:
|
||||||
log.debug('Found a plex.tv token in the settings')
|
LOG.debug('Found a plex.tv token in the settings')
|
||||||
|
|
||||||
def PlexTVSignIn(self):
|
def plex_tv_sign_in(self):
|
||||||
"""
|
"""
|
||||||
Signs (freshly) in to plex.tv (will be saved to file settings)
|
Signs (freshly) in to plex.tv (will be saved to file settings)
|
||||||
|
|
||||||
Returns True if successful, or False if not
|
Returns True if successful, or False if not
|
||||||
"""
|
"""
|
||||||
result = self.plx.PlexTvSignInWithPin()
|
result = plex_tv.sign_in_with_pin()
|
||||||
if result:
|
if result:
|
||||||
self.plexLogin = result['username']
|
self.plex_login = result['username']
|
||||||
self.plexToken = result['token']
|
self.plex_token = result['token']
|
||||||
self.plexid = result['plexid']
|
self.plexid = result['plexid']
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def CheckPlexTVSignIn(self):
|
def check_plex_tv_sign_in(self):
|
||||||
"""
|
"""
|
||||||
Checks existing connection to plex.tv. If not, triggers sign in
|
Checks existing connection to plex.tv. If not, triggers sign in
|
||||||
|
|
||||||
Returns True if signed in, False otherwise
|
Returns True if signed in, False otherwise
|
||||||
"""
|
"""
|
||||||
answer = True
|
answer = True
|
||||||
chk = self.plx.CheckConnection('plex.tv', token=self.plexToken)
|
chk = PF.check_connection('plex.tv', token=self.plex_token)
|
||||||
if chk in (401, 403):
|
if chk in (401, 403):
|
||||||
# HTTP Error: unauthorized. Token is no longer valid
|
# HTTP Error: unauthorized. Token is no longer valid
|
||||||
log.info('plex.tv connection returned HTTP %s' % str(chk))
|
LOG.info('plex.tv connection returned HTTP %s', str(chk))
|
||||||
# Delete token in the settings
|
# Delete token in the settings
|
||||||
settings('plexToken', value='')
|
settings('plexToken', value='')
|
||||||
settings('plexLogin', value='')
|
settings('plexLogin', value='')
|
||||||
# Could not login, please try again
|
# Could not login, please try again
|
||||||
self.dialog.ok(lang(29999), lang(39009))
|
dialog('ok', lang(29999), lang(39009))
|
||||||
answer = self.PlexTVSignIn()
|
answer = self.plex_tv_sign_in()
|
||||||
elif chk is False or chk >= 400:
|
elif chk is False or chk >= 400:
|
||||||
# Problems connecting to plex.tv. Network or internet issue?
|
# Problems connecting to plex.tv. Network or internet issue?
|
||||||
log.info('Problems connecting to plex.tv; connection returned '
|
LOG.info('Problems connecting to plex.tv; connection returned '
|
||||||
'HTTP %s' % str(chk))
|
'HTTP %s', str(chk))
|
||||||
self.dialog.ok(lang(29999), lang(39010))
|
dialog('ok', lang(29999), lang(39010))
|
||||||
answer = False
|
answer = False
|
||||||
else:
|
else:
|
||||||
log.info('plex.tv connection with token successful')
|
LOG.info('plex.tv connection with token successful')
|
||||||
settings('plex_status', value=lang(39227))
|
settings('plex_status', value=lang(39227))
|
||||||
# Refresh the info from Plex.tv
|
# Refresh the info from Plex.tv
|
||||||
xml = self.doUtils('https://plex.tv/users/account',
|
xml = DU().downloadUrl('https://plex.tv/users/account',
|
||||||
authenticate=False,
|
authenticate=False,
|
||||||
headerOptions={'X-Plex-Token': self.plexToken})
|
headerOptions={'X-Plex-Token': self.plex_token})
|
||||||
try:
|
try:
|
||||||
self.plexLogin = xml.attrib['title']
|
self.plex_login = xml.attrib['title']
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
log.error('Failed to update Plex info from plex.tv')
|
LOG.error('Failed to update Plex info from plex.tv')
|
||||||
else:
|
else:
|
||||||
settings('plexLogin', value=self.plexLogin)
|
settings('plexLogin', value=self.plex_login)
|
||||||
home = 'true' if xml.attrib.get('home') == '1' else 'false'
|
home = 'true' if xml.attrib.get('home') == '1' else 'false'
|
||||||
settings('plexhome', value=home)
|
settings('plexhome', value=home)
|
||||||
settings('plexAvatar', value=xml.attrib.get('thumb'))
|
settings('plexAvatar', value=xml.attrib.get('thumb'))
|
||||||
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
|
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
|
||||||
log.info('Updated Plex info from plex.tv')
|
LOG.info('Updated Plex info from plex.tv')
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
def CheckPMS(self):
|
def check_existing_pms(self):
|
||||||
"""
|
"""
|
||||||
Check the PMS that was set in file settings.
|
Check the PMS that was set in file settings.
|
||||||
Will return False if we need to reconnect, because:
|
Will return False if we need to reconnect, because:
|
||||||
|
@ -112,80 +222,80 @@ class InitialSetup():
|
||||||
not set before
|
not set before
|
||||||
"""
|
"""
|
||||||
answer = True
|
answer = True
|
||||||
chk = self.plx.CheckConnection(self.server, verifySSL=False)
|
chk = PF.check_connection(self.server, verifySSL=False)
|
||||||
if chk is False:
|
if chk is False:
|
||||||
log.warn('Could not reach PMS %s' % self.server)
|
LOG.warn('Could not reach PMS %s', self.server)
|
||||||
answer = False
|
answer = False
|
||||||
if answer is True and not self.serverid:
|
if answer is True and not self.serverid:
|
||||||
log.info('No PMS machineIdentifier found for %s. Trying to '
|
LOG.info('No PMS machineIdentifier found for %s. Trying to '
|
||||||
'get the PMS unique ID' % self.server)
|
'get the PMS unique ID', self.server)
|
||||||
self.serverid = GetMachineIdentifier(self.server)
|
self.serverid = PF.GetMachineIdentifier(self.server)
|
||||||
if self.serverid is None:
|
if self.serverid is None:
|
||||||
log.warn('Could not retrieve machineIdentifier')
|
LOG.warn('Could not retrieve machineIdentifier')
|
||||||
answer = False
|
answer = False
|
||||||
else:
|
else:
|
||||||
settings('plex_machineIdentifier', value=self.serverid)
|
settings('plex_machineIdentifier', value=self.serverid)
|
||||||
elif answer is True:
|
elif answer is True:
|
||||||
tempServerid = GetMachineIdentifier(self.server)
|
temp_server_id = PF.GetMachineIdentifier(self.server)
|
||||||
if tempServerid != self.serverid:
|
if temp_server_id != self.serverid:
|
||||||
log.warn('The current PMS %s was expected to have a '
|
LOG.warn('The current PMS %s was expected to have a '
|
||||||
'unique machineIdentifier of %s. But we got '
|
'unique machineIdentifier of %s. But we got '
|
||||||
'%s. Pick a new server to be sure'
|
'%s. Pick a new server to be sure',
|
||||||
% (self.server, self.serverid, tempServerid))
|
self.server, self.serverid, temp_server_id)
|
||||||
answer = False
|
answer = False
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
def _getServerList(self):
|
@staticmethod
|
||||||
|
def _check_pms_connectivity(server):
|
||||||
"""
|
"""
|
||||||
Returns a list of servers from GDM and possibly plex.tv
|
Checks for server's connectivity. Returns check_connection result
|
||||||
"""
|
|
||||||
self.plx.discoverPMS(xbmc.getIPAddress(),
|
|
||||||
plexToken=self.plexToken)
|
|
||||||
serverlist = self.plx.returnServerList(self.plx.g_PMS)
|
|
||||||
log.debug('PMS serverlist: %s' % serverlist)
|
|
||||||
return serverlist
|
|
||||||
|
|
||||||
def _checkServerCon(self, server):
|
|
||||||
"""
|
|
||||||
Checks for server's connectivity. Returns CheckConnection result
|
|
||||||
"""
|
"""
|
||||||
# Re-direct via plex if remote - will lead to the correct SSL
|
# Re-direct via plex if remote - will lead to the correct SSL
|
||||||
# certificate
|
# certificate
|
||||||
if server['local'] == '1':
|
if server['local']:
|
||||||
url = '%s://%s:%s' \
|
url = ('%s://%s:%s'
|
||||||
% (server['scheme'], server['ip'], server['port'])
|
% (server['scheme'], server['ip'], server['port']))
|
||||||
# Deactive SSL verification if the server is local!
|
# Deactive SSL verification if the server is local!
|
||||||
verifySSL = False
|
verifySSL = False
|
||||||
else:
|
else:
|
||||||
url = server['baseURL']
|
url = server['baseURL']
|
||||||
verifySSL = True
|
verifySSL = True
|
||||||
chk = self.plx.CheckConnection(url,
|
chk = PF.check_connection(url,
|
||||||
token=server['accesstoken'],
|
token=server['token'],
|
||||||
verifySSL=verifySSL)
|
verifySSL=verifySSL)
|
||||||
return chk
|
return chk
|
||||||
|
|
||||||
def PickPMS(self, showDialog=False):
|
def pick_pms(self, showDialog=False):
|
||||||
"""
|
"""
|
||||||
Searches for PMS in local Lan and optionally (if self.plexToken set)
|
Searches for PMS in local Lan and optionally (if self.plex_token set)
|
||||||
also on plex.tv
|
also on plex.tv
|
||||||
showDialog=True: let the user pick one
|
showDialog=True: let the user pick one
|
||||||
showDialog=False: automatically pick PMS based on machineIdentifier
|
showDialog=False: automatically pick PMS based on machineIdentifier
|
||||||
|
|
||||||
Returns the picked PMS' detail as a dict:
|
Returns the picked PMS' detail as a dict:
|
||||||
{
|
{
|
||||||
'name': friendlyName, the Plex server's name
|
'machineIdentifier' [str] unique identifier of the PMS
|
||||||
'address': ip:port
|
'name' [str] name of the PMS
|
||||||
'ip': ip, without http/https
|
'token' [str] token needed to access that PMS
|
||||||
'port': port
|
'ownername' [str] name of the owner of this PMS or None if
|
||||||
'scheme': 'http'/'https', nice for checking for secure connections
|
the owner itself supplied tries to connect
|
||||||
'local': '1'/'0', Is the server a local server?
|
'product' e.g. 'Plex Media Server' or None
|
||||||
'owned': '1'/'0', Is the server owned by the user?
|
'version' e.g. '1.11.2.4772-3e...' or None
|
||||||
'machineIdentifier': id, Plex server machine identifier
|
'device': e.g. 'PC' or 'Windows' or None
|
||||||
'accesstoken': token Access token to this server
|
'platform': e.g. 'Windows', 'Android' or None
|
||||||
'baseURL': baseURL scheme://ip:port
|
'local' [bool] True if plex.tv supplied
|
||||||
'ownername' Plex username of PMS owner
|
'publicAddressMatches'='1'
|
||||||
|
or if found using Plex GDM in the local LAN
|
||||||
|
'owned' [bool] True if it's the owner's PMS
|
||||||
|
'relay' [bool] True if plex.tv supplied 'relay'='1'
|
||||||
|
'presence' [bool] True if plex.tv supplied 'presence'='1'
|
||||||
|
'httpsRequired' [bool] True if plex.tv supplied
|
||||||
|
'httpsRequired'='1'
|
||||||
|
'scheme' [str] either 'http' or 'https'
|
||||||
|
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
|
||||||
|
'port': [str] Port of the PMS, e.g. '32400'
|
||||||
|
'baseURL': [str] <scheme>://<ip>:<port> of the PMS
|
||||||
}
|
}
|
||||||
|
|
||||||
or None if unsuccessful
|
or None if unsuccessful
|
||||||
"""
|
"""
|
||||||
server = None
|
server = None
|
||||||
|
@ -193,105 +303,77 @@ class InitialSetup():
|
||||||
if not self.server or not self.serverid:
|
if not self.server or not self.serverid:
|
||||||
showDialog = True
|
showDialog = True
|
||||||
if showDialog is True:
|
if showDialog is True:
|
||||||
server = self._UserPickPMS()
|
server = self._user_pick_pms()
|
||||||
else:
|
else:
|
||||||
server = self._AutoPickPMS()
|
server = self._auto_pick_pms()
|
||||||
if server is not None:
|
if server is not None:
|
||||||
self._write_PMS_settings(server['baseURL'], server['accesstoken'])
|
_write_pms_settings(server['baseURL'], server['token'])
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def _write_PMS_settings(self, url, token):
|
def _auto_pick_pms(self):
|
||||||
"""
|
|
||||||
Sets certain settings for server by asking for the PMS' settings
|
|
||||||
Call with url: scheme://ip:port
|
|
||||||
"""
|
|
||||||
xml = get_PMS_settings(url, token)
|
|
||||||
try:
|
|
||||||
xml.attrib
|
|
||||||
except AttributeError:
|
|
||||||
log.error('Could not get PMS settings for %s' % url)
|
|
||||||
return
|
|
||||||
for entry in xml:
|
|
||||||
if entry.attrib.get('id', '') == 'allowMediaDeletion':
|
|
||||||
settings('plex_allows_mediaDeletion',
|
|
||||||
value=entry.attrib.get('value', 'true'))
|
|
||||||
window('plex_allows_mediaDeletion',
|
|
||||||
value=entry.attrib.get('value', 'true'))
|
|
||||||
|
|
||||||
def _AutoPickPMS(self):
|
|
||||||
"""
|
"""
|
||||||
Will try to pick PMS based on machineIdentifier saved in file settings
|
Will try to pick PMS based on machineIdentifier saved in file settings
|
||||||
but only once
|
but only once
|
||||||
|
|
||||||
Returns server or None if unsuccessful
|
Returns server or None if unsuccessful
|
||||||
"""
|
"""
|
||||||
httpsUpdated = False
|
https_updated = False
|
||||||
checkedPlexTV = False
|
|
||||||
server = None
|
server = None
|
||||||
while True:
|
while True:
|
||||||
if httpsUpdated is False:
|
if https_updated is False:
|
||||||
serverlist = self._getServerList()
|
serverlist = PF.discover_pms(self.plex_token)
|
||||||
for item in serverlist:
|
for item in serverlist:
|
||||||
if item.get('machineIdentifier') == self.serverid:
|
if item.get('machineIdentifier') == self.serverid:
|
||||||
server = item
|
server = item
|
||||||
if server is None:
|
if server is None:
|
||||||
name = settings('plex_servername')
|
name = settings('plex_servername')
|
||||||
log.warn('The PMS you have used before with a unique '
|
LOG.warn('The PMS you have used before with a unique '
|
||||||
'machineIdentifier of %s and name %s is '
|
'machineIdentifier of %s and name %s is '
|
||||||
'offline' % (self.serverid, name))
|
'offline', self.serverid, name)
|
||||||
return
|
return
|
||||||
chk = self._checkServerCon(server)
|
chk = self._check_pms_connectivity(server)
|
||||||
if chk == 504 and httpsUpdated is False:
|
if chk == 504 and https_updated is False:
|
||||||
# Not able to use HTTP, try HTTPs for now
|
# switch HTTPS to HTTP or vice-versa
|
||||||
server['scheme'] = 'https'
|
if server['scheme'] == 'https':
|
||||||
httpsUpdated = True
|
server['scheme'] = 'http'
|
||||||
continue
|
|
||||||
if chk == 401:
|
|
||||||
log.warn('Not yet authorized for Plex server %s'
|
|
||||||
% server['name'])
|
|
||||||
if self.CheckPlexTVSignIn() is True:
|
|
||||||
if checkedPlexTV is False:
|
|
||||||
# Try again
|
|
||||||
checkedPlexTV = True
|
|
||||||
httpsUpdated = False
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
log.warn('Not authorized even though we are signed '
|
|
||||||
' in to plex.tv correctly')
|
|
||||||
self.dialog.ok(lang(29999), '%s %s'
|
|
||||||
% (lang(39214),
|
|
||||||
tryEncode(server['name'])))
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
return
|
server['scheme'] = 'https'
|
||||||
|
https_updated = True
|
||||||
|
continue
|
||||||
# Problems connecting
|
# Problems connecting
|
||||||
elif chk >= 400 or chk is False:
|
elif chk >= 400 or chk is False:
|
||||||
log.warn('Problems connecting to server %s. chk is %s'
|
LOG.warn('Problems connecting to server %s. chk is %s',
|
||||||
% (server['name'], chk))
|
server['name'], chk)
|
||||||
return
|
return
|
||||||
log.info('We found a server to automatically connect to: %s'
|
LOG.info('We found a server to automatically connect to: %s',
|
||||||
% server['name'])
|
server['name'])
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def _UserPickPMS(self):
|
def _user_pick_pms(self):
|
||||||
"""
|
"""
|
||||||
Lets user pick his/her PMS from a list
|
Lets user pick his/her PMS from a list
|
||||||
|
|
||||||
Returns server or None if unsuccessful
|
Returns server or None if unsuccessful
|
||||||
"""
|
"""
|
||||||
httpsUpdated = False
|
https_updated = False
|
||||||
|
# Searching for PMS
|
||||||
|
dialog('notification',
|
||||||
|
heading='{plex}',
|
||||||
|
message=lang(30001),
|
||||||
|
icon='{plex}',
|
||||||
|
time=5000)
|
||||||
while True:
|
while True:
|
||||||
if httpsUpdated is False:
|
if https_updated is False:
|
||||||
serverlist = self._getServerList()
|
serverlist = PF.discover_pms(self.plex_token)
|
||||||
# Exit if no servers found
|
# Exit if no servers found
|
||||||
if len(serverlist) == 0:
|
if not serverlist:
|
||||||
log.warn('No plex media servers found!')
|
LOG.warn('No plex media servers found!')
|
||||||
self.dialog.ok(lang(29999), lang(39011))
|
dialog('ok', lang(29999), lang(39011))
|
||||||
return
|
return
|
||||||
# Get a nicer list
|
# Get a nicer list
|
||||||
dialoglist = []
|
dialoglist = []
|
||||||
for server in serverlist:
|
for server in serverlist:
|
||||||
if server['local'] == '1':
|
if server['local']:
|
||||||
# server is in the same network as client.
|
# server is in the same network as client.
|
||||||
# Add"local"
|
# Add"local"
|
||||||
msg = lang(39022)
|
msg = lang(39022)
|
||||||
|
@ -308,34 +390,34 @@ class InitialSetup():
|
||||||
dialoglist.append('%s (%s)'
|
dialoglist.append('%s (%s)'
|
||||||
% (server['name'], msg))
|
% (server['name'], msg))
|
||||||
# Let user pick server from a list
|
# Let user pick server from a list
|
||||||
resp = self.dialog.select(lang(39012), dialoglist)
|
resp = dialog('select', lang(39012), dialoglist)
|
||||||
if resp == -1:
|
if resp == -1:
|
||||||
# User cancelled
|
# User cancelled
|
||||||
return
|
return
|
||||||
|
|
||||||
server = serverlist[resp]
|
server = serverlist[resp]
|
||||||
chk = self._checkServerCon(server)
|
chk = self._check_pms_connectivity(server)
|
||||||
if chk == 504 and httpsUpdated is False:
|
if chk == 504 and https_updated is False:
|
||||||
# Not able to use HTTP, try HTTPs for now
|
# Not able to use HTTP, try HTTPs for now
|
||||||
serverlist[resp]['scheme'] = 'https'
|
serverlist[resp]['scheme'] = 'https'
|
||||||
httpsUpdated = True
|
https_updated = True
|
||||||
continue
|
continue
|
||||||
httpsUpdated = False
|
https_updated = False
|
||||||
if chk == 401:
|
if chk == 401:
|
||||||
log.warn('Not yet authorized for Plex server %s'
|
LOG.warn('Not yet authorized for Plex server %s',
|
||||||
% server['name'])
|
server['name'])
|
||||||
# Please sign in to plex.tv
|
# Please sign in to plex.tv
|
||||||
self.dialog.ok(lang(29999),
|
dialog('ok',
|
||||||
lang(39013) + server['name'],
|
lang(29999),
|
||||||
lang(39014))
|
lang(39013) + server['name'],
|
||||||
if self.PlexTVSignIn() is False:
|
lang(39014))
|
||||||
|
if self.plex_tv_sign_in() is False:
|
||||||
# Exit while loop if user cancels
|
# Exit while loop if user cancels
|
||||||
return
|
return
|
||||||
# Problems connecting
|
# Problems connecting
|
||||||
elif chk >= 400 or chk is False:
|
elif chk >= 400 or chk is False:
|
||||||
# Problems connecting to server. Pick another server?
|
# Problems connecting to server. Pick another server?
|
||||||
answ = self.dialog.yesno(lang(29999),
|
answ = dialog('yesno', lang(29999), lang(39015))
|
||||||
lang(39015))
|
|
||||||
# Exit while loop if user chooses No
|
# Exit while loop if user chooses No
|
||||||
if not answ:
|
if not answ:
|
||||||
return
|
return
|
||||||
|
@ -343,34 +425,20 @@ class InitialSetup():
|
||||||
else:
|
else:
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def WritePMStoSettings(self, server):
|
@staticmethod
|
||||||
|
def write_pms_to_settings(server):
|
||||||
"""
|
"""
|
||||||
Saves server to file settings. server is a dict of the form:
|
Saves server to file settings
|
||||||
{
|
|
||||||
'name': friendlyName, the Plex server's name
|
|
||||||
'address': ip:port
|
|
||||||
'ip': ip, without http/https
|
|
||||||
'port': port
|
|
||||||
'scheme': 'http'/'https', nice for checking for secure connections
|
|
||||||
'local': '1'/'0', Is the server a local server?
|
|
||||||
'owned': '1'/'0', Is the server owned by the user?
|
|
||||||
'machineIdentifier': id, Plex server machine identifier
|
|
||||||
'accesstoken': token Access token to this server
|
|
||||||
'baseURL': baseURL scheme://ip:port
|
|
||||||
'ownername' Plex username of PMS owner
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
settings('plex_machineIdentifier', server['machineIdentifier'])
|
settings('plex_machineIdentifier', server['machineIdentifier'])
|
||||||
settings('plex_servername', server['name'])
|
settings('plex_servername', server['name'])
|
||||||
settings('plex_serverowned',
|
settings('plex_serverowned', 'true' if server['owned'] else 'false')
|
||||||
'true' if server['owned'] == '1'
|
|
||||||
else 'false')
|
|
||||||
# Careful to distinguish local from remote PMS
|
# Careful to distinguish local from remote PMS
|
||||||
if server['local'] == '1':
|
if server['local']:
|
||||||
scheme = server['scheme']
|
scheme = server['scheme']
|
||||||
settings('ipaddress', server['ip'])
|
settings('ipaddress', server['ip'])
|
||||||
settings('port', server['port'])
|
settings('port', server['port'])
|
||||||
log.debug("Setting SSL verify to false, because server is "
|
LOG.debug("Setting SSL verify to false, because server is "
|
||||||
"local")
|
"local")
|
||||||
settings('sslverify', 'false')
|
settings('sslverify', 'false')
|
||||||
else:
|
else:
|
||||||
|
@ -378,7 +446,7 @@ class InitialSetup():
|
||||||
scheme = baseURL[0]
|
scheme = baseURL[0]
|
||||||
settings('ipaddress', baseURL[1].replace('//', ''))
|
settings('ipaddress', baseURL[1].replace('//', ''))
|
||||||
settings('port', baseURL[2])
|
settings('port', baseURL[2])
|
||||||
log.debug("Setting SSL verify to true, because server is not "
|
LOG.debug("Setting SSL verify to true, because server is not "
|
||||||
"local")
|
"local")
|
||||||
settings('sslverify', 'true')
|
settings('sslverify', 'true')
|
||||||
|
|
||||||
|
@ -387,10 +455,10 @@ class InitialSetup():
|
||||||
else:
|
else:
|
||||||
settings('https', 'false')
|
settings('https', 'false')
|
||||||
# And finally do some logging
|
# And finally do some logging
|
||||||
log.debug("Writing to Kodi user settings file")
|
LOG.debug("Writing to Kodi user settings file")
|
||||||
log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
|
LOG.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s ",
|
||||||
% (server['machineIdentifier'], server['ip'],
|
server['machineIdentifier'], server['ip'], server['port'],
|
||||||
server['port'], server['scheme']))
|
server['scheme'])
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""
|
"""
|
||||||
|
@ -399,99 +467,162 @@ class InitialSetup():
|
||||||
Check server, user, direct paths, music, direct stream if not direct
|
Check server, user, direct paths, music, direct stream if not direct
|
||||||
path.
|
path.
|
||||||
"""
|
"""
|
||||||
log.info("Initial setup called.")
|
LOG.info("Initial setup called.")
|
||||||
dialog = self.dialog
|
try:
|
||||||
|
with XmlKodiSetting('advancedsettings.xml',
|
||||||
# Get current Kodi video cache setting
|
force_create=True,
|
||||||
cache, _ = advancedsettings_xml(['cache', 'memorysize'])
|
top_element='advancedsettings') as xml:
|
||||||
if cache is None:
|
# Get current Kodi video cache setting
|
||||||
# Kodi default cache
|
cache = xml.get_setting(['cache', 'memorysize'])
|
||||||
cache = '20971520'
|
# Disable foreground "Loading media information from files"
|
||||||
else:
|
# (still used by Kodi, even though the Wiki says otherwise)
|
||||||
cache = str(cache.text)
|
xml.set_setting(['musiclibrary', 'backgroundupdate'],
|
||||||
log.info('Current Kodi video memory cache in bytes: %s' % cache)
|
value='true')
|
||||||
|
# Disable cleaning of library - not compatible with PKC
|
||||||
|
xml.set_setting(['videolibrary', 'cleanonupdate'],
|
||||||
|
value='false')
|
||||||
|
# Set completely watched point same as plex (and not 92%)
|
||||||
|
xml.set_setting(['video', 'ignorepercentatend'], value='10')
|
||||||
|
xml.set_setting(['video', 'playcountminimumpercent'],
|
||||||
|
value='90')
|
||||||
|
xml.set_setting(['video', 'ignoresecondsatstart'],
|
||||||
|
value='60')
|
||||||
|
reboot = xml.write_xml
|
||||||
|
except etree.ParseError:
|
||||||
|
cache = None
|
||||||
|
reboot = False
|
||||||
|
# Kodi default cache if no setting is set
|
||||||
|
cache = str(cache.text) if cache is not None else '20971520'
|
||||||
|
LOG.info('Current Kodi video memory cache in bytes: %s', cache)
|
||||||
settings('kodi_video_cache', value=cache)
|
settings('kodi_video_cache', value=cache)
|
||||||
|
|
||||||
|
# Hack to make PKC Kodi master lock compatible
|
||||||
|
try:
|
||||||
|
with XmlKodiSetting('sources.xml',
|
||||||
|
force_create=True,
|
||||||
|
top_element='sources') as xml:
|
||||||
|
root = xml.set_setting(['video'])
|
||||||
|
count = 2
|
||||||
|
for source in root.findall('.//path'):
|
||||||
|
if source.text == "smb://":
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
# sources already set
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Missing smb:// occurences, re-add.
|
||||||
|
for _ in range(0, count):
|
||||||
|
source = etree.SubElement(root, 'source')
|
||||||
|
etree.SubElement(
|
||||||
|
source,
|
||||||
|
'name').text = "PlexKodiConnect Masterlock Hack"
|
||||||
|
etree.SubElement(
|
||||||
|
source,
|
||||||
|
'path',
|
||||||
|
attrib={'pathversion': "1"}).text = "smb://"
|
||||||
|
etree.SubElement(source, 'allowsharing').text = "true"
|
||||||
|
if reboot is False:
|
||||||
|
reboot = xml.write_xml
|
||||||
|
except etree.ParseError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Do we need to migrate stuff?
|
# Do we need to migrate stuff?
|
||||||
check_migration()
|
check_migration()
|
||||||
|
|
||||||
# Optionally sign into plex.tv. Will not be called on very first run
|
# Display a warning if Kodi puts ALL movies into the queue, basically
|
||||||
# as plexToken will be ''
|
# breaking playback reporting for PKC
|
||||||
settings('plex_status', value=lang(39226))
|
if js.settings_getsettingvalue('videoplayer.autoplaynextitem'):
|
||||||
if self.plexToken and self.myplexlogin:
|
LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!')
|
||||||
self.CheckPlexTVSignIn()
|
if settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
|
||||||
|
# Only warn once
|
||||||
|
settings('warned_setting_videoplayer.autoplaynextitem',
|
||||||
|
value='true')
|
||||||
|
# Warning: Kodi setting "Play next video automatically" is
|
||||||
|
# enabled. This could break PKC. Deactivate?
|
||||||
|
if dialog('yesno', lang(29999), lang(30003)):
|
||||||
|
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||||
|
False)
|
||||||
|
# Set any video library updates to happen in the background in order to
|
||||||
|
# hide "Compressing database"
|
||||||
|
js.settings_setsettingvalue('videolibrary.backgroundupdate', True)
|
||||||
|
|
||||||
# If a Plex server IP has already been set
|
# If a Plex server IP has already been set
|
||||||
# return only if the right machine identifier is found
|
# return only if the right machine identifier is found
|
||||||
if self.server:
|
if self.server:
|
||||||
log.info("PMS is already set: %s. Checking now..." % self.server)
|
LOG.info("PMS is already set: %s. Checking now...", self.server)
|
||||||
if self.CheckPMS():
|
if self.check_existing_pms():
|
||||||
log.info("Using PMS %s with machineIdentifier %s"
|
LOG.info("Using PMS %s with machineIdentifier %s",
|
||||||
% (self.server, self.serverid))
|
self.server, self.serverid)
|
||||||
self._write_PMS_settings(self.server, self.pms_token)
|
_write_pms_settings(self.server, self.pms_token)
|
||||||
|
if reboot is True:
|
||||||
|
reboot_kodi()
|
||||||
return
|
return
|
||||||
|
|
||||||
# If not already retrieved myplex info, optionally let user sign in
|
# If not already retrieved myplex info, optionally let user sign in
|
||||||
# to plex.tv. This DOES get called on very first install run
|
# to plex.tv. This DOES get called on very first install run
|
||||||
if not self.plexToken and self.myplexlogin:
|
if not self.plex_token and self.myplexlogin:
|
||||||
self.PlexTVSignIn()
|
self.plex_tv_sign_in()
|
||||||
|
|
||||||
server = self.PickPMS()
|
server = self.pick_pms()
|
||||||
if server is not None:
|
if server is not None:
|
||||||
# Write our chosen server to Kodi settings file
|
# Write our chosen server to Kodi settings file
|
||||||
self.WritePMStoSettings(server)
|
self.write_pms_to_settings(server)
|
||||||
|
|
||||||
# User already answered the installation questions
|
# User already answered the installation questions
|
||||||
if settings('InstallQuestionsAnswered') == 'true':
|
if settings('InstallQuestionsAnswered') == 'true':
|
||||||
|
if reboot is True:
|
||||||
|
reboot_kodi()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Additional settings where the user needs to choose
|
# Additional settings where the user needs to choose
|
||||||
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
||||||
goToSettings = False
|
goto_settings = False
|
||||||
if dialog.yesno(lang(29999),
|
if dialog('yesno',
|
||||||
lang(39027),
|
lang(29999),
|
||||||
lang(39028),
|
lang(39027),
|
||||||
nolabel="Addon (Default)",
|
lang(39028),
|
||||||
yeslabel="Native (Direct Paths)"):
|
nolabel="Addon (Default)",
|
||||||
log.debug("User opted to use direct paths.")
|
yeslabel="Native (Direct Paths)"):
|
||||||
|
LOG.debug("User opted to use direct paths.")
|
||||||
settings('useDirectPaths', value="1")
|
settings('useDirectPaths', value="1")
|
||||||
state.DIRECT_PATHS = True
|
state.DIRECT_PATHS = True
|
||||||
# Are you on a system where you would like to replace paths
|
# Are you on a system where you would like to replace paths
|
||||||
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
|
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
|
||||||
if dialog.yesno(heading=lang(29999), line1=lang(39033)):
|
if dialog('yesno', heading=lang(29999), line1=lang(39033)):
|
||||||
log.debug("User chose to replace paths with smb")
|
LOG.debug("User chose to replace paths with smb")
|
||||||
else:
|
else:
|
||||||
settings('replaceSMB', value="false")
|
settings('replaceSMB', value="false")
|
||||||
|
|
||||||
# complete replace all original Plex library paths with custom SMB
|
# complete replace all original Plex library paths with custom SMB
|
||||||
if dialog.yesno(heading=lang(29999), line1=lang(39043)):
|
if dialog('yesno', heading=lang(29999), line1=lang(39043)):
|
||||||
log.debug("User chose custom smb paths")
|
LOG.debug("User chose custom smb paths")
|
||||||
settings('remapSMB', value="true")
|
settings('remapSMB', value="true")
|
||||||
# Please enter your custom smb paths in the settings under
|
# Please enter your custom smb paths in the settings under
|
||||||
# "Sync Options" and then restart Kodi
|
# "Sync Options" and then restart Kodi
|
||||||
dialog.ok(heading=lang(29999), line1=lang(39044))
|
dialog('ok', heading=lang(29999), line1=lang(39044))
|
||||||
goToSettings = True
|
goto_settings = True
|
||||||
|
|
||||||
# Go to network credentials?
|
# Go to network credentials?
|
||||||
if dialog.yesno(heading=lang(29999),
|
if dialog('yesno',
|
||||||
line1=lang(39029),
|
heading=lang(29999),
|
||||||
line2=lang(39030)):
|
line1=lang(39029),
|
||||||
log.debug("Presenting network credentials dialog.")
|
line2=lang(39030)):
|
||||||
from utils import passwordsXML
|
LOG.debug("Presenting network credentials dialog.")
|
||||||
passwordsXML()
|
from utils import passwords_xml
|
||||||
|
passwords_xml()
|
||||||
# Disable Plex music?
|
# Disable Plex music?
|
||||||
if dialog.yesno(heading=lang(29999), line1=lang(39016)):
|
if dialog('yesno', heading=lang(29999), line1=lang(39016)):
|
||||||
log.debug("User opted to disable Plex music library.")
|
LOG.debug("User opted to disable Plex music library.")
|
||||||
settings('enableMusic', value="false")
|
settings('enableMusic', value="false")
|
||||||
|
|
||||||
# Download additional art from FanArtTV
|
# Download additional art from FanArtTV
|
||||||
if dialog.yesno(heading=lang(29999), line1=lang(39061)):
|
if dialog('yesno', heading=lang(29999), line1=lang(39061)):
|
||||||
log.debug("User opted to use FanArtTV")
|
LOG.debug("User opted to use FanArtTV")
|
||||||
settings('FanartTV', value="true")
|
settings('FanartTV', value="true")
|
||||||
# Do you want to replace your custom user ratings with an indicator of
|
# Do you want to replace your custom user ratings with an indicator of
|
||||||
# how many versions of a media item you posses?
|
# how many versions of a media item you posses?
|
||||||
if dialog.yesno(heading=lang(29999), line1=lang(39718)):
|
if dialog('yesno', heading=lang(29999), line1=lang(39718)):
|
||||||
log.debug("User opted to replace user ratings with version number")
|
LOG.debug("User opted to replace user ratings with version number")
|
||||||
settings('indicate_media_versions', value="true")
|
settings('indicate_media_versions', value="true")
|
||||||
|
|
||||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||||
|
@ -503,10 +634,13 @@ class InitialSetup():
|
||||||
# Make sure that we only ask these questions upon first installation
|
# Make sure that we only ask these questions upon first installation
|
||||||
settings('InstallQuestionsAnswered', value='true')
|
settings('InstallQuestionsAnswered', value='true')
|
||||||
|
|
||||||
if goToSettings is False:
|
if goto_settings is False:
|
||||||
# Open Settings page now? You will need to restart!
|
# Open Settings page now? You will need to restart!
|
||||||
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
|
goto_settings = dialog('yesno',
|
||||||
if goToSettings:
|
heading=lang(29999),
|
||||||
|
line1=lang(39017))
|
||||||
|
if goto_settings:
|
||||||
state.PMS_STATUS = 'Stop'
|
state.PMS_STATUS = 'Stop'
|
||||||
xbmc.executebuiltin(
|
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||||
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
elif reboot is True:
|
||||||
|
reboot_kodi()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
555
resources/lib/json_rpc.py
Normal file
555
resources/lib/json_rpc.py
Normal file
|
@ -0,0 +1,555 @@
|
||||||
|
"""
|
||||||
|
Collection of functions using the Kodi JSON RPC interface.
|
||||||
|
See http://kodi.wiki/view/JSON-RPC_API
|
||||||
|
"""
|
||||||
|
from json import loads, dumps
|
||||||
|
from utils import millis_to_kodi_time
|
||||||
|
from xbmc import executeJSONRPC
|
||||||
|
|
||||||
|
|
||||||
|
class JsonRPC(object):
|
||||||
|
"""
|
||||||
|
Used for all Kodi JSON RPC calls.
|
||||||
|
"""
|
||||||
|
id_ = 1
|
||||||
|
version = "2.0"
|
||||||
|
|
||||||
|
def __init__(self, method, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize with the Kodi method, e.g. 'Player.GetActivePlayers'
|
||||||
|
"""
|
||||||
|
self.method = method
|
||||||
|
self.params = None
|
||||||
|
for arg in kwargs:
|
||||||
|
self.arg = arg
|
||||||
|
|
||||||
|
def _query(self):
|
||||||
|
query = {
|
||||||
|
'jsonrpc': self.version,
|
||||||
|
'id': self.id_,
|
||||||
|
'method': self.method,
|
||||||
|
}
|
||||||
|
if self.params is not None:
|
||||||
|
query['params'] = self.params
|
||||||
|
return dumps(query)
|
||||||
|
|
||||||
|
def execute(self, params=None):
|
||||||
|
"""
|
||||||
|
Pass any params as a dict. Will return Kodi's answer as a dict.
|
||||||
|
"""
|
||||||
|
self.params = params
|
||||||
|
return loads(executeJSONRPC(self._query()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_players():
|
||||||
|
"""
|
||||||
|
Returns all the active Kodi players (usually 3) in a dict:
|
||||||
|
{
|
||||||
|
'video': {'playerid': int, 'type': 'video'}
|
||||||
|
'audio': ...
|
||||||
|
'picture': ...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
ret = {}
|
||||||
|
for player in JsonRPC("Player.GetActivePlayers").execute()['result']:
|
||||||
|
player['playerid'] = int(player['playerid'])
|
||||||
|
ret[player['type']] = player
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_player_ids():
|
||||||
|
"""
|
||||||
|
Returns a list of all the active Kodi player ids (usually 3) as int
|
||||||
|
"""
|
||||||
|
ret = []
|
||||||
|
for player in get_players().values():
|
||||||
|
ret.append(player['playerid'])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_playlist_id(typus):
|
||||||
|
"""
|
||||||
|
Returns the corresponding Kodi playlist id as an int
|
||||||
|
typus: Kodi playlist types: 'video', 'audio' or 'picture'
|
||||||
|
|
||||||
|
Returns None if nothing was found
|
||||||
|
"""
|
||||||
|
for playlist in get_playlists():
|
||||||
|
if playlist.get('type') == typus:
|
||||||
|
return playlist.get('playlistid')
|
||||||
|
|
||||||
|
|
||||||
|
def get_playlists():
|
||||||
|
"""
|
||||||
|
Returns a list of all the Kodi playlists, e.g.
|
||||||
|
[
|
||||||
|
{u'playlistid': 0, u'type': u'audio'},
|
||||||
|
{u'playlistid': 1, u'type': u'video'},
|
||||||
|
{u'playlistid': 2, u'type': u'picture'}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = JsonRPC('Playlist.GetPlaylists').execute()['result']
|
||||||
|
except KeyError:
|
||||||
|
ret = []
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_volume():
|
||||||
|
"""
|
||||||
|
Returns the Kodi volume as an int between 0 (min) and 100 (max)
|
||||||
|
"""
|
||||||
|
return JsonRPC('Application.GetProperties').execute(
|
||||||
|
{"properties": ['volume']})['result']['volume']
|
||||||
|
|
||||||
|
|
||||||
|
def set_volume(volume):
|
||||||
|
"""
|
||||||
|
Set's the volume (for Kodi overall, not only a player).
|
||||||
|
Feed with an int
|
||||||
|
"""
|
||||||
|
return JsonRPC('Application.SetVolume').execute({"volume": volume})
|
||||||
|
|
||||||
|
|
||||||
|
def get_muted():
|
||||||
|
"""
|
||||||
|
Returns True if Kodi is muted, False otherwise
|
||||||
|
"""
|
||||||
|
return JsonRPC('Application.GetProperties').execute(
|
||||||
|
{"properties": ['muted']})['result']['muted']
|
||||||
|
|
||||||
|
|
||||||
|
def play():
|
||||||
|
"""
|
||||||
|
Toggles all Kodi players to play
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.PlayPause").execute({"playerid": playerid,
|
||||||
|
"play": True})
|
||||||
|
|
||||||
|
|
||||||
|
def pause():
|
||||||
|
"""
|
||||||
|
Pauses playback for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.PlayPause").execute({"playerid": playerid,
|
||||||
|
"play": False})
|
||||||
|
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
"""
|
||||||
|
Stops playback for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.Stop").execute({"playerid": playerid})
|
||||||
|
|
||||||
|
|
||||||
|
def seek_to(offset):
|
||||||
|
"""
|
||||||
|
Seeks all Kodi players to offset [int]
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.Seek").execute(
|
||||||
|
{"playerid": playerid,
|
||||||
|
"value": millis_to_kodi_time(offset)})
|
||||||
|
|
||||||
|
|
||||||
|
def smallforward():
|
||||||
|
"""
|
||||||
|
Small step forward for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.Seek").execute({"playerid": playerid,
|
||||||
|
"value": "smallforward"})
|
||||||
|
|
||||||
|
|
||||||
|
def smallbackward():
|
||||||
|
"""
|
||||||
|
Small step backward for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.Seek").execute({"playerid": playerid,
|
||||||
|
"value": "smallbackward"})
|
||||||
|
|
||||||
|
|
||||||
|
def skipnext():
|
||||||
|
"""
|
||||||
|
Skips to the next item to play for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.GoTo").execute({"playerid": playerid,
|
||||||
|
"to": "next"})
|
||||||
|
|
||||||
|
|
||||||
|
def skipprevious():
|
||||||
|
"""
|
||||||
|
Skips to the previous item to play for all Kodi players
|
||||||
|
Using a HACK to make sure we're not just starting same item over again
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
try:
|
||||||
|
skipto(get_position(playerid) - 1)
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def wont_work_skipprevious():
|
||||||
|
"""
|
||||||
|
Skips to the previous item to play for all Kodi players
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.GoTo").execute({"playerid": playerid,
|
||||||
|
"to": "previous"})
|
||||||
|
|
||||||
|
|
||||||
|
def skipto(position):
|
||||||
|
"""
|
||||||
|
Skips to the position [int] of the current playlist
|
||||||
|
"""
|
||||||
|
for playerid in get_player_ids():
|
||||||
|
JsonRPC("Player.GoTo").execute({"playerid": playerid,
|
||||||
|
"to": position})
|
||||||
|
|
||||||
|
|
||||||
|
def input_up():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed up
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Up").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_down():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed down
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Down").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_left():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed left
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Left").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_right():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed left
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Right").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_select():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed select
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Select").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_home():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed home
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Home").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_back():
|
||||||
|
"""
|
||||||
|
Tells Kodi the user pushed back
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.Back").execute()
|
||||||
|
|
||||||
|
|
||||||
|
def input_sendtext(text):
|
||||||
|
"""
|
||||||
|
Tells Kodi the user sent text [unicode]
|
||||||
|
"""
|
||||||
|
return JsonRPC("Input.SendText").execute({'test': text, 'done': False})
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_get_items(playlistid):
|
||||||
|
"""
|
||||||
|
playlistid: [int] id of the Kodi playlist
|
||||||
|
|
||||||
|
Returns a list of Kodi playlist items as dicts with the keys specified in
|
||||||
|
properties. Or an empty list if unsuccessful. Example:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
u'file':u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv',
|
||||||
|
u'title': u'3 Idiots',
|
||||||
|
u'type': u'movie', # IF possible! Else key missing
|
||||||
|
u'id': 3, # IF possible! Else key missing
|
||||||
|
u'label': u'3 Idiots'}]
|
||||||
|
"""
|
||||||
|
reply = JsonRPC('Playlist.GetItems').execute({
|
||||||
|
'playlistid': playlistid,
|
||||||
|
'properties': ['title', 'file']
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
reply = reply['result']['items']
|
||||||
|
except KeyError:
|
||||||
|
reply = []
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_add(playlistid, item):
|
||||||
|
"""
|
||||||
|
Adds an item to the Kodi playlist with id playlistid. item is either the
|
||||||
|
dict
|
||||||
|
{'file': filepath as string}
|
||||||
|
or
|
||||||
|
{kodi_type: kodi_id}
|
||||||
|
|
||||||
|
Returns a dict with the key 'error' if unsuccessful.
|
||||||
|
"""
|
||||||
|
return JsonRPC('Playlist.Add').execute({'playlistid': playlistid,
|
||||||
|
'item': item})
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_insert(params):
|
||||||
|
"""
|
||||||
|
Insert item(s) into playlist. Does not work for picture playlists (aka
|
||||||
|
slideshows). params is the dict
|
||||||
|
{
|
||||||
|
'playlistid': [int]
|
||||||
|
'position': [int]
|
||||||
|
'item': <item>
|
||||||
|
}
|
||||||
|
item is either the dict
|
||||||
|
{'file': filepath as string}
|
||||||
|
or
|
||||||
|
{kodi_type: kodi_id}
|
||||||
|
Returns a dict with the key 'error' if something went wrong.
|
||||||
|
"""
|
||||||
|
return JsonRPC('Playlist.Insert').execute(params)
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_remove(playlistid, position):
|
||||||
|
"""
|
||||||
|
Removes the playlist item at position from the playlist
|
||||||
|
position: [int]
|
||||||
|
|
||||||
|
Returns a dict with the key 'error' if something went wrong.
|
||||||
|
"""
|
||||||
|
return JsonRPC('Playlist.Remove').execute({'playlistid': playlistid,
|
||||||
|
'position': position})
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting(setting):
|
||||||
|
"""
|
||||||
|
Returns the Kodi setting (GetSettingValue), a [str], or None if not
|
||||||
|
possible
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = JsonRPC('Settings.GetSettingValue').execute(
|
||||||
|
{'setting': setting})['result']['value']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = None
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def set_setting(setting, value):
|
||||||
|
"""
|
||||||
|
Sets the Kodi setting, a [str], to value
|
||||||
|
"""
|
||||||
|
return JsonRPC('Settings.SetSettingValue').execute(
|
||||||
|
{'setting': setting, 'value': value})
|
||||||
|
|
||||||
|
|
||||||
|
def get_tv_shows(params):
|
||||||
|
"""
|
||||||
|
Returns a list of tv shows for params (check the Kodi wiki)
|
||||||
|
"""
|
||||||
|
ret = JsonRPC('VideoLibrary.GetTVShows').execute(params)
|
||||||
|
try:
|
||||||
|
ret = ret['result']['tvshows']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = []
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_episodes(params):
|
||||||
|
"""
|
||||||
|
Returns a list of tv show episodes for params (check the Kodi wiki)
|
||||||
|
"""
|
||||||
|
ret = JsonRPC('VideoLibrary.GetEpisodes').execute(params)
|
||||||
|
try:
|
||||||
|
ret = ret['result']['episodes']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = []
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_item(playerid):
|
||||||
|
"""
|
||||||
|
UNRELIABLE on playback startup! (as other JSON and Python Kodi functions)
|
||||||
|
Returns the following for the currently playing item:
|
||||||
|
{
|
||||||
|
u'title': u'Okja',
|
||||||
|
u'type': u'movie',
|
||||||
|
u'id': 258,
|
||||||
|
u'file': u'smb://...movie.mkv',
|
||||||
|
u'label': u'Okja'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return JsonRPC('Player.GetItem').execute({
|
||||||
|
'playerid': playerid,
|
||||||
|
'properties': ['title', 'file']})['result']['item']
|
||||||
|
|
||||||
|
|
||||||
|
def get_player_props(playerid):
|
||||||
|
"""
|
||||||
|
Returns a dict for the active Kodi player with the following values:
|
||||||
|
{
|
||||||
|
'type' [str] the Kodi player type, e.g. 'video'
|
||||||
|
'time' The current item's time in Kodi time
|
||||||
|
'totaltime' The current item's total length in Kodi time
|
||||||
|
'speed' [int] playback speed, 0 is paused, 1 is playing
|
||||||
|
'shuffled' [bool] True if shuffled
|
||||||
|
'repeat' [str] 'off', 'one', 'all'
|
||||||
|
'position' [int] position in playlist (or -1)
|
||||||
|
'playlistid' [int] the Kodi playlist id (or -1)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return JsonRPC('Player.GetProperties').execute({
|
||||||
|
'playerid': playerid,
|
||||||
|
'properties': ['type',
|
||||||
|
'time',
|
||||||
|
'totaltime',
|
||||||
|
'speed',
|
||||||
|
'shuffled',
|
||||||
|
'repeat',
|
||||||
|
'position',
|
||||||
|
'playlistid',
|
||||||
|
'currentvideostream',
|
||||||
|
'currentaudiostream',
|
||||||
|
'subtitleenabled',
|
||||||
|
'currentsubtitle']})['result']
|
||||||
|
|
||||||
|
|
||||||
|
def get_position(playerid):
|
||||||
|
"""
|
||||||
|
Returns the currently playing item's position [int] within the playlist
|
||||||
|
"""
|
||||||
|
return JsonRPC('Player.GetProperties').execute({
|
||||||
|
'playerid': playerid,
|
||||||
|
'properties': ['position']})['result']['position']
|
||||||
|
|
||||||
|
|
||||||
|
def current_audiostream(playerid):
|
||||||
|
"""
|
||||||
|
Returns a dict of the active audiostream for playerid [int]:
|
||||||
|
{
|
||||||
|
'index': [int], audiostream index
|
||||||
|
'language': [str]
|
||||||
|
'name': [str]
|
||||||
|
'codec': [str]
|
||||||
|
'bitrate': [int]
|
||||||
|
'channels': [int]
|
||||||
|
}
|
||||||
|
or an empty dict if unsuccessful
|
||||||
|
"""
|
||||||
|
ret = JsonRPC('Player.GetProperties').execute(
|
||||||
|
{'properties': ['currentaudiostream'], 'playerid': playerid})
|
||||||
|
try:
|
||||||
|
ret = ret['result']['currentaudiostream']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = {}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def current_subtitle(playerid):
|
||||||
|
"""
|
||||||
|
Returns a dict of the active subtitle for playerid [int]:
|
||||||
|
{
|
||||||
|
'index': [int], subtitle index
|
||||||
|
'language': [str]
|
||||||
|
'name': [str]
|
||||||
|
}
|
||||||
|
or an empty dict if unsuccessful
|
||||||
|
"""
|
||||||
|
ret = JsonRPC('Player.GetProperties').execute(
|
||||||
|
{'properties': ['currentsubtitle'], 'playerid': playerid})
|
||||||
|
try:
|
||||||
|
ret = ret['result']['currentsubtitle']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = {}
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def subtitle_enabled(playerid):
|
||||||
|
"""
|
||||||
|
Returns True if a subtitle is enabled, False otherwise
|
||||||
|
"""
|
||||||
|
ret = JsonRPC('Player.GetProperties').execute(
|
||||||
|
{'properties': ['subtitleenabled'], 'playerid': playerid})
|
||||||
|
try:
|
||||||
|
ret = ret['result']['subtitleenabled']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
ret = False
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def ping():
|
||||||
|
"""
|
||||||
|
Pings the JSON RPC interface
|
||||||
|
"""
|
||||||
|
return JsonRPC('JSONRPC.Ping').execute()
|
||||||
|
|
||||||
|
|
||||||
|
def activate_window(window, parameters):
|
||||||
|
"""
|
||||||
|
Pass the parameters as str/unicode to open the corresponding window
|
||||||
|
"""
|
||||||
|
return JsonRPC('GUI.ActivateWindow').execute({'window': window,
|
||||||
|
'parameters': [parameters]})
|
||||||
|
|
||||||
|
|
||||||
|
def settings_getsections():
|
||||||
|
'''
|
||||||
|
Retrieve all Kodi settings sections
|
||||||
|
'''
|
||||||
|
return JsonRPC('Settings.GetSections').execute({'level': 'expert'})
|
||||||
|
|
||||||
|
|
||||||
|
def settings_getcategories():
|
||||||
|
'''
|
||||||
|
Retrieve all Kodi settings categories (one level below sections)
|
||||||
|
'''
|
||||||
|
return JsonRPC('Settings.GetCategories').execute({'level': 'expert'})
|
||||||
|
|
||||||
|
|
||||||
|
def settings_getsettings(filter_params):
|
||||||
|
'''
|
||||||
|
Get all the settings for
|
||||||
|
filter_params = {'category': <str>, 'section': <str>}
|
||||||
|
e.g. = {'category':'videoplayer', 'section':'player'}
|
||||||
|
'''
|
||||||
|
return JsonRPC('Settings.GetSettings').execute({
|
||||||
|
'level': 'expert',
|
||||||
|
'filter': filter_params
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def settings_getsettingvalue(setting):
|
||||||
|
'''
|
||||||
|
Pass in the setting id as a string (as retrieved from settings_getsettings),
|
||||||
|
e.g. 'videoplayer.autoplaynextitem' or None is something went wrong
|
||||||
|
'''
|
||||||
|
ret = JsonRPC('Settings.GetSettingValue').execute({'setting': setting})
|
||||||
|
try:
|
||||||
|
ret = ret['result']['value']
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
ret = None
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def settings_setsettingvalue(setting, value):
|
||||||
|
'''
|
||||||
|
Set the Kodi setting (str) to value (type depends, see JSON wiki)
|
||||||
|
'''
|
||||||
|
return JsonRPC('Settings.SetSettingValue').execute({
|
||||||
|
'setting': setting,
|
||||||
|
'value': value
|
||||||
|
})
|
File diff suppressed because it is too large
Load diff
|
@ -1,31 +1,35 @@
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
PKC Kodi Monitoring implementation
|
||||||
###############################################################################
|
"""
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from json import loads
|
from json import loads
|
||||||
|
from threading import Thread
|
||||||
|
import copy
|
||||||
|
|
||||||
from xbmc import Monitor, Player, sleep
|
from xbmc import Monitor, Player, sleep, getCondVisibility, getInfoLabel, \
|
||||||
|
getLocalizedString
|
||||||
|
from xbmcgui import Window
|
||||||
|
|
||||||
from downloadutils import DownloadUtils
|
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
|
from utils import window, settings, plex_command, thread_methods, try_encode
|
||||||
plex_command
|
|
||||||
from PlexFunctions import scrobble
|
from PlexFunctions import scrobble
|
||||||
from kodidb_functions import get_kodiid_from_filename
|
from kodidb_functions import kodiid_from_filename
|
||||||
from PlexAPI import API
|
from plexbmchelper.subscribers import LOCKER
|
||||||
|
from playback import playback_triage
|
||||||
|
from initialsetup import set_replace_paths
|
||||||
|
import playqueue as PQ
|
||||||
|
import json_rpc as js
|
||||||
|
import playlist_func as PL
|
||||||
import state
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
# settings: window-variable
|
# settings: window-variable
|
||||||
WINDOW_SETTINGS = {
|
WINDOW_SETTINGS = {
|
||||||
'enableContext': 'plex_context',
|
|
||||||
'plex_restricteduser': 'plex_restricteduser',
|
'plex_restricteduser': 'plex_restricteduser',
|
||||||
'force_transcode_pix': 'plex_force_transcode_pix',
|
'force_transcode_pix': 'plex_force_transcode_pix'
|
||||||
'fetch_pms_item_number': 'fetch_pms_item_number'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# settings: state-variable (state.py)
|
# settings: state-variable (state.py)
|
||||||
|
@ -42,28 +46,39 @@ STATE_SETTINGS = {
|
||||||
'remapSMBphotoOrg': 'remapSMBphotoOrg',
|
'remapSMBphotoOrg': 'remapSMBphotoOrg',
|
||||||
'remapSMBphotoNew': 'remapSMBphotoNew',
|
'remapSMBphotoNew': 'remapSMBphotoNew',
|
||||||
'enableMusic': 'ENABLE_MUSIC',
|
'enableMusic': 'ENABLE_MUSIC',
|
||||||
'enableBackgroundSync': 'BACKGROUND_SYNC'
|
'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN',
|
||||||
|
'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER'
|
||||||
}
|
}
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class KodiMonitor(Monitor):
|
class KodiMonitor(Monitor):
|
||||||
|
"""
|
||||||
def __init__(self, callback):
|
PKC implementation of the Kodi Monitor class. Invoke only once.
|
||||||
self.mgr = callback
|
"""
|
||||||
self.doUtils = DownloadUtils().downloadUrl
|
def __init__(self):
|
||||||
self.xbmcplayer = Player()
|
self.xbmcplayer = Player()
|
||||||
self.playqueue = self.mgr.playqueue
|
self._already_slept = False
|
||||||
Monitor.__init__(self)
|
Monitor.__init__(self)
|
||||||
log.info("Kodi monitor started.")
|
for playerid in state.PLAYER_STATES:
|
||||||
|
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||||
|
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||||
|
LOG.info("Kodi monitor started.")
|
||||||
|
|
||||||
def onScanStarted(self, library):
|
def onScanStarted(self, library):
|
||||||
log.debug("Kodi library scan %s running." % library)
|
"""
|
||||||
|
Will be called when Kodi starts scanning the library
|
||||||
|
"""
|
||||||
|
LOG.debug("Kodi library scan %s running.", library)
|
||||||
if library == "video":
|
if library == "video":
|
||||||
window('plex_kodiScan', value="true")
|
window('plex_kodiScan', value="true")
|
||||||
|
|
||||||
def onScanFinished(self, library):
|
def onScanFinished(self, library):
|
||||||
log.debug("Kodi library scan %s finished." % library)
|
"""
|
||||||
|
Will be called when Kodi finished scanning the library
|
||||||
|
"""
|
||||||
|
LOG.debug("Kodi library scan %s finished.", library)
|
||||||
if library == "video":
|
if library == "video":
|
||||||
window('plex_kodiScan', clear=True)
|
window('plex_kodiScan', clear=True)
|
||||||
|
|
||||||
|
@ -71,18 +86,15 @@ class KodiMonitor(Monitor):
|
||||||
"""
|
"""
|
||||||
Monitor the PKC settings for changes made by the user
|
Monitor the PKC settings for changes made by the user
|
||||||
"""
|
"""
|
||||||
log.debug('PKC settings change detected')
|
LOG.debug('PKC settings change detected')
|
||||||
changed = False
|
changed = False
|
||||||
# Reset the window variables from the settings variables
|
# Reset the window variables from the settings variables
|
||||||
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
|
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
|
||||||
if window(window_value) != settings(settings_value):
|
if window(window_value) != settings(settings_value):
|
||||||
changed = True
|
changed = True
|
||||||
log.debug('PKC window settings changed: %s is now %s'
|
LOG.debug('PKC window settings changed: %s is now %s',
|
||||||
% (settings_value, settings(settings_value)))
|
settings_value, settings(settings_value))
|
||||||
window(window_value, value=settings(settings_value))
|
window(window_value, value=settings(settings_value))
|
||||||
if settings_value == 'fetch_pms_item_number':
|
|
||||||
log.info('Requesting playlist/nodes refresh')
|
|
||||||
plex_command('RUN_LIB_SCAN', 'views')
|
|
||||||
# Reset the state variables in state.py
|
# Reset the state variables in state.py
|
||||||
for settings_value, state_name in STATE_SETTINGS.iteritems():
|
for settings_value, state_name in STATE_SETTINGS.iteritems():
|
||||||
new = settings(settings_value)
|
new = settings(settings_value)
|
||||||
|
@ -92,14 +104,22 @@ class KodiMonitor(Monitor):
|
||||||
new = False
|
new = False
|
||||||
if getattr(state, state_name) != new:
|
if getattr(state, state_name) != new:
|
||||||
changed = True
|
changed = True
|
||||||
log.debug('PKC state settings %s changed from %s to %s'
|
LOG.debug('PKC state settings %s changed from %s to %s',
|
||||||
% (settings_value, getattr(state, state_name), new))
|
settings_value, getattr(state, state_name), new)
|
||||||
setattr(state, state_name, new)
|
setattr(state, state_name, new)
|
||||||
|
if state_name == 'FETCH_PMS_ITEM_NUMBER':
|
||||||
|
LOG.info('Requesting playlist/nodes refresh')
|
||||||
|
plex_command('RUN_LIB_SCAN', 'views')
|
||||||
# Special cases, overwrite all internal settings
|
# Special cases, overwrite all internal settings
|
||||||
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval'))*60
|
set_replace_paths()
|
||||||
|
state.BACKGROUND_SYNC_DISABLED = settings(
|
||||||
|
'enableBackgroundSync') == 'false'
|
||||||
|
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
|
||||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||||
settings('backgroundsync_saftyMargin'))
|
settings('backgroundsync_saftyMargin'))
|
||||||
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
||||||
|
state.SSL_CERT_PATH = settings('sslcert') \
|
||||||
|
if settings('sslcert') != 'None' else None
|
||||||
# Never set through the user
|
# Never set through the user
|
||||||
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
||||||
if changed is True:
|
if changed is True:
|
||||||
|
@ -108,183 +128,312 @@ class KodiMonitor(Monitor):
|
||||||
state.STOP_SYNC = False
|
state.STOP_SYNC = False
|
||||||
state.PATH_VERIFIED = False
|
state.PATH_VERIFIED = False
|
||||||
|
|
||||||
@CatchExceptions(warnuser=False)
|
|
||||||
def onNotification(self, sender, method, data):
|
def onNotification(self, sender, method, data):
|
||||||
|
"""
|
||||||
|
Called when a bunch of different stuff happens on the Kodi side
|
||||||
|
"""
|
||||||
if data:
|
if data:
|
||||||
data = loads(data, 'utf-8')
|
data = loads(data, 'utf-8')
|
||||||
log.debug("Method: %s Data: %s" % (method, data))
|
LOG.debug("Method: %s Data: %s", method, data)
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
|
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
# Should refresh our video nodes, e.g. on deck
|
# Should refresh our video nodes, e.g. on deck
|
||||||
# xbmc.executebuiltin('ReloadSkin()')
|
# xbmc.executebuiltin('ReloadSkin()')
|
||||||
pass
|
pass
|
||||||
|
elif method == 'Playlist.OnAdd':
|
||||||
|
self._playlist_onadd(data)
|
||||||
|
elif method == 'Playlist.OnRemove':
|
||||||
|
self._playlist_onremove(data)
|
||||||
|
elif method == 'Playlist.OnClear':
|
||||||
|
self._playlist_onclear(data)
|
||||||
elif method == "VideoLibrary.OnUpdate":
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
# Manually marking as watched/unwatched
|
# Manually marking as watched/unwatched
|
||||||
playcount = data.get('playcount')
|
playcount = data.get('playcount')
|
||||||
item = data.get('item')
|
item = data.get('item')
|
||||||
|
if playcount is None or item is None:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
kodiid = item['id']
|
kodiid = item['id']
|
||||||
item_type = item['type']
|
item_type = item['type']
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
log.info("Item is invalid for playstate update.")
|
LOG.info("Item is invalid for playstate update.")
|
||||||
|
return
|
||||||
|
# Send notification to the server.
|
||||||
|
with plexdb.Get_Plex_DB() as plexcur:
|
||||||
|
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
|
||||||
|
try:
|
||||||
|
itemid = plex_dbitem[0]
|
||||||
|
except TypeError:
|
||||||
|
LOG.error("Could not find itemid in plex database for a "
|
||||||
|
"video library update")
|
||||||
else:
|
else:
|
||||||
# Send notification to the server.
|
# Stop from manually marking as watched unwatched, with
|
||||||
with plexdb.Get_Plex_DB() as plexcur:
|
# actual playback.
|
||||||
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
|
if window('plex_skipWatched%s' % itemid) == "true":
|
||||||
try:
|
# property is set in player.py
|
||||||
itemid = plex_dbitem[0]
|
window('plex_skipWatched%s' % itemid, clear=True)
|
||||||
except TypeError:
|
|
||||||
log.error("Could not find itemid in plex database for a "
|
|
||||||
"video library update")
|
|
||||||
else:
|
else:
|
||||||
# Stop from manually marking as watched unwatched, with
|
# notify the server
|
||||||
# actual playback.
|
if playcount > 0:
|
||||||
if window('plex_skipWatched%s' % itemid) == "true":
|
scrobble(itemid, 'watched')
|
||||||
# property is set in player.py
|
|
||||||
window('plex_skipWatched%s' % itemid, clear=True)
|
|
||||||
else:
|
else:
|
||||||
# notify the server
|
scrobble(itemid, 'unwatched')
|
||||||
if playcount > 0:
|
|
||||||
scrobble(itemid, 'watched')
|
|
||||||
else:
|
|
||||||
scrobble(itemid, 'unwatched')
|
|
||||||
|
|
||||||
elif method == "VideoLibrary.OnRemove":
|
elif method == "VideoLibrary.OnRemove":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif method == "System.OnSleep":
|
elif method == "System.OnSleep":
|
||||||
# Connection is going to sleep
|
# Connection is going to sleep
|
||||||
log.info("Marking the server as offline. SystemOnSleep activated.")
|
LOG.info("Marking the server as offline. SystemOnSleep activated.")
|
||||||
window('plex_online', value="sleep")
|
window('plex_online', value="sleep")
|
||||||
|
|
||||||
elif method == "System.OnWake":
|
elif method == "System.OnWake":
|
||||||
# Allow network to wake up
|
# Allow network to wake up
|
||||||
sleep(10000)
|
sleep(10000)
|
||||||
window('plex_onWake', value="true")
|
window('plex_onWake', value="true")
|
||||||
window('plex_online', value="false")
|
window('plex_online', value="false")
|
||||||
|
|
||||||
elif method == "GUI.OnScreensaverDeactivated":
|
elif method == "GUI.OnScreensaverDeactivated":
|
||||||
if settings('dbSyncScreensaver') == "true":
|
if settings('dbSyncScreensaver') == "true":
|
||||||
sleep(5000)
|
sleep(5000)
|
||||||
plex_command('RUN_LIB_SCAN', 'full')
|
plex_command('RUN_LIB_SCAN', 'full')
|
||||||
|
|
||||||
elif method == "System.OnQuit":
|
elif method == "System.OnQuit":
|
||||||
log.info('Kodi OnQuit detected - shutting down')
|
LOG.info('Kodi OnQuit detected - shutting down')
|
||||||
state.STOP_PKC = True
|
state.STOP_PKC = True
|
||||||
|
|
||||||
def PlayBackStart(self, data):
|
@LOCKER.lockthis
|
||||||
|
def _playlist_onadd(self, data):
|
||||||
"""
|
"""
|
||||||
Called whenever a playback is started
|
Called if an item is added to a Kodi playlist. Example data dict:
|
||||||
|
{
|
||||||
|
u'item': {
|
||||||
|
u'type': u'movie',
|
||||||
|
u'id': 2},
|
||||||
|
u'playlistid': 1,
|
||||||
|
u'position': 0
|
||||||
|
}
|
||||||
|
Will NOT be called if playback initiated by Kodi widgets
|
||||||
"""
|
"""
|
||||||
# Get currently playing file - can take a while. Will be utf-8!
|
if 'id' not in data['item']:
|
||||||
try:
|
return
|
||||||
currentFile = self.xbmcplayer.getPlayingFile()
|
old = state.OLD_PLAYER_STATES[data['playlistid']]
|
||||||
except:
|
if (not state.DIRECT_PATHS and data['position'] == 0 and
|
||||||
currentFile = None
|
not PQ.PLAYQUEUES[data['playlistid']].items and
|
||||||
count = 0
|
data['item']['type'] == old['kodi_type'] and
|
||||||
while currentFile is None:
|
data['item']['id'] == old['kodi_id']):
|
||||||
sleep(100)
|
# Hack we need for RESUMABLE items because Kodi lost the path of the
|
||||||
try:
|
# last played item that is now being replayed (see playback.py's
|
||||||
currentFile = self.xbmcplayer.getPlayingFile()
|
# Player().play()) Also see playqueue.py _compare_playqueues()
|
||||||
except:
|
LOG.info('Detected re-start of playback of last item')
|
||||||
pass
|
kwargs = {
|
||||||
if count == 50:
|
'plex_id': old['plex_id'],
|
||||||
log.info("No current File, cancel OnPlayBackStart...")
|
'plex_type': old['plex_type'],
|
||||||
return
|
'path': old['file'],
|
||||||
else:
|
'resolve': False
|
||||||
count += 1
|
}
|
||||||
# Just to be on the safe side
|
thread = Thread(target=playback_triage, kwargs=kwargs)
|
||||||
currentFile = tryDecode(currentFile)
|
thread.start()
|
||||||
log.debug("Currently playing file is: %s" % currentFile)
|
|
||||||
|
|
||||||
# Get the type of media we're playing
|
|
||||||
try:
|
|
||||||
typus = data['item']['type']
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
log.info("Item is invalid for PMS playstate update.")
|
|
||||||
return
|
return
|
||||||
log.debug("Playing itemtype is (or appears to be): %s" % typus)
|
|
||||||
|
|
||||||
# Try to get a Kodi ID
|
def _playlist_onremove(self, data):
|
||||||
# If PKC was used - native paths, not direct paths
|
"""
|
||||||
plex_id = window('plex_%s.itemid' % tryEncode(currentFile))
|
Called if an item is removed from a Kodi playlist. Example data dict:
|
||||||
# Get rid of the '' if the window property was not set
|
{
|
||||||
plex_id = None if not plex_id else plex_id
|
u'playlistid': 1,
|
||||||
kodiid = None
|
u'position': 0
|
||||||
if plex_id is None:
|
}
|
||||||
log.debug('Did not get Plex id from window properties')
|
"""
|
||||||
try:
|
pass
|
||||||
kodiid = data['item']['id']
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
log.debug('Did not get a Kodi id from Kodi, darn')
|
|
||||||
# For direct paths, if we're not streaming something
|
|
||||||
# When using Widgets, Kodi doesn't tell us shit so we need this hack
|
|
||||||
if (kodiid is None and plex_id is None and typus != 'song'
|
|
||||||
and not currentFile.startswith('http')):
|
|
||||||
(kodiid, typus) = get_kodiid_from_filename(currentFile)
|
|
||||||
if kodiid is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if plex_id is None:
|
@LOCKER.lockthis
|
||||||
# Get Plex' item id
|
def _playlist_onclear(self, data):
|
||||||
with plexdb.Get_Plex_DB() as plexcursor:
|
"""
|
||||||
plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus)
|
Called if a Kodi playlist is cleared. Example data dict:
|
||||||
|
{
|
||||||
|
u'playlistid': 1,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
playqueue = PQ.PLAYQUEUES[data['playlistid']]
|
||||||
|
if not playqueue.is_pkc_clear():
|
||||||
|
playqueue.clear(kodi=False)
|
||||||
|
else:
|
||||||
|
LOG.debug('Detected PKC clear - ignoring')
|
||||||
|
|
||||||
|
def _get_ids(self, kodi_id, kodi_type, path):
|
||||||
|
"""
|
||||||
|
Returns the tuple (plex_id, plex_type) or (None, None)
|
||||||
|
"""
|
||||||
|
# No Kodi id returned by Kodi, even if there is one. Ex: Widgets
|
||||||
|
plex_id = None
|
||||||
|
plex_type = None
|
||||||
|
# If using direct paths and starting playback from a widget
|
||||||
|
if not kodi_id and kodi_type and path:
|
||||||
|
kodi_id = kodiid_from_filename(path, kodi_type)
|
||||||
|
if kodi_id:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
||||||
try:
|
try:
|
||||||
plex_id = plex_dbitem[0]
|
plex_id = plex_dbitem[0]
|
||||||
|
plex_type = plex_dbitem[2]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.info("No Plex id returned for kodiid %s. Aborting playback"
|
# No plex id, hence item not in the library. E.g. clips
|
||||||
" report" % kodiid)
|
pass
|
||||||
return
|
return plex_id, plex_type
|
||||||
log.debug("Found Plex id %s for Kodi id %s for type %s"
|
|
||||||
% (plex_id, kodiid, typus))
|
|
||||||
|
|
||||||
# Switch subtitle tracks if applicable
|
@staticmethod
|
||||||
subtitle = window('plex_%s.subtitle' % tryEncode(currentFile))
|
def _add_remaining_items_to_playlist(playqueue):
|
||||||
if window(tryEncode('plex_%s.playmethod' % currentFile)) \
|
|
||||||
== 'Transcode' and subtitle:
|
|
||||||
if window('plex_%s.subtitle' % currentFile) == 'None':
|
|
||||||
self.xbmcplayer.showSubtitles(False)
|
|
||||||
else:
|
|
||||||
self.xbmcplayer.setSubtitleStream(int(subtitle))
|
|
||||||
|
|
||||||
# Set some stuff if Kodi initiated playback
|
|
||||||
if ((settings('useDirectPaths') == "1" and not typus == "song")
|
|
||||||
or
|
|
||||||
(typus == "song" and settings('enableMusic') == "true")):
|
|
||||||
if self.StartDirectPath(plex_id,
|
|
||||||
typus,
|
|
||||||
tryEncode(currentFile)) is False:
|
|
||||||
log.error('Could not initiate monitoring; aborting')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Save currentFile for cleanup later and to be able to access refs
|
|
||||||
window('plex_lastPlayedFiled', value=currentFile)
|
|
||||||
window('plex_currently_playing_itemid', value=plex_id)
|
|
||||||
window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id)
|
|
||||||
log.info('Finish playback startup')
|
|
||||||
|
|
||||||
def StartDirectPath(self, plex_id, type, currentFile):
|
|
||||||
"""
|
"""
|
||||||
Set some additional stuff if playback was initiated by Kodi, not PKC
|
Adds all but the very first item of the Kodi playlist to the Plex
|
||||||
|
playqueue
|
||||||
"""
|
"""
|
||||||
xml = self.doUtils('{server}/library/metadata/%s' % plex_id)
|
items = js.playlist_get_items(playqueue.playlistid)
|
||||||
|
if not items:
|
||||||
|
LOG.error('Could not retrieve Kodi playlist items')
|
||||||
|
return
|
||||||
|
# Remove first item
|
||||||
|
items.pop(0)
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
for i, item in enumerate(items):
|
||||||
except:
|
PL.add_item_to_PMS_playlist(playqueue, i + 1, kodi_item=item)
|
||||||
log.error('Did not receive a valid XML for plex_id %s.' % plex_id)
|
except PL.PlaylistError:
|
||||||
return False
|
LOG.info('Could not build Plex playlist for: %s', items)
|
||||||
# Setup stuff, because playback was started by Kodi, not PKC
|
|
||||||
api = API(xml[0])
|
def _json_item(self, playerid):
|
||||||
listitem = api.CreateListItemFromPlexItem()
|
"""
|
||||||
api.set_playback_win_props(currentFile, listitem)
|
Uses JSON RPC to get the playing item's info and returns the tuple
|
||||||
if type == "song" and settings('streamMusic') == "true":
|
kodi_id, kodi_type, path
|
||||||
window('plex_%s.playmethod' % currentFile, value="DirectStream")
|
or None each time if not found.
|
||||||
|
"""
|
||||||
|
if not self._already_slept:
|
||||||
|
# SLEEP before calling this for the first time just after playback
|
||||||
|
# start as Kodi updates this info very late!! Might get previous
|
||||||
|
# element otherwise
|
||||||
|
self._already_slept = True
|
||||||
|
sleep(1000)
|
||||||
|
json_item = js.get_item(playerid)
|
||||||
|
LOG.debug('Kodi playing item properties: %s', json_item)
|
||||||
|
return (json_item.get('id'),
|
||||||
|
json_item.get('type'),
|
||||||
|
json_item.get('file'))
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
|
def PlayBackStart(self, data):
|
||||||
|
"""
|
||||||
|
Called whenever playback is started. Example data:
|
||||||
|
{
|
||||||
|
u'item': {u'type': u'movie', u'title': u''},
|
||||||
|
u'player': {u'playerid': 1, u'speed': 1}
|
||||||
|
}
|
||||||
|
Unfortunately when using Widgets, Kodi doesn't tell us shit
|
||||||
|
"""
|
||||||
|
self._already_slept = False
|
||||||
|
# Get the type of media we're playing
|
||||||
|
try:
|
||||||
|
playerid = data['player']['playerid']
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
LOG.info('Aborting playback report - item invalid for updates %s',
|
||||||
|
data)
|
||||||
|
return
|
||||||
|
if playerid == -1:
|
||||||
|
# Kodi might return -1 for "last player"
|
||||||
|
try:
|
||||||
|
playerid = js.get_player_ids()[0]
|
||||||
|
except IndexError:
|
||||||
|
LOG.error('Could not retreive active player - aborting')
|
||||||
|
return
|
||||||
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
|
info = js.get_player_props(playerid)
|
||||||
|
pos = info['position'] if info['position'] != -1 else 0
|
||||||
|
LOG.debug('Detected position %s for %s', pos, playqueue)
|
||||||
|
status = state.PLAYER_STATES[playerid]
|
||||||
|
kodi_id = data.get('id')
|
||||||
|
kodi_type = data.get('type')
|
||||||
|
path = data.get('file')
|
||||||
|
try:
|
||||||
|
item = playqueue.items[pos]
|
||||||
|
except IndexError:
|
||||||
|
# PKC playqueue not yet initialized
|
||||||
|
LOG.debug('Position %s not in PKC playqueue yet', pos)
|
||||||
|
initialize = True
|
||||||
else:
|
else:
|
||||||
window('plex_%s.playmethod' % currentFile, value="DirectPlay")
|
if not kodi_id:
|
||||||
log.debug('Window properties set for direct paths!')
|
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||||
|
if kodi_id and item.kodi_id:
|
||||||
|
if item.kodi_id != kodi_id or item.kodi_type != kodi_type:
|
||||||
|
LOG.debug('Detected different Kodi id')
|
||||||
|
initialize = True
|
||||||
|
else:
|
||||||
|
initialize = False
|
||||||
|
else:
|
||||||
|
# E.g. clips set-up previously with no Kodi DB entry
|
||||||
|
if not path:
|
||||||
|
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||||
|
if item.file != path:
|
||||||
|
LOG.debug('Detected different path')
|
||||||
|
initialize = True
|
||||||
|
else:
|
||||||
|
initialize = False
|
||||||
|
if initialize:
|
||||||
|
LOG.debug('Need to initialize Plex and PKC playqueue')
|
||||||
|
if not kodi_id or not kodi_type:
|
||||||
|
kodi_id, kodi_type, path = self._json_item(playerid)
|
||||||
|
plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path)
|
||||||
|
if not plex_id:
|
||||||
|
LOG.debug('No Plex id obtained - aborting playback report')
|
||||||
|
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||||
|
return
|
||||||
|
item = PL.init_Plex_playlist(playqueue, plex_id=plex_id)
|
||||||
|
# Set the Plex container key (e.g. using the Plex playqueue)
|
||||||
|
container_key = None
|
||||||
|
if info['playlistid'] != -1:
|
||||||
|
# -1 is Kodi's answer if there is no playlist
|
||||||
|
container_key = PQ.PLAYQUEUES[playerid].id
|
||||||
|
if container_key is not None:
|
||||||
|
container_key = '/playQueues/%s' % container_key
|
||||||
|
elif plex_id is not None:
|
||||||
|
container_key = '/library/metadata/%s' % plex_id
|
||||||
|
else:
|
||||||
|
LOG.debug('No need to initialize playqueues')
|
||||||
|
kodi_id = item.kodi_id
|
||||||
|
kodi_type = item.kodi_type
|
||||||
|
plex_id = item.plex_id
|
||||||
|
plex_type = item.plex_type
|
||||||
|
if playqueue.id:
|
||||||
|
container_key = '/playQueues/%s' % playqueue.id
|
||||||
|
else:
|
||||||
|
container_key = '/library/metadata/%s' % plex_id
|
||||||
|
# Remember that this player has been active
|
||||||
|
state.ACTIVE_PLAYERS.append(playerid)
|
||||||
|
status.update(info)
|
||||||
|
LOG.debug('Set the Plex container_key to: %s', container_key)
|
||||||
|
status['container_key'] = container_key
|
||||||
|
status['file'] = path
|
||||||
|
status['kodi_id'] = kodi_id
|
||||||
|
status['kodi_type'] = kodi_type
|
||||||
|
status['plex_id'] = plex_id
|
||||||
|
status['plex_type'] = plex_type
|
||||||
|
status['playmethod'] = item.playmethod
|
||||||
|
status['playcount'] = item.playcount
|
||||||
|
LOG.debug('Set the player state: %s', status)
|
||||||
|
|
||||||
|
|
||||||
|
@thread_methods
|
||||||
|
class SpecialMonitor(Thread):
|
||||||
|
"""
|
||||||
|
Detect the resume dialog for widgets.
|
||||||
|
Could also be used to detect external players (see Emby implementation)
|
||||||
|
"""
|
||||||
|
def run(self):
|
||||||
|
LOG.info("----====# Starting Special Monitor #====----")
|
||||||
|
# "Start from beginning", "Play from beginning"
|
||||||
|
strings = (try_encode(getLocalizedString(12021)),
|
||||||
|
try_encode(getLocalizedString(12023)))
|
||||||
|
while not self.stopped():
|
||||||
|
if getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
|
||||||
|
if getInfoLabel('Control.GetLabel(1002)') in strings:
|
||||||
|
# Remember that the item IS indeed resumable
|
||||||
|
control = int(Window(10106).getFocusId())
|
||||||
|
state.RESUME_PLAYBACK = True if control == 1001 else False
|
||||||
|
else:
|
||||||
|
# Different context menu is displayed
|
||||||
|
state.RESUME_PLAYBACK = False
|
||||||
|
sleep(200)
|
||||||
|
LOG.info("#====---- Special Monitor Stopped ----====#")
|
||||||
|
|
|
@ -54,14 +54,14 @@ class Process_Fanart_Thread(Thread):
|
||||||
Do the work
|
Do the work
|
||||||
"""
|
"""
|
||||||
log.debug("---===### Starting FanartSync ###===---")
|
log.debug("---===### Starting FanartSync ###===---")
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
queue = self.queue
|
queue = self.queue
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
# Set in service.py
|
# Set in service.py
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
log.info("---===### Stopped FanartSync ###===---")
|
log.info("---===### Stopped FanartSync ###===---")
|
||||||
return
|
return
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Threaded_Get_Metadata(Thread):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
if self.thread_stopped():
|
if self.stopped():
|
||||||
# Shutdown from outside requested; purge out_queue as well
|
# Shutdown from outside requested; purge out_queue as well
|
||||||
while not self.out_queue.empty():
|
while not self.out_queue.empty():
|
||||||
# Still try because remaining item might have been taken
|
# Still try because remaining item might have been taken
|
||||||
|
@ -78,8 +78,8 @@ class Threaded_Get_Metadata(Thread):
|
||||||
# cache local variables because it's faster
|
# cache local variables because it's faster
|
||||||
queue = self.queue
|
queue = self.queue
|
||||||
out_queue = self.out_queue
|
out_queue = self.out_queue
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
while thread_stopped() is False:
|
while stopped() is False:
|
||||||
# grabs Plex item from queue
|
# grabs Plex item from queue
|
||||||
try:
|
try:
|
||||||
item = queue.get(block=False)
|
item = queue.get(block=False)
|
||||||
|
|
|
@ -68,9 +68,9 @@ class Threaded_Process_Metadata(Thread):
|
||||||
item_fct = getattr(itemtypes, self.item_type)
|
item_fct = getattr(itemtypes, self.item_type)
|
||||||
# cache local variables because it's faster
|
# cache local variables because it's faster
|
||||||
queue = self.queue
|
queue = self.queue
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
with item_fct() as item_class:
|
with item_fct() as item_class:
|
||||||
while thread_stopped() is False:
|
while stopped() is False:
|
||||||
# grabs item from queue
|
# grabs item from queue
|
||||||
try:
|
try:
|
||||||
item = queue.get(block=False)
|
item = queue.get(block=False)
|
||||||
|
|
|
@ -52,14 +52,13 @@ class Threaded_Show_Sync_Info(Thread):
|
||||||
# cache local variables because it's faster
|
# cache local variables because it's faster
|
||||||
total = self.total
|
total = self.total
|
||||||
dialog = DialogProgressBG('dialoglogProgressBG')
|
dialog = DialogProgressBG('dialoglogProgressBG')
|
||||||
thread_stopped = self.thread_stopped
|
|
||||||
dialog.create("%s %s: %s %s"
|
dialog.create("%s %s: %s %s"
|
||||||
% (lang(39714), self.item_type, str(total), lang(39715)))
|
% (lang(39714), self.item_type, str(total), lang(39715)))
|
||||||
player = Player()
|
player = Player()
|
||||||
|
|
||||||
total = 2 * total
|
total = 2 * total
|
||||||
totalProgress = 0
|
totalProgress = 0
|
||||||
while thread_stopped() is False and not player.isPlaying():
|
while self.stopped() is False and not player.isPlaying():
|
||||||
with LOCK:
|
with LOCK:
|
||||||
get_progress = GET_METADATA_COUNT
|
get_progress = GET_METADATA_COUNT
|
||||||
process_progress = PROCESS_METADATA_COUNT
|
process_progress = PROCESS_METADATA_COUNT
|
||||||
|
|
|
@ -8,16 +8,17 @@ from random import shuffle
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcvfs import exists
|
from xbmcvfs import exists
|
||||||
|
|
||||||
from utils import window, settings, getUnixTimestamp, sourcesXML,\
|
from utils import window, settings, unix_timestamp, thread_methods, \
|
||||||
thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\
|
create_actor_db_index, dialog, log_time, playlist_xsp, language as lang, \
|
||||||
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
|
unix_date_to_kodi, reset, try_decode, delete_playlists, delete_nodes, \
|
||||||
tryDecode, deletePlaylists, deleteNodes, tryEncode, compare_version
|
try_encode, compare_version
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import itemtypes
|
import itemtypes
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
import kodidb_functions as kodidb
|
import kodidb_functions as kodidb
|
||||||
import userclient
|
import userclient
|
||||||
import videonodes
|
import videonodes
|
||||||
|
import json_rpc as js
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
|
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
|
||||||
|
@ -42,42 +43,19 @@ log = getLogger("PLEX."+__name__)
|
||||||
class LibrarySync(Thread):
|
class LibrarySync(Thread):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
def __init__(self, callback=None):
|
def __init__(self):
|
||||||
self.mgr = callback
|
|
||||||
|
|
||||||
self.itemsToProcess = []
|
self.itemsToProcess = []
|
||||||
self.sessionKeys = []
|
self.sessionKeys = {}
|
||||||
self.fanartqueue = Queue.Queue()
|
self.fanartqueue = Queue.Queue()
|
||||||
if settings('FanartTV') == 'true':
|
if settings('FanartTV') == 'true':
|
||||||
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
|
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
|
||||||
# How long should we wait at least to process new/changed PMS items?
|
# How long should we wait at least to process new/changed PMS items?
|
||||||
|
|
||||||
self.user = userclient.UserClient()
|
self.user = userclient.UserClient()
|
||||||
self.vnodes = videonodes.VideoNodes()
|
self.vnodes = videonodes.VideoNodes()
|
||||||
self.xbmcplayer = xbmc.Player()
|
self.xbmcplayer = xbmc.Player()
|
||||||
|
|
||||||
self.installSyncDone = settings('SyncInstallRunDone') == 'true'
|
self.installSyncDone = settings('SyncInstallRunDone') == 'true'
|
||||||
|
|
||||||
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
|
|
||||||
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
|
|
||||||
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
|
|
||||||
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
|
|
||||||
state.BACKGROUND_SYNC = settings(
|
|
||||||
'enableBackgroundSync') == 'true'
|
|
||||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
|
||||||
settings('backgroundsync_saftyMargin'))
|
|
||||||
|
|
||||||
# Show sync dialog even if user deactivated?
|
# Show sync dialog even if user deactivated?
|
||||||
self.force_dialog = True
|
self.force_dialog = True
|
||||||
# Init for replacing paths
|
|
||||||
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
|
|
||||||
state.REMAP_PATH = settings('remapSMB') == 'true'
|
|
||||||
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
|
|
||||||
for arg in ('Org', 'New'):
|
|
||||||
key = 'remapSMB%s%s' % (typus, arg)
|
|
||||||
setattr(state, key, settings(key))
|
|
||||||
# Just in case a time sync goes wrong
|
|
||||||
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
|
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def showKodiNote(self, message, icon="plex"):
|
def showKodiNote(self, message, icon="plex"):
|
||||||
|
@ -177,7 +155,7 @@ class LibrarySync(Thread):
|
||||||
log.debug('No timestamp; using 0')
|
log.debug('No timestamp; using 0')
|
||||||
|
|
||||||
# Set the timer
|
# Set the timer
|
||||||
koditime = getUnixTimestamp()
|
koditime = unix_timestamp()
|
||||||
# Toggle watched state
|
# Toggle watched state
|
||||||
scrobble(plexId, 'watched')
|
scrobble(plexId, 'watched')
|
||||||
# Let the PMS process this first!
|
# Let the PMS process this first!
|
||||||
|
@ -240,11 +218,13 @@ class LibrarySync(Thread):
|
||||||
# Create an index for actors to speed up sync
|
# Create an index for actors to speed up sync
|
||||||
create_actor_db_index()
|
create_actor_db_index()
|
||||||
|
|
||||||
@LogTime
|
@log_time
|
||||||
def fullSync(self, repair=False):
|
def fullSync(self, repair=False):
|
||||||
"""
|
"""
|
||||||
repair=True: force sync EVERY item
|
repair=True: force sync EVERY item
|
||||||
"""
|
"""
|
||||||
|
# Reset our keys
|
||||||
|
self.sessionKeys = {}
|
||||||
# self.compare == False: we're syncing EVERY item
|
# self.compare == False: we're syncing EVERY item
|
||||||
# True: we're syncing only the delta, e.g. different checksum
|
# True: we're syncing only the delta, e.g. different checksum
|
||||||
self.compare = not repair
|
self.compare = not repair
|
||||||
|
@ -263,20 +243,14 @@ class LibrarySync(Thread):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _fullSync(self):
|
def _fullSync(self):
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
|
||||||
screensaver = getScreensaver()
|
|
||||||
setScreensaver(value="")
|
|
||||||
|
|
||||||
if self.new_items_only is True:
|
if self.new_items_only is True:
|
||||||
# Only do the following once for new items
|
|
||||||
# Add sources
|
|
||||||
sourcesXML()
|
|
||||||
|
|
||||||
# Set views. Abort if unsuccessful
|
# Set views. Abort if unsuccessful
|
||||||
if not self.maintainViews():
|
if not self.maintainViews():
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
|
||||||
setScreensaver(value=screensaver)
|
|
||||||
return False
|
return False
|
||||||
|
# Delete all existing resume points first
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
# Setup the paths for addon-paths (even when using direct paths)
|
||||||
|
kodi_db.setup_path_table()
|
||||||
|
|
||||||
process = {
|
process = {
|
||||||
'movies': self.PlexMovies,
|
'movies': self.PlexMovies,
|
||||||
|
@ -287,11 +261,9 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
# Do the processing
|
# Do the processing
|
||||||
for itemtype in process:
|
for itemtype in process:
|
||||||
if (self.thread_stopped() or
|
if (self.stopped() or
|
||||||
self.thread_suspended() or
|
self.suspended() or
|
||||||
not process[itemtype]()):
|
not process[itemtype]()):
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
|
||||||
setScreensaver(value=screensaver)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Let kodi update the views in any case, since we're doing a full sync
|
# Let kodi update the views in any case, since we're doing a full sync
|
||||||
|
@ -300,8 +272,6 @@ class LibrarySync(Thread):
|
||||||
xbmc.executebuiltin('UpdateLibrary(music)')
|
xbmc.executebuiltin('UpdateLibrary(music)')
|
||||||
|
|
||||||
window('plex_initialScan', clear=True)
|
window('plex_initialScan', clear=True)
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
|
||||||
setScreensaver(value=screensaver)
|
|
||||||
if window('plex_scancrashed') == 'true':
|
if window('plex_scancrashed') == 'true':
|
||||||
# Show warning if itemtypes.py crashed at some point
|
# Show warning if itemtypes.py crashed at some point
|
||||||
dialog('ok', heading='{plex}', line1=lang(39408))
|
dialog('ok', heading='{plex}', line1=lang(39408))
|
||||||
|
@ -311,16 +281,6 @@ class LibrarySync(Thread):
|
||||||
if state.PMS_STATUS not in ('401', 'Auth'):
|
if state.PMS_STATUS not in ('401', 'Auth'):
|
||||||
# Plex server had too much and returned ERROR
|
# Plex server had too much and returned ERROR
|
||||||
dialog('ok', heading='{plex}', line1=lang(39409))
|
dialog('ok', heading='{plex}', line1=lang(39409))
|
||||||
|
|
||||||
# Path hack, so Kodis Information screen works
|
|
||||||
with kodidb.GetKodiDB('video') as kodi_db:
|
|
||||||
try:
|
|
||||||
kodi_db.pathHack()
|
|
||||||
log.info('Path hack successful')
|
|
||||||
except Exception as e:
|
|
||||||
# Empty movies, tv shows?
|
|
||||||
log.error('Path hack failed with error message: %s' % str(e))
|
|
||||||
setScreensaver(value=screensaver)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def processView(self, folderItem, kodi_db, plex_db, totalnodes):
|
def processView(self, folderItem, kodi_db, plex_db, totalnodes):
|
||||||
|
@ -354,7 +314,7 @@ class LibrarySync(Thread):
|
||||||
# Create playlist for the video library
|
# Create playlist for the video library
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and
|
||||||
mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype, foldername, folderid, viewtype)
|
playlist_xsp(mediatype, foldername, folderid, viewtype)
|
||||||
playlists.append(foldername)
|
playlists.append(foldername)
|
||||||
# Create the video node
|
# Create the video node
|
||||||
if (foldername not in nodes and
|
if (foldername not in nodes and
|
||||||
|
@ -396,7 +356,7 @@ class LibrarySync(Thread):
|
||||||
# The tag could be a combined view. Ensure there's
|
# The tag could be a combined view. Ensure there's
|
||||||
# no other tags with the same name before deleting
|
# no other tags with the same name before deleting
|
||||||
# playlist.
|
# playlist.
|
||||||
playlistXSP(mediatype,
|
playlist_xsp(mediatype,
|
||||||
current_viewname,
|
current_viewname,
|
||||||
folderid,
|
folderid,
|
||||||
current_viewtype,
|
current_viewtype,
|
||||||
|
@ -413,7 +373,7 @@ class LibrarySync(Thread):
|
||||||
# Added new playlist
|
# Added new playlist
|
||||||
if (foldername not in playlists and mediatype in
|
if (foldername not in playlists and mediatype in
|
||||||
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlist_xsp(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
viewtype)
|
viewtype)
|
||||||
|
@ -439,7 +399,7 @@ class LibrarySync(Thread):
|
||||||
if mediatype != v.PLEX_TYPE_ARTIST:
|
if mediatype != v.PLEX_TYPE_ARTIST:
|
||||||
if (foldername not in playlists and mediatype in
|
if (foldername not in playlists and mediatype in
|
||||||
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlist_xsp(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
viewtype)
|
viewtype)
|
||||||
|
@ -460,14 +420,8 @@ class LibrarySync(Thread):
|
||||||
Compare the views to Plex
|
Compare the views to Plex
|
||||||
"""
|
"""
|
||||||
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
|
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
|
||||||
if music.set_excludefromscan_music_folders() is True:
|
# Will reboot Kodi is new library detected
|
||||||
log.info('Detected new Music library - restarting now')
|
music.excludefromscan_music_folders()
|
||||||
# 'New Plex music library detected. Sorry, but we need to
|
|
||||||
# restart Kodi now due to the changes made.'
|
|
||||||
dialog('ok', heading='{plex}', line1=lang(39711))
|
|
||||||
from xbmc import executebuiltin
|
|
||||||
executebuiltin('RestartApp')
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.views = []
|
self.views = []
|
||||||
vnodes = self.vnodes
|
vnodes = self.vnodes
|
||||||
|
@ -599,7 +553,7 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
Output: self.updatelist, self.allPlexElementsId
|
Output: self.updatelist, self.allPlexElementsId
|
||||||
self.updatelist APPENDED(!!) list itemids (Plex Keys as
|
self.updatelist APPENDED(!!) list itemids (Plex Keys as
|
||||||
as received from API.getRatingKey())
|
as received from API.plex_id())
|
||||||
One item in this list is of the form:
|
One item in this list is of the form:
|
||||||
'itemId': xxx,
|
'itemId': xxx,
|
||||||
'itemType': 'Movies','TVShows', ...
|
'itemType': 'Movies','TVShows', ...
|
||||||
|
@ -736,7 +690,7 @@ class LibrarySync(Thread):
|
||||||
for thread in threads:
|
for thread in threads:
|
||||||
# Threads might already have quit by themselves (e.g. Kodi exit)
|
# Threads might already have quit by themselves (e.g. Kodi exit)
|
||||||
try:
|
try:
|
||||||
thread.stop_thread()
|
thread.stop()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
log.debug("Stop sent to all threads")
|
log.debug("Stop sent to all threads")
|
||||||
|
@ -758,7 +712,7 @@ class LibrarySync(Thread):
|
||||||
})
|
})
|
||||||
self.updatelist = []
|
self.updatelist = []
|
||||||
|
|
||||||
@LogTime
|
@log_time
|
||||||
def PlexMovies(self):
|
def PlexMovies(self):
|
||||||
# Initialize
|
# Initialize
|
||||||
self.allPlexElementsId = {}
|
self.allPlexElementsId = {}
|
||||||
|
@ -775,7 +729,7 @@ class LibrarySync(Thread):
|
||||||
# Pull the list of movies and boxsets in Kodi
|
# Pull the list of movies and boxsets in Kodi
|
||||||
try:
|
try:
|
||||||
self.allKodiElementsId = dict(
|
self.allKodiElementsId = dict(
|
||||||
plex_db.getChecksum(v.PLEX_TYPE_MOVIE))
|
plex_db.checksum(v.PLEX_TYPE_MOVIE))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
|
|
||||||
|
@ -784,7 +738,7 @@ class LibrarySync(Thread):
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.installSyncDone is not True:
|
if self.installSyncDone is not True:
|
||||||
state.PATH_VERIFIED = False
|
state.PATH_VERIFIED = False
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
# Get items per view
|
# Get items per view
|
||||||
viewId = view['id']
|
viewId = view['id']
|
||||||
|
@ -804,7 +758,7 @@ class LibrarySync(Thread):
|
||||||
self.GetAndProcessXMLs(itemType)
|
self.GetAndProcessXMLs(itemType)
|
||||||
# Update viewstate for EVERY item
|
# Update viewstate for EVERY item
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
self.PlexUpdateWatched(view['id'], itemType)
|
self.PlexUpdateWatched(view['id'], itemType)
|
||||||
|
|
||||||
|
@ -850,7 +804,7 @@ class LibrarySync(Thread):
|
||||||
with itemMth() as method:
|
with itemMth() as method:
|
||||||
method.updateUserdata(xml)
|
method.updateUserdata(xml)
|
||||||
|
|
||||||
@LogTime
|
@log_time
|
||||||
def PlexTVShows(self):
|
def PlexTVShows(self):
|
||||||
# Initialize
|
# Initialize
|
||||||
self.allPlexElementsId = {}
|
self.allPlexElementsId = {}
|
||||||
|
@ -867,7 +821,7 @@ class LibrarySync(Thread):
|
||||||
v.PLEX_TYPE_SEASON,
|
v.PLEX_TYPE_SEASON,
|
||||||
v.PLEX_TYPE_EPISODE):
|
v.PLEX_TYPE_EPISODE):
|
||||||
try:
|
try:
|
||||||
elements = dict(plex.getChecksum(kind))
|
elements = dict(plex.checksum(kind))
|
||||||
self.allKodiElementsId.update(elements)
|
self.allKodiElementsId.update(elements)
|
||||||
# Yet empty/not yet synched
|
# Yet empty/not yet synched
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -878,7 +832,7 @@ class LibrarySync(Thread):
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.installSyncDone is not True:
|
if self.installSyncDone is not True:
|
||||||
state.PATH_VERIFIED = False
|
state.PATH_VERIFIED = False
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
# Get items per view
|
# Get items per view
|
||||||
viewId = view['id']
|
viewId = view['id']
|
||||||
|
@ -907,7 +861,7 @@ class LibrarySync(Thread):
|
||||||
# PROCESS TV Seasons #####
|
# PROCESS TV Seasons #####
|
||||||
# Cycle through tv shows
|
# Cycle through tv shows
|
||||||
for tvShowId in allPlexTvShowsId:
|
for tvShowId in allPlexTvShowsId:
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
# Grab all seasons to tvshow from PMS
|
# Grab all seasons to tvshow from PMS
|
||||||
seasons = GetAllPlexChildren(tvShowId)
|
seasons = GetAllPlexChildren(tvShowId)
|
||||||
|
@ -932,7 +886,7 @@ class LibrarySync(Thread):
|
||||||
# PROCESS TV Episodes #####
|
# PROCESS TV Episodes #####
|
||||||
# Cycle through tv shows
|
# Cycle through tv shows
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
# Grab all episodes to tvshow from PMS
|
# Grab all episodes to tvshow from PMS
|
||||||
episodes = GetAllPlexLeaves(view['id'])
|
episodes = GetAllPlexLeaves(view['id'])
|
||||||
|
@ -967,7 +921,7 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
# Update viewstate:
|
# Update viewstate:
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
self.PlexUpdateWatched(view['id'], itemType)
|
self.PlexUpdateWatched(view['id'], itemType)
|
||||||
|
|
||||||
|
@ -980,7 +934,7 @@ class LibrarySync(Thread):
|
||||||
log.info("%s sync is finished." % itemType)
|
log.info("%s sync is finished." % itemType)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@LogTime
|
@log_time
|
||||||
def PlexMusic(self):
|
def PlexMusic(self):
|
||||||
itemType = 'Music'
|
itemType = 'Music'
|
||||||
|
|
||||||
|
@ -1004,7 +958,7 @@ class LibrarySync(Thread):
|
||||||
for kind in (v.PLEX_TYPE_ARTIST,
|
for kind in (v.PLEX_TYPE_ARTIST,
|
||||||
v.PLEX_TYPE_ALBUM,
|
v.PLEX_TYPE_ALBUM,
|
||||||
v.PLEX_TYPE_SONG):
|
v.PLEX_TYPE_SONG):
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
log.debug("Start processing music %s" % kind)
|
log.debug("Start processing music %s" % kind)
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
|
@ -1021,7 +975,7 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
# Update viewstate for EVERY item
|
# Update viewstate for EVERY item
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
self.PlexUpdateWatched(view['id'], itemType)
|
self.PlexUpdateWatched(view['id'], itemType)
|
||||||
|
|
||||||
|
@ -1040,7 +994,7 @@ class LibrarySync(Thread):
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
# Pull the list of items already in Kodi
|
# Pull the list of items already in Kodi
|
||||||
try:
|
try:
|
||||||
elements = dict(plex_db.getChecksum(kind))
|
elements = dict(plex_db.checksum(kind))
|
||||||
self.allKodiElementsId.update(elements)
|
self.allKodiElementsId.update(elements)
|
||||||
# Yet empty/nothing yet synched
|
# Yet empty/nothing yet synched
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1048,7 +1002,7 @@ class LibrarySync(Thread):
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.installSyncDone is not True:
|
if self.installSyncDone is not True:
|
||||||
state.PATH_VERIFIED = False
|
state.PATH_VERIFIED = False
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
return False
|
return False
|
||||||
# Get items per view
|
# Get items per view
|
||||||
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
|
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
|
||||||
|
@ -1133,10 +1087,10 @@ class LibrarySync(Thread):
|
||||||
"""
|
"""
|
||||||
self.videoLibUpdate = False
|
self.videoLibUpdate = False
|
||||||
self.musicLibUpdate = False
|
self.musicLibUpdate = False
|
||||||
now = getUnixTimestamp()
|
now = unix_timestamp()
|
||||||
deleteListe = []
|
deleteListe = []
|
||||||
for i, item in enumerate(self.itemsToProcess):
|
for i, item in enumerate(self.itemsToProcess):
|
||||||
if self.thread_stopped() or self.thread_suspended():
|
if self.stopped() or self.suspended():
|
||||||
# Chances are that Kodi gets shut down
|
# Chances are that Kodi gets shut down
|
||||||
break
|
break
|
||||||
if item['state'] == 9:
|
if item['state'] == 9:
|
||||||
|
@ -1215,7 +1169,8 @@ class LibrarySync(Thread):
|
||||||
elif item['type'] in (v.PLEX_TYPE_SHOW,
|
elif item['type'] in (v.PLEX_TYPE_SHOW,
|
||||||
v.PLEX_TYPE_SEASON,
|
v.PLEX_TYPE_SEASON,
|
||||||
v.PLEX_TYPE_EPISODE):
|
v.PLEX_TYPE_EPISODE):
|
||||||
log.debug("Removing episode/season/tv show %s" % item['ratingKey'])
|
log.debug("Removing episode/season/show with plex id %s",
|
||||||
|
item['ratingKey'])
|
||||||
self.videoLibUpdate = True
|
self.videoLibUpdate = True
|
||||||
with itemtypes.TVShows() as show:
|
with itemtypes.TVShows() as show:
|
||||||
show.remove(item['ratingKey'])
|
show.remove(item['ratingKey'])
|
||||||
|
@ -1251,7 +1206,7 @@ class LibrarySync(Thread):
|
||||||
'state': status,
|
'state': status,
|
||||||
'type': typus,
|
'type': typus,
|
||||||
'ratingKey': str(item['itemID']),
|
'ratingKey': str(item['itemID']),
|
||||||
'timestamp': getUnixTimestamp(),
|
'timestamp': unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
elif typus in (v.PLEX_TYPE_MOVIE,
|
elif typus in (v.PLEX_TYPE_MOVIE,
|
||||||
|
@ -1268,7 +1223,7 @@ class LibrarySync(Thread):
|
||||||
'state': status,
|
'state': status,
|
||||||
'type': typus,
|
'type': typus,
|
||||||
'ratingKey': plex_id,
|
'ratingKey': plex_id,
|
||||||
'timestamp': getUnixTimestamp(),
|
'timestamp': unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1307,7 +1262,7 @@ class LibrarySync(Thread):
|
||||||
'state': None, # Don't need a state here
|
'state': None, # Don't need a state here
|
||||||
'type': kodi_info[5],
|
'type': kodi_info[5],
|
||||||
'ratingKey': plex_id,
|
'ratingKey': plex_id,
|
||||||
'timestamp': getUnixTimestamp(),
|
'timestamp': unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1316,102 +1271,111 @@ class LibrarySync(Thread):
|
||||||
Someone (not necessarily the user signed in) is playing something some-
|
Someone (not necessarily the user signed in) is playing something some-
|
||||||
where
|
where
|
||||||
"""
|
"""
|
||||||
items = []
|
|
||||||
for item in data:
|
for item in data:
|
||||||
# Drop buffering messages immediately
|
|
||||||
status = item['state']
|
status = item['state']
|
||||||
if status == 'buffering':
|
if status == 'buffering':
|
||||||
|
# Drop buffering messages immediately
|
||||||
continue
|
continue
|
||||||
ratingKey = str(item['ratingKey'])
|
plex_id = item['ratingKey']
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
skip = False
|
||||||
kodi_info = plex_db.getItem_byId(ratingKey)
|
for pid in (0, 1, 2):
|
||||||
if kodi_info is None:
|
if plex_id == state.PLAYER_STATES[pid]['plex_id']:
|
||||||
# Item not (yet) in Kodi library
|
# Kodi is playing this item - no need to set the playstate
|
||||||
|
skip = True
|
||||||
|
if skip:
|
||||||
continue
|
continue
|
||||||
sessionKey = item['sessionKey']
|
sessionKey = item['sessionKey']
|
||||||
# Do we already have a sessionKey stored?
|
# Do we already have a sessionKey stored?
|
||||||
if sessionKey not in self.sessionKeys:
|
if sessionKey not in self.sessionKeys:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
kodi_info = plex_db.getItem_byId(plex_id)
|
||||||
|
if kodi_info is None:
|
||||||
|
# Item not (yet) in Kodi library
|
||||||
|
continue
|
||||||
if settings('plex_serverowned') == 'false':
|
if settings('plex_serverowned') == 'false':
|
||||||
# Not our PMS, we are not authorized to get the
|
# Not our PMS, we are not authorized to get the sessions
|
||||||
# sessions
|
|
||||||
# On the bright side, it must be us playing :-)
|
# On the bright side, it must be us playing :-)
|
||||||
self.sessionKeys = {
|
self.sessionKeys[sessionKey] = {}
|
||||||
sessionKey: {}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
# PMS is ours - get all current sessions
|
# PMS is ours - get all current sessions
|
||||||
self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN)
|
self.sessionKeys.update(GetPMSStatus(state.PLEX_TOKEN))
|
||||||
log.debug('Updated current sessions. They are: %s'
|
log.debug('Updated current sessions. They are: %s',
|
||||||
% self.sessionKeys)
|
self.sessionKeys)
|
||||||
if sessionKey not in self.sessionKeys:
|
if sessionKey not in self.sessionKeys:
|
||||||
log.warn('Session key %s still unknown! Skip '
|
log.info('Session key %s still unknown! Skip '
|
||||||
'item' % sessionKey)
|
'playstate update', sessionKey)
|
||||||
continue
|
continue
|
||||||
|
# Attach Kodi info to the session
|
||||||
currSess = self.sessionKeys[sessionKey]
|
self.sessionKeys[sessionKey]['kodi_id'] = kodi_info[0]
|
||||||
|
self.sessionKeys[sessionKey]['file_id'] = kodi_info[1]
|
||||||
|
self.sessionKeys[sessionKey]['kodi_type'] = kodi_info[4]
|
||||||
|
session = self.sessionKeys[sessionKey]
|
||||||
if settings('plex_serverowned') != 'false':
|
if settings('plex_serverowned') != 'false':
|
||||||
# Identify the user - same one as signed on with PKC? Skip
|
# Identify the user - same one as signed on with PKC? Skip
|
||||||
# update if neither session's username nor userid match
|
# update if neither session's username nor userid match
|
||||||
# (Owner sometime's returns id '1', not always)
|
# (Owner sometime's returns id '1', not always)
|
||||||
if (not state.PLEX_TOKEN and currSess['userId'] == '1'):
|
if not state.PLEX_TOKEN and session['userId'] == '1':
|
||||||
# PKC not signed in to plex.tv. Plus owner of PMS is
|
# PKC not signed in to plex.tv. Plus owner of PMS is
|
||||||
# playing (the '1').
|
# playing (the '1').
|
||||||
# Hence must be us (since several users require plex.tv
|
# Hence must be us (since several users require plex.tv
|
||||||
# token for PKC)
|
# token for PKC)
|
||||||
pass
|
pass
|
||||||
elif not (currSess['userId'] == state.PLEX_USER_ID
|
elif not (session['userId'] == state.PLEX_USER_ID or
|
||||||
or
|
session['username'] == state.PLEX_USERNAME):
|
||||||
currSess['username'] == state.PLEX_USERNAME):
|
|
||||||
log.debug('Our username %s, userid %s did not match '
|
log.debug('Our username %s, userid %s did not match '
|
||||||
'the session username %s with userid %s'
|
'the session username %s with userid %s',
|
||||||
% (state.PLEX_USERNAME,
|
state.PLEX_USERNAME,
|
||||||
state.PLEX_USER_ID,
|
state.PLEX_USER_ID,
|
||||||
currSess['username'],
|
session['username'],
|
||||||
currSess['userId']))
|
session['userId'])
|
||||||
continue
|
continue
|
||||||
|
# Get an up-to-date XML from the PMS because PMS will NOT directly
|
||||||
# Get an up-to-date XML from the PMS
|
# tell us: duration of item viewCount
|
||||||
# because PMS will NOT directly tell us:
|
if session.get('duration') is None:
|
||||||
# duration of item
|
xml = GetPlexMetadata(plex_id)
|
||||||
# viewCount
|
|
||||||
if currSess.get('duration') is None:
|
|
||||||
xml = GetPlexMetadata(ratingKey)
|
|
||||||
if xml in (None, 401):
|
if xml in (None, 401):
|
||||||
log.error('Could not get up-to-date xml for item %s'
|
log.error('Could not get up-to-date xml for item %s',
|
||||||
% ratingKey)
|
plex_id)
|
||||||
continue
|
continue
|
||||||
API = PlexAPI.API(xml[0])
|
api = PlexAPI.API(xml[0])
|
||||||
userdata = API.getUserData()
|
userdata = api.userdata()
|
||||||
currSess['duration'] = userdata['Runtime']
|
session['duration'] = userdata['Runtime']
|
||||||
currSess['viewCount'] = userdata['PlayCount']
|
session['viewCount'] = userdata['PlayCount']
|
||||||
# Sometimes, Plex tells us resume points in milliseconds and
|
# Sometimes, Plex tells us resume points in milliseconds and
|
||||||
# not in seconds - thank you very much!
|
# not in seconds - thank you very much!
|
||||||
if item.get('viewOffset') > currSess['duration']:
|
if item['viewOffset'] > session['duration']:
|
||||||
resume = item.get('viewOffset') / 1000
|
resume = item['viewOffset'] / 1000
|
||||||
else:
|
else:
|
||||||
resume = item.get('viewOffset')
|
resume = item['viewOffset']
|
||||||
# Append to list that we need to process
|
if resume < v.IGNORE_SECONDS_AT_START:
|
||||||
items.append({
|
continue
|
||||||
'ratingKey': ratingKey,
|
try:
|
||||||
'kodi_id': kodi_info[0],
|
completed = float(resume) / float(session['duration'])
|
||||||
'file_id': kodi_info[1],
|
except (ZeroDivisionError, TypeError):
|
||||||
'kodi_type': kodi_info[4],
|
log.error('Could not mark playstate for %s and session %s',
|
||||||
'viewOffset': resume,
|
data, session)
|
||||||
'state': status,
|
continue
|
||||||
'duration': currSess['duration'],
|
if completed >= v.MARK_PLAYED_AT:
|
||||||
'viewCount': currSess['viewCount'],
|
# Only mark completely watched ONCE
|
||||||
'lastViewedAt': DateToKodi(getUnixTimestamp())
|
if session.get('marked_played') is None:
|
||||||
})
|
session['marked_played'] = True
|
||||||
log.debug('Update playstate for user %s with id %s: %s'
|
mark_played = True
|
||||||
% (state.PLEX_USERNAME,
|
else:
|
||||||
state.PLEX_USER_ID,
|
# Don't mark it as completely watched again
|
||||||
items[-1]))
|
continue
|
||||||
# Now tell Kodi where we are
|
else:
|
||||||
for item in items:
|
mark_played = False
|
||||||
itemFkt = getattr(itemtypes,
|
log.debug('Update playstate for user %s with id %s for plex id %s',
|
||||||
v.ITEMTYPE_FROM_KODITYPE[item['kodi_type']])
|
state.PLEX_USERNAME, state.PLEX_USER_ID, plex_id)
|
||||||
with itemFkt() as Fkt:
|
item_fkt = getattr(itemtypes,
|
||||||
Fkt.updatePlaystate(item)
|
v.ITEMTYPE_FROM_KODITYPE[session['kodi_type']])
|
||||||
|
with item_fkt() as fkt:
|
||||||
|
fkt.updatePlaystate(mark_played,
|
||||||
|
session['viewCount'],
|
||||||
|
resume,
|
||||||
|
session['duration'],
|
||||||
|
session['file_id'],
|
||||||
|
unix_date_to_kodi(unix_timestamp()))
|
||||||
|
|
||||||
def fanartSync(self, refresh=False):
|
def fanartSync(self, refresh=False):
|
||||||
"""
|
"""
|
||||||
|
@ -1455,9 +1419,9 @@ class LibrarySync(Thread):
|
||||||
window('plex_dbScan', value="true")
|
window('plex_dbScan', value="true")
|
||||||
state.DB_SCAN = True
|
state.DB_SCAN = True
|
||||||
# First remove playlists
|
# First remove playlists
|
||||||
deletePlaylists()
|
delete_playlists()
|
||||||
# Remove video nodes
|
# Remove video nodes
|
||||||
deleteNodes()
|
delete_nodes()
|
||||||
# Kick off refresh
|
# Kick off refresh
|
||||||
if self.maintainViews() is True:
|
if self.maintainViews() is True:
|
||||||
# Ran successfully
|
# Ran successfully
|
||||||
|
@ -1509,21 +1473,19 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
def run_internal(self):
|
def run_internal(self):
|
||||||
# Re-assign handles to have faster calls
|
# Re-assign handles to have faster calls
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
installSyncDone = self.installSyncDone
|
installSyncDone = self.installSyncDone
|
||||||
background_sync = state.BACKGROUND_SYNC
|
|
||||||
fullSync = self.fullSync
|
fullSync = self.fullSync
|
||||||
processMessage = self.processMessage
|
processMessage = self.processMessage
|
||||||
processItems = self.processItems
|
processItems = self.processItems
|
||||||
FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL
|
|
||||||
lastSync = 0
|
lastSync = 0
|
||||||
lastTimeSync = 0
|
lastTimeSync = 0
|
||||||
lastProcessing = 0
|
lastProcessing = 0
|
||||||
oneDay = 60*60*24
|
oneDay = 60*60*24
|
||||||
|
|
||||||
# Link to Websocket queue
|
# Link to Websocket queue
|
||||||
queue = self.mgr.ws.queue
|
queue = state.WEBSOCKET_QUEUE
|
||||||
|
|
||||||
startupComplete = False
|
startupComplete = False
|
||||||
self.views = []
|
self.views = []
|
||||||
|
@ -1536,12 +1498,12 @@ class LibrarySync(Thread):
|
||||||
if settings('FanartTV') == 'true':
|
if settings('FanartTV') == 'true':
|
||||||
self.fanartthread.start()
|
self.fanartthread.start()
|
||||||
|
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
|
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
# Set in service.py
|
# Set in service.py
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
log.info("###===--- LibrarySync Stopped ---===###")
|
log.info("###===--- LibrarySync Stopped ---===###")
|
||||||
return
|
return
|
||||||
|
@ -1552,11 +1514,9 @@ class LibrarySync(Thread):
|
||||||
self.force_dialog = False
|
self.force_dialog = False
|
||||||
# Verify the validity of the database
|
# Verify the validity of the database
|
||||||
currentVersion = settings('dbCreatedWithVersion')
|
currentVersion = settings('dbCreatedWithVersion')
|
||||||
minVersion = window('plex_minDBVersion')
|
if not compare_version(currentVersion, v.MIN_DB_VERSION):
|
||||||
|
|
||||||
if not compare_version(currentVersion, minVersion):
|
|
||||||
log.warn("Db version out of date: %s minimum version "
|
log.warn("Db version out of date: %s minimum version "
|
||||||
"required: %s" % (currentVersion, minVersion))
|
"required: %s", currentVersion, v.MIN_DB_VERSION)
|
||||||
# DB out of date. Proceed to recreate?
|
# DB out of date. Proceed to recreate?
|
||||||
resp = dialog('yesno',
|
resp = dialog('yesno',
|
||||||
heading=lang(29999),
|
heading=lang(29999),
|
||||||
|
@ -1576,11 +1536,11 @@ class LibrarySync(Thread):
|
||||||
# Also runs when first installed
|
# Also runs when first installed
|
||||||
# Verify the video database can be found
|
# Verify the video database can be found
|
||||||
videoDb = v.DB_VIDEO_PATH
|
videoDb = v.DB_VIDEO_PATH
|
||||||
if not exists(tryEncode(videoDb)):
|
if not exists(try_encode(videoDb)):
|
||||||
# Database does not exists
|
# Database does not exists
|
||||||
log.error("The current Kodi version is incompatible "
|
log.error("The current Kodi version is incompatible "
|
||||||
"to know which Kodi versions are supported.")
|
"to know which Kodi versions are supported.")
|
||||||
log.error('Current Kodi version: %s' % tryDecode(
|
log.error('Current Kodi version: %s' % try_decode(
|
||||||
xbmc.getInfoLabel('System.BuildVersion')))
|
xbmc.getInfoLabel('System.BuildVersion')))
|
||||||
# "Current Kodi version is unsupported, cancel lib sync"
|
# "Current Kodi version is unsupported, cancel lib sync"
|
||||||
dialog('ok', heading='{plex}', line1=lang(39403))
|
dialog('ok', heading='{plex}', line1=lang(39403))
|
||||||
|
@ -1589,10 +1549,10 @@ class LibrarySync(Thread):
|
||||||
state.DB_SCAN = True
|
state.DB_SCAN = True
|
||||||
window('plex_dbScan', value="true")
|
window('plex_dbScan', value="true")
|
||||||
log.info("Db version: %s" % settings('dbCreatedWithVersion'))
|
log.info("Db version: %s" % settings('dbCreatedWithVersion'))
|
||||||
lastTimeSync = getUnixTimestamp()
|
lastTimeSync = unix_timestamp()
|
||||||
# Initialize time offset Kodi - PMS
|
# Initialize time offset Kodi - PMS
|
||||||
self.syncPMStime()
|
self.syncPMStime()
|
||||||
lastSync = getUnixTimestamp()
|
lastSync = unix_timestamp()
|
||||||
if settings('FanartTV') == 'true':
|
if settings('FanartTV') == 'true':
|
||||||
# Start getting additional missing artwork
|
# Start getting additional missing artwork
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
@ -1606,10 +1566,12 @@ class LibrarySync(Thread):
|
||||||
'refresh': True
|
'refresh': True
|
||||||
})
|
})
|
||||||
log.info('Refreshing video nodes and playlists now')
|
log.info('Refreshing video nodes and playlists now')
|
||||||
deletePlaylists()
|
delete_playlists()
|
||||||
deleteNodes()
|
delete_nodes()
|
||||||
log.info("Initial start-up full sync starting")
|
log.info("Initial start-up full sync starting")
|
||||||
|
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
||||||
librarySync = fullSync()
|
librarySync = fullSync()
|
||||||
|
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||||
window('plex_dbScan', clear=True)
|
window('plex_dbScan', clear=True)
|
||||||
state.DB_SCAN = False
|
state.DB_SCAN = False
|
||||||
if librarySync:
|
if librarySync:
|
||||||
|
@ -1631,16 +1593,16 @@ class LibrarySync(Thread):
|
||||||
self.triage_lib_scans()
|
self.triage_lib_scans()
|
||||||
self.force_dialog = False
|
self.force_dialog = False
|
||||||
continue
|
continue
|
||||||
now = getUnixTimestamp()
|
now = unix_timestamp()
|
||||||
# Standard syncs - don't force-show dialogs
|
# Standard syncs - don't force-show dialogs
|
||||||
self.force_dialog = False
|
self.force_dialog = False
|
||||||
if (now - lastSync > FULL_SYNC_INTERVALL and
|
if (now - lastSync > state.FULL_SYNC_INTERVALL and
|
||||||
not self.xbmcplayer.isPlaying()):
|
not self.xbmcplayer.isPlaying()):
|
||||||
lastSync = now
|
lastSync = now
|
||||||
log.info('Doing scheduled full library scan')
|
log.info('Doing scheduled full library scan')
|
||||||
state.DB_SCAN = True
|
state.DB_SCAN = True
|
||||||
window('plex_dbScan', value="true")
|
window('plex_dbScan', value="true")
|
||||||
if fullSync() is False and not thread_stopped():
|
if fullSync() is False and not stopped():
|
||||||
log.error('Could not finish scheduled full sync')
|
log.error('Could not finish scheduled full sync')
|
||||||
self.force_dialog = True
|
self.force_dialog = True
|
||||||
self.showKodiNote(lang(39410),
|
self.showKodiNote(lang(39410),
|
||||||
|
@ -1658,7 +1620,7 @@ class LibrarySync(Thread):
|
||||||
self.syncPMStime()
|
self.syncPMStime()
|
||||||
window('plex_dbScan', clear=True)
|
window('plex_dbScan', clear=True)
|
||||||
state.DB_SCAN = False
|
state.DB_SCAN = False
|
||||||
elif background_sync:
|
elif not state.BACKGROUND_SYNC_DISABLED:
|
||||||
# Check back whether we should process something
|
# Check back whether we should process something
|
||||||
# Only do this once every while (otherwise, potentially
|
# Only do this once every while (otherwise, potentially
|
||||||
# many screen refreshes lead to flickering)
|
# many screen refreshes lead to flickering)
|
||||||
|
|
|
@ -12,7 +12,7 @@ LEVELS = {
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def tryEncode(uniString, encoding='utf-8'):
|
def try_encode(uniString, encoding='utf-8'):
|
||||||
"""
|
"""
|
||||||
Will try to encode uniString (in unicode) to encoding. This possibly
|
Will try to encode uniString (in unicode) to encoding. This possibly
|
||||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||||
|
@ -43,5 +43,5 @@ class LogHandler(logging.StreamHandler):
|
||||||
try:
|
try:
|
||||||
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
xbmc.log(tryEncode(self.format(record)),
|
xbmc.log(try_encode(self.format(record)),
|
||||||
level=LEVELS[record.levelno])
|
level=LEVELS[record.levelno])
|
||||||
|
|
|
@ -1,57 +1,33 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from re import compile as re_compile
|
from re import compile as re_compile
|
||||||
import xml.etree.ElementTree as etree
|
from xml.etree.ElementTree import ParseError
|
||||||
|
|
||||||
from utils import advancedsettings_xml, indent, tryEncode
|
from utils import XmlKodiSetting, reboot_kodi, language as lang
|
||||||
from PlexFunctions import get_plex_sections
|
from PlexFunctions import get_plex_sections
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
|
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def get_current_music_folders():
|
def excludefromscan_music_folders():
|
||||||
"""
|
|
||||||
Returns a list of encoded strings as paths to the currently "blacklisted"
|
|
||||||
excludefromscan music folders in the advancedsettings.xml
|
|
||||||
"""
|
|
||||||
paths = []
|
|
||||||
root, _ = advancedsettings_xml(['audio', 'excludefromscan'])
|
|
||||||
if root is None:
|
|
||||||
return paths
|
|
||||||
|
|
||||||
for element in root:
|
|
||||||
try:
|
|
||||||
path = REGEX_MUSICPATH.findall(element.text)[0]
|
|
||||||
except IndexError:
|
|
||||||
log.error('Could not parse %s of xml element %s'
|
|
||||||
% (element.text, element.tag))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
paths.append(path)
|
|
||||||
return paths
|
|
||||||
|
|
||||||
|
|
||||||
def set_excludefromscan_music_folders():
|
|
||||||
"""
|
"""
|
||||||
Gets a complete list of paths for music libraries from the PMS. Sets them
|
Gets a complete list of paths for music libraries from the PMS. Sets them
|
||||||
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
||||||
Existing keys will be replaced
|
Existing keys will be replaced
|
||||||
|
|
||||||
Returns False if no new Plex libraries needed to be exluded, True otherwise
|
Reboots Kodi if new library detected
|
||||||
"""
|
"""
|
||||||
changed = False
|
|
||||||
write_xml = False
|
|
||||||
xml = get_plex_sections()
|
xml = get_plex_sections()
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
xml[0].attrib
|
||||||
except (TypeError, IndexError, AttributeError):
|
except (TypeError, IndexError, AttributeError):
|
||||||
log.error('Could not get Plex sections')
|
LOG.error('Could not get Plex sections')
|
||||||
return
|
return
|
||||||
# Build paths
|
# Build paths
|
||||||
paths = []
|
paths = []
|
||||||
|
@ -62,43 +38,42 @@ def set_excludefromscan_music_folders():
|
||||||
continue
|
continue
|
||||||
for location in library:
|
for location in library:
|
||||||
if location.tag == 'Location':
|
if location.tag == 'Location':
|
||||||
path = api.validatePlayurl(location.attrib['path'],
|
path = api.validate_playurl(location.attrib['path'],
|
||||||
typus=v.PLEX_TYPE_ARTIST,
|
typus=v.PLEX_TYPE_ARTIST,
|
||||||
omitCheck=True)
|
omit_check=True)
|
||||||
paths.append(__turn_to_regex(path))
|
paths.append(__turn_to_regex(path))
|
||||||
# Get existing advancedsettings
|
try:
|
||||||
root, tree = advancedsettings_xml(['audio', 'excludefromscan'],
|
with XmlKodiSetting('advancedsettings.xml',
|
||||||
force_create=True)
|
force_create=True,
|
||||||
|
top_element='advancedsettings') as xml:
|
||||||
for path in paths:
|
parent = xml.set_setting(['audio', 'excludefromscan'])
|
||||||
for element in root:
|
for path in paths:
|
||||||
if element.text == path:
|
for element in parent:
|
||||||
# Path already excluded
|
if element.text == path:
|
||||||
break
|
# Path already excluded
|
||||||
else:
|
break
|
||||||
changed = True
|
else:
|
||||||
write_xml = True
|
LOG.info('New Plex music library detected: %s', path)
|
||||||
log.info('New Plex music library detected: %s' % path)
|
xml.set_setting(['audio', 'excludefromscan', 'regexp'],
|
||||||
element = etree.Element(tag='regexp')
|
value=path, append=True)
|
||||||
element.text = path
|
# We only need to reboot if we ADD new paths!
|
||||||
root.append(element)
|
reboot = xml.write_xml
|
||||||
|
# Delete obsolete entries
|
||||||
# Delete obsolete entries (unlike above, we don't change 'changed' to not
|
for element in parent:
|
||||||
# enforce a restart)
|
for path in paths:
|
||||||
for element in root:
|
if element.text == path:
|
||||||
for path in paths:
|
break
|
||||||
if element.text == path:
|
else:
|
||||||
break
|
LOG.info('Deleting music library from advancedsettings: %s',
|
||||||
else:
|
element.text)
|
||||||
log.info('Deleting Plex music library from advancedsettings: %s'
|
parent.remove(element)
|
||||||
% element.text)
|
except (ParseError, IOError):
|
||||||
root.remove(element)
|
LOG.error('Could not adjust advancedsettings.xml')
|
||||||
write_xml = True
|
reboot = False
|
||||||
|
if reboot is True:
|
||||||
if write_xml is True:
|
# 'New Plex music library detected. Sorry, but we need to
|
||||||
indent(tree.getroot())
|
# restart Kodi now due to the changes made.'
|
||||||
tree.write('%sadvancedsettings.xml' % v.KODI_PROFILE, encoding="UTF-8")
|
reboot_kodi(lang(39711))
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
def __turn_to_regex(path):
|
def __turn_to_regex(path):
|
||||||
|
|
|
@ -32,7 +32,7 @@ def pickle_me(obj, window_var='plex_result'):
|
||||||
obj can be pretty much any Python object. However, classes and
|
obj can be pretty much any Python object. However, classes and
|
||||||
functions won't work. See the Pickle documentation
|
functions won't work. See the Pickle documentation
|
||||||
"""
|
"""
|
||||||
log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
|
log('%sStart pickling' % PREFIX, level=LOGDEBUG)
|
||||||
pickl_window(window_var, value=dumps(obj))
|
pickl_window(window_var, value=dumps(obj))
|
||||||
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
|
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def unpickle_me(window_var='plex_result'):
|
||||||
pickl_window(window_var, clear=True)
|
pickl_window(window_var, clear=True)
|
||||||
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
|
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
|
||||||
obj = loads(result)
|
obj = loads(result)
|
||||||
log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
|
log('%sSuccessfully unpickled' % PREFIX, level=LOGDEBUG)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
|
447
resources/lib/playback.py
Normal file
447
resources/lib/playback.py
Normal file
|
@ -0,0 +1,447 @@
|
||||||
|
"""
|
||||||
|
Used to kick off Kodi playback
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
from xbmc import Player, sleep
|
||||||
|
|
||||||
|
from PlexAPI import API
|
||||||
|
from PlexFunctions import GetPlexMetadata, init_plex_playqueue
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
import kodidb_functions as kodidb
|
||||||
|
import playlist_func as PL
|
||||||
|
import playqueue as PQ
|
||||||
|
from playutils import PlayUtils
|
||||||
|
from PKC_listitem import PKC_ListItem
|
||||||
|
from pickler import pickle_me, Playback_Successful
|
||||||
|
import json_rpc as js
|
||||||
|
from utils import settings, dialog, language as lang, try_encode
|
||||||
|
from plexbmchelper.subscribers import LOCKER
|
||||||
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
# Do we need to return ultimately with a setResolvedUrl?
|
||||||
|
RESOLVE = True
|
||||||
|
# We're "failing" playback with a video of 0 length
|
||||||
|
NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4')
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
|
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
|
"""
|
||||||
|
Hit this function for addon path playback, Plex trailers, etc.
|
||||||
|
Will setup playback first, then on second call complete playback.
|
||||||
|
|
||||||
|
Will set Playback_Successful() with potentially a PKC_ListItem() attached
|
||||||
|
(to be consumed by setResolvedURL in default.py)
|
||||||
|
|
||||||
|
If trailers or additional (movie-)parts are added, default.py is released
|
||||||
|
and a completely new player instance is called with a new playlist. This
|
||||||
|
circumvents most issues with Kodi & playqueues
|
||||||
|
|
||||||
|
Set resolve to False if you do not want setResolvedUrl to be called on
|
||||||
|
the first pass - e.g. if you're calling this function from the original
|
||||||
|
service.py Python instance
|
||||||
|
"""
|
||||||
|
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s',
|
||||||
|
plex_id, plex_type, path)
|
||||||
|
global RESOLVE
|
||||||
|
RESOLVE = resolve
|
||||||
|
if not state.AUTHENTICATED:
|
||||||
|
LOG.error('Not yet authenticated for PMS, abort starting playback')
|
||||||
|
# "Unauthorized for PMS"
|
||||||
|
dialog('notification', lang(29999), lang(30017))
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
||||||
|
pos = js.get_position(playqueue.playlistid)
|
||||||
|
# Can return -1 (as in "no playlist")
|
||||||
|
pos = pos if pos != -1 else 0
|
||||||
|
LOG.debug('playQueue position: %s for %s', pos, playqueue)
|
||||||
|
# Have we already initiated playback?
|
||||||
|
try:
|
||||||
|
item = playqueue.items[pos]
|
||||||
|
except IndexError:
|
||||||
|
initiate = True
|
||||||
|
else:
|
||||||
|
initiate = True if item.plex_id != plex_id else False
|
||||||
|
if initiate:
|
||||||
|
_playback_init(plex_id, plex_type, playqueue, pos)
|
||||||
|
else:
|
||||||
|
# kick off playback on second pass
|
||||||
|
_conclude_playback(playqueue, pos)
|
||||||
|
|
||||||
|
|
||||||
|
def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
|
"""
|
||||||
|
Playback setup if Kodi starts playing an item for the first time.
|
||||||
|
"""
|
||||||
|
LOG.info('Initializing PKC playback')
|
||||||
|
xml = GetPlexMetadata(plex_id)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (IndexError, TypeError, AttributeError):
|
||||||
|
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
||||||
|
# "Play error"
|
||||||
|
dialog('notification', lang(29999), lang(30128), icon='{error}')
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
if playqueue.kodi_pl.size() > 1:
|
||||||
|
# Special case - we already got a filled Kodi playqueue
|
||||||
|
try:
|
||||||
|
_init_existing_kodi_playlist(playqueue, pos)
|
||||||
|
except PL.PlaylistError:
|
||||||
|
LOG.error('Aborting playback_init for longer Kodi playlist')
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
# Now we need to use setResolvedUrl for the item at position ZERO
|
||||||
|
# playqueue.py will pick up the missing items
|
||||||
|
_conclude_playback(playqueue, 0)
|
||||||
|
return
|
||||||
|
# "Usual" case - consider trailers and parts and build both Kodi and Plex
|
||||||
|
# playqueues
|
||||||
|
# Fail the item we're trying to play now so we can restart the player
|
||||||
|
_ensure_resolve()
|
||||||
|
api = API(xml[0])
|
||||||
|
trailers = False
|
||||||
|
if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and
|
||||||
|
settings('enableCinema') == "true"):
|
||||||
|
if settings('askCinema') == "true":
|
||||||
|
# "Play trailers?"
|
||||||
|
trailers = dialog('yesno', lang(29999), lang(33016))
|
||||||
|
trailers = True if trailers else False
|
||||||
|
else:
|
||||||
|
trailers = True
|
||||||
|
LOG.debug('Playing trailers: %s', trailers)
|
||||||
|
playqueue.clear()
|
||||||
|
if plex_type != v.PLEX_TYPE_CLIP:
|
||||||
|
# Post to the PMS to create a playqueue - in any case due to Companion
|
||||||
|
xml = init_plex_playqueue(plex_id,
|
||||||
|
xml.attrib.get('librarySectionUUID'),
|
||||||
|
mediatype=plex_type,
|
||||||
|
trailers=trailers)
|
||||||
|
if xml is None:
|
||||||
|
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
|
||||||
|
plex_id, xml.attrib.get('librarySectionUUID'))
|
||||||
|
# "Play error"
|
||||||
|
dialog('notification', lang(29999), lang(30128), icon='{error}')
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
# Should already be empty, but just in case
|
||||||
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
stack = _prep_playlist_stack(xml)
|
||||||
|
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
|
||||||
|
sleep(200)
|
||||||
|
_process_stack(playqueue, stack)
|
||||||
|
# Always resume if playback initiated via PMS and there IS a resume
|
||||||
|
# point
|
||||||
|
offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None
|
||||||
|
# Reset some playback variables
|
||||||
|
state.CONTEXT_MENU_PLAY = False
|
||||||
|
state.FORCE_TRANSCODE = False
|
||||||
|
# Do NOT set offset, because Kodi player will return here to resolveURL
|
||||||
|
# New thread to release this one sooner (e.g. harddisk spinning up)
|
||||||
|
thread = Thread(target=threaded_playback,
|
||||||
|
args=(playqueue.kodi_pl, pos, offset))
|
||||||
|
thread.setDaemon(True)
|
||||||
|
LOG.info('Done initializing playback, starting Kodi player at pos %s and '
|
||||||
|
'resume point %s', pos, offset)
|
||||||
|
# By design, PKC will start Kodi playback using Player().play(). Kodi
|
||||||
|
# caches paths like our plugin://pkc. If we use Player().play() between
|
||||||
|
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
|
||||||
|
# cache will have been flushed for some reason. Hence the 2nd call for
|
||||||
|
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
||||||
|
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_resolve(abort=False):
|
||||||
|
"""
|
||||||
|
Will check whether RESOLVE=True and if so, fail Kodi playback startup
|
||||||
|
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
|
||||||
|
pickling)
|
||||||
|
|
||||||
|
This way we're making sure that other Python instances (calling default.py)
|
||||||
|
will be destroyed.
|
||||||
|
"""
|
||||||
|
if RESOLVE:
|
||||||
|
LOG.debug('Passing dummy path to Kodi')
|
||||||
|
if not state.CONTEXT_MENU_PLAY:
|
||||||
|
# Because playback won't start with context menu play
|
||||||
|
state.PKC_CAUSED_STOP = True
|
||||||
|
result = Playback_Successful()
|
||||||
|
result.listitem = PKC_ListItem(path=NULL_VIDEO)
|
||||||
|
pickle_me(result)
|
||||||
|
if abort:
|
||||||
|
# Reset some playback variables
|
||||||
|
state.CONTEXT_MENU_PLAY = False
|
||||||
|
state.FORCE_TRANSCODE = False
|
||||||
|
state.RESUME_PLAYBACK = False
|
||||||
|
|
||||||
|
|
||||||
|
def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
|
"""
|
||||||
|
Will take the playqueue's kodi_pl with MORE than 1 element and initiate
|
||||||
|
playback (without adding trailers)
|
||||||
|
"""
|
||||||
|
LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size())
|
||||||
|
kodi_items = js.playlist_get_items(playqueue.playlistid)
|
||||||
|
if not kodi_items:
|
||||||
|
raise PL.PlaylistError('No Kodi items returned')
|
||||||
|
item = PL.init_Plex_playlist(playqueue, kodi_item=kodi_items[pos])
|
||||||
|
item.force_transcode = state.FORCE_TRANSCODE
|
||||||
|
# playqueue.py will add the rest - this will likely put the PMS under
|
||||||
|
# a LOT of strain if the following Kodi setting is enabled:
|
||||||
|
# Settings -> Player -> Videos -> Play next video automatically
|
||||||
|
LOG.debug('Done init_existing_kodi_playlist')
|
||||||
|
|
||||||
|
|
||||||
|
def _prep_playlist_stack(xml):
|
||||||
|
stack = []
|
||||||
|
for item in xml:
|
||||||
|
api = API(item)
|
||||||
|
if (state.CONTEXT_MENU_PLAY is False and
|
||||||
|
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
||||||
|
# If user chose to play via PMS or force transcode, do not
|
||||||
|
# use the item path stored in the Kodi DB
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byId(api.plex_id())
|
||||||
|
kodi_id = plex_dbitem[0] if plex_dbitem else None
|
||||||
|
kodi_type = plex_dbitem[4] if plex_dbitem else None
|
||||||
|
else:
|
||||||
|
# We will never store clips (trailers) in the Kodi DB.
|
||||||
|
# Also set kodi_id to None for playback via PMS, so that we're
|
||||||
|
# using add-on paths.
|
||||||
|
# Also do NOT associate episodes with library items for addon paths
|
||||||
|
# as artwork lookup is broken (episode path does not link back to
|
||||||
|
# season and show)
|
||||||
|
kodi_id = None
|
||||||
|
kodi_type = None
|
||||||
|
for part, _ in enumerate(item[0]):
|
||||||
|
api.set_part_number(part)
|
||||||
|
if kodi_id is None:
|
||||||
|
# Need to redirect again to PKC to conclude playback
|
||||||
|
path = ('plugin://%s/?plex_id=%s&plex_type=%s&mode=play'
|
||||||
|
% (v.ADDON_TYPE[api.plex_type()],
|
||||||
|
api.plex_id(),
|
||||||
|
api.plex_type()))
|
||||||
|
listitem = api.create_listitem()
|
||||||
|
listitem.setPath(try_encode(path))
|
||||||
|
else:
|
||||||
|
# Will add directly via the Kodi DB
|
||||||
|
path = None
|
||||||
|
listitem = None
|
||||||
|
stack.append({
|
||||||
|
'kodi_id': kodi_id,
|
||||||
|
'kodi_type': kodi_type,
|
||||||
|
'file': path,
|
||||||
|
'xml_video_element': item,
|
||||||
|
'listitem': listitem,
|
||||||
|
'part': part,
|
||||||
|
'playcount': api.viewcount(),
|
||||||
|
'offset': api.resume_point(),
|
||||||
|
'id': api.item_id()
|
||||||
|
})
|
||||||
|
return stack
|
||||||
|
|
||||||
|
|
||||||
|
def _process_stack(playqueue, stack):
|
||||||
|
"""
|
||||||
|
Takes our stack and adds the items to the PKC and Kodi playqueues.
|
||||||
|
"""
|
||||||
|
# getposition() can return -1
|
||||||
|
pos = max(playqueue.kodi_pl.getposition(), 0) + 1
|
||||||
|
for item in stack:
|
||||||
|
if item['kodi_id'] is None:
|
||||||
|
playlist_item = PL.add_listitem_to_Kodi_playlist(
|
||||||
|
playqueue,
|
||||||
|
pos,
|
||||||
|
item['listitem'],
|
||||||
|
file=item['file'],
|
||||||
|
xml_video_element=item['xml_video_element'])
|
||||||
|
else:
|
||||||
|
# Directly add element so we have full metadata
|
||||||
|
playlist_item = PL.add_item_to_kodi_playlist(
|
||||||
|
playqueue,
|
||||||
|
pos,
|
||||||
|
kodi_id=item['kodi_id'],
|
||||||
|
kodi_type=item['kodi_type'],
|
||||||
|
xml_video_element=item['xml_video_element'])
|
||||||
|
playlist_item.playcount = item['playcount']
|
||||||
|
playlist_item.offset = item['offset']
|
||||||
|
playlist_item.part = item['part']
|
||||||
|
playlist_item.id = item['id']
|
||||||
|
playlist_item.force_transcode = state.FORCE_TRANSCODE
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _conclude_playback(playqueue, pos):
|
||||||
|
"""
|
||||||
|
ONLY if actually being played (e.g. at 5th position of a playqueue).
|
||||||
|
|
||||||
|
Decide on direct play, direct stream, transcoding
|
||||||
|
path to
|
||||||
|
direct paths: file itself
|
||||||
|
PMS URL
|
||||||
|
Web URL
|
||||||
|
audiostream (e.g. let user choose)
|
||||||
|
subtitle stream (e.g. let user choose)
|
||||||
|
Init Kodi Playback (depending on situation):
|
||||||
|
start playback
|
||||||
|
return PKC listitem attached to result
|
||||||
|
"""
|
||||||
|
LOG.info('Concluding playback for playqueue position %s', pos)
|
||||||
|
result = Playback_Successful()
|
||||||
|
listitem = PKC_ListItem()
|
||||||
|
item = playqueue.items[pos]
|
||||||
|
if item.xml is not None:
|
||||||
|
# Got a Plex element
|
||||||
|
api = API(item.xml)
|
||||||
|
api.set_part_number(item.part)
|
||||||
|
api.create_listitem(listitem)
|
||||||
|
playutils = PlayUtils(api, item)
|
||||||
|
playurl = playutils.getPlayUrl()
|
||||||
|
else:
|
||||||
|
playurl = item.file
|
||||||
|
listitem.setPath(try_encode(playurl))
|
||||||
|
if item.playmethod == 'DirectStream':
|
||||||
|
listitem.setSubtitles(api.cache_external_subs())
|
||||||
|
elif item.playmethod == 'Transcode':
|
||||||
|
playutils.audio_subtitle_prefs(listitem)
|
||||||
|
|
||||||
|
if state.RESUME_PLAYBACK is True:
|
||||||
|
state.RESUME_PLAYBACK = False
|
||||||
|
if (item.offset is None and
|
||||||
|
item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)):
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byId(item.plex_id)
|
||||||
|
file_id = plex_dbitem[1] if plex_dbitem else None
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
item.offset = kodi_db.get_resume(file_id)
|
||||||
|
LOG.info('Resuming playback at %s', item.offset)
|
||||||
|
listitem.setProperty('StartOffset', str(item.offset))
|
||||||
|
listitem.setProperty('resumetime', str(item.offset))
|
||||||
|
# Reset the resumable flag
|
||||||
|
result.listitem = listitem
|
||||||
|
pickle_me(result)
|
||||||
|
LOG.info('Done concluding playback')
|
||||||
|
|
||||||
|
|
||||||
|
def process_indirect(key, offset, resolve=True):
|
||||||
|
"""
|
||||||
|
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
|
||||||
|
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
|
||||||
|
set.
|
||||||
|
|
||||||
|
Will release default.py with setResolvedUrl
|
||||||
|
|
||||||
|
Set resolve to False if playback should be kicked off directly, not via
|
||||||
|
setResolvedUrl
|
||||||
|
"""
|
||||||
|
LOG.info('process_indirect called with key: %s, offset: %s', key, offset)
|
||||||
|
global RESOLVE
|
||||||
|
RESOLVE = resolve
|
||||||
|
result = Playback_Successful()
|
||||||
|
if key.startswith('http') or key.startswith('{server}'):
|
||||||
|
xml = DU().downloadUrl(key)
|
||||||
|
elif key.startswith('/system/services'):
|
||||||
|
xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key)
|
||||||
|
else:
|
||||||
|
xml = DU().downloadUrl('{server}%s' % key)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
LOG.error('Could not download PMS metadata')
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
if offset != '0':
|
||||||
|
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
|
||||||
|
# Todo: implement offset
|
||||||
|
api = API(xml[0])
|
||||||
|
listitem = PKC_ListItem()
|
||||||
|
api.create_listitem(listitem)
|
||||||
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
||||||
|
playqueue.clear()
|
||||||
|
item = PL.Playlist_Item()
|
||||||
|
item.xml = xml[0]
|
||||||
|
item.offset = int(offset)
|
||||||
|
item.plex_type = v.PLEX_TYPE_CLIP
|
||||||
|
item.playmethod = 'DirectStream'
|
||||||
|
# Need to get yet another xml to get the final playback url
|
||||||
|
xml = DU().downloadUrl('http://node.plexapp.com:32400%s'
|
||||||
|
% xml[0][0][0].attrib['key'])
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
LOG.error('Could not download last xml for playurl')
|
||||||
|
_ensure_resolve(abort=True)
|
||||||
|
return
|
||||||
|
playurl = xml[0].attrib['key']
|
||||||
|
item.file = playurl
|
||||||
|
listitem.setPath(try_encode(playurl))
|
||||||
|
playqueue.items.append(item)
|
||||||
|
if resolve is True:
|
||||||
|
result.listitem = listitem
|
||||||
|
pickle_me(result)
|
||||||
|
else:
|
||||||
|
thread = Thread(target=Player().play,
|
||||||
|
args={'item': try_encode(playurl),
|
||||||
|
'listitem': listitem})
|
||||||
|
thread.setDaemon(True)
|
||||||
|
LOG.info('Done initializing PKC playback, starting Kodi player')
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
|
"""
|
||||||
|
Play all items contained in the xml passed in. Called by Plex Companion.
|
||||||
|
|
||||||
|
Either supply the ratingKey of the starting Plex element. Or set
|
||||||
|
playqueue.selectedItemID
|
||||||
|
"""
|
||||||
|
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
||||||
|
offset, start_plex_id)
|
||||||
|
stack = _prep_playlist_stack(xml)
|
||||||
|
_process_stack(playqueue, stack)
|
||||||
|
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||||
|
if start_plex_id is not None:
|
||||||
|
for startpos, item in enumerate(playqueue.items):
|
||||||
|
if item.plex_id == start_plex_id:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
startpos = 0
|
||||||
|
else:
|
||||||
|
for startpos, item in enumerate(playqueue.items):
|
||||||
|
if item.id == playqueue.selectedItemID:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
startpos = 0
|
||||||
|
thread = Thread(target=threaded_playback,
|
||||||
|
args=(playqueue.kodi_pl, startpos, offset))
|
||||||
|
LOG.info('Done play_xml, starting Kodi player at position %s', startpos)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def threaded_playback(kodi_playlist, startpos, offset):
|
||||||
|
"""
|
||||||
|
Seek immediately after kicking off playback is not reliable.
|
||||||
|
"""
|
||||||
|
player = Player()
|
||||||
|
player.play(kodi_playlist, None, False, startpos)
|
||||||
|
if offset and offset != '0':
|
||||||
|
i = 0
|
||||||
|
while not player.isPlaying():
|
||||||
|
sleep(100)
|
||||||
|
i += 1
|
||||||
|
if i > 100:
|
||||||
|
LOG.error('Could not seek to %s', offset)
|
||||||
|
return
|
||||||
|
js.seek_to(int(offset))
|
|
@ -1,27 +1,19 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
|
|
||||||
from xbmc import Player
|
import playback
|
||||||
|
|
||||||
from PKC_listitem import PKC_ListItem
|
|
||||||
from pickler import pickle_me, Playback_Successful
|
|
||||||
from playbackutils import PlaybackUtils
|
|
||||||
from utils import window
|
|
||||||
from PlexFunctions import GetPlexMetadata
|
|
||||||
from PlexAPI import API
|
|
||||||
from playqueue import lock
|
|
||||||
import variables as v
|
|
||||||
from downloadutils import DownloadUtils
|
|
||||||
from PKC_listitem import convert_PKC_to_listitem
|
|
||||||
import plexdb_functions as plexdb
|
|
||||||
from context_entry import ContextMenu
|
from context_entry import ContextMenu
|
||||||
import state
|
import state
|
||||||
|
import json_rpc as js
|
||||||
|
from pickler import pickle_me, Playback_Successful
|
||||||
|
import kodidb_functions as kodidb
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -30,135 +22,36 @@ class Playback_Starter(Thread):
|
||||||
"""
|
"""
|
||||||
Processes new plays
|
Processes new plays
|
||||||
"""
|
"""
|
||||||
def __init__(self, callback=None):
|
|
||||||
self.mgr = callback
|
|
||||||
self.playqueue = self.mgr.playqueue
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def process_play(self, plex_id, kodi_id=None):
|
|
||||||
"""
|
|
||||||
Processes Kodi playback init for ONE item
|
|
||||||
"""
|
|
||||||
log.info("Process_play called with plex_id %s, kodi_id %s"
|
|
||||||
% (plex_id, kodi_id))
|
|
||||||
if not state.AUTHENTICATED:
|
|
||||||
log.error('Not yet authenticated for PMS, abort starting playback')
|
|
||||||
# Todo: Warn user with dialog
|
|
||||||
return
|
|
||||||
xml = GetPlexMetadata(plex_id)
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except (IndexError, TypeError, AttributeError):
|
|
||||||
log.error('Could not get a PMS xml for plex id %s' % plex_id)
|
|
||||||
return
|
|
||||||
api = API(xml[0])
|
|
||||||
if api.getType() == v.PLEX_TYPE_PHOTO:
|
|
||||||
# Photo
|
|
||||||
result = Playback_Successful()
|
|
||||||
listitem = PKC_ListItem()
|
|
||||||
listitem = api.CreateListItemFromPlexItem(listitem)
|
|
||||||
result.listitem = listitem
|
|
||||||
else:
|
|
||||||
# Video and Music
|
|
||||||
playqueue = self.playqueue.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
|
|
||||||
with lock:
|
|
||||||
result = PlaybackUtils(xml, playqueue).play(
|
|
||||||
plex_id,
|
|
||||||
kodi_id,
|
|
||||||
xml.attrib.get('librarySectionUUID'))
|
|
||||||
log.info('Done process_play, playqueues: %s'
|
|
||||||
% self.playqueue.playqueues)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def process_plex_node(self, url, viewOffset, directplay=False,
|
|
||||||
node=True):
|
|
||||||
"""
|
|
||||||
Called for Plex directories or redirect for playback (e.g. trailers,
|
|
||||||
clips, watchlater)
|
|
||||||
"""
|
|
||||||
log.info('process_plex_node called with url: %s, viewOffset: %s'
|
|
||||||
% (url, viewOffset))
|
|
||||||
# Plex redirect, e.g. watch later. Need to get actual URLs
|
|
||||||
if url.startswith('http') or url.startswith('{server}'):
|
|
||||||
xml = DownloadUtils().downloadUrl(url)
|
|
||||||
else:
|
|
||||||
xml = DownloadUtils().downloadUrl('{server}%s' % url)
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except:
|
|
||||||
log.error('Could not download PMS metadata')
|
|
||||||
return
|
|
||||||
if viewOffset != '0':
|
|
||||||
try:
|
|
||||||
viewOffset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(viewOffset))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
window('plex_customplaylist.seektime', value=str(viewOffset))
|
|
||||||
log.info('Set resume point to %s' % str(viewOffset))
|
|
||||||
api = API(xml[0])
|
|
||||||
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]
|
|
||||||
if node is True:
|
|
||||||
plex_id = None
|
|
||||||
kodi_id = 'plexnode'
|
|
||||||
else:
|
|
||||||
plex_id = api.getRatingKey()
|
|
||||||
kodi_id = None
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plexdb_item = plex_db.getItem_byId(plex_id)
|
|
||||||
try:
|
|
||||||
kodi_id = plexdb_item[0]
|
|
||||||
except TypeError:
|
|
||||||
log.info('Couldnt find item %s in Kodi db'
|
|
||||||
% api.getRatingKey())
|
|
||||||
playqueue = self.playqueue.get_playqueue_from_type(typus)
|
|
||||||
with lock:
|
|
||||||
result = PlaybackUtils(xml, playqueue).play(
|
|
||||||
plex_id,
|
|
||||||
kodi_id=kodi_id,
|
|
||||||
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
|
||||||
if directplay:
|
|
||||||
if result.listitem:
|
|
||||||
listitem = convert_PKC_to_listitem(result.listitem)
|
|
||||||
Player().play(listitem.getfilename(), listitem)
|
|
||||||
return Playback_Successful()
|
|
||||||
else:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def triage(self, item):
|
def triage(self, item):
|
||||||
_, params = item.split('?', 1)
|
try:
|
||||||
|
_, params = item.split('?', 1)
|
||||||
|
except ValueError:
|
||||||
|
# e.g. when plugin://...tvshows is called for entire season
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
show_id = kodi_db.show_id_from_path(item)
|
||||||
|
if show_id:
|
||||||
|
js.activate_window('videos',
|
||||||
|
'videodb://tvshows/titles/%s' % show_id)
|
||||||
|
else:
|
||||||
|
LOG.error('Could not find tv show id for %s', item)
|
||||||
|
pickle_me(Playback_Successful())
|
||||||
|
return
|
||||||
params = dict(parse_qsl(params))
|
params = dict(parse_qsl(params))
|
||||||
mode = params.get('mode')
|
mode = params.get('mode')
|
||||||
log.debug('Received mode: %s, params: %s' % (mode, params))
|
LOG.debug('Received mode: %s, params: %s', mode, params)
|
||||||
try:
|
if mode == 'play':
|
||||||
if mode == 'play':
|
playback.playback_triage(plex_id=params.get('plex_id'),
|
||||||
result = self.process_play(params.get('id'),
|
plex_type=params.get('plex_type'),
|
||||||
params.get('dbid'))
|
path=params.get('path'))
|
||||||
elif mode == 'companion':
|
elif mode == 'plex_node':
|
||||||
result = self.process_companion()
|
playback.process_indirect(params['key'], params['offset'])
|
||||||
elif mode == 'plex_node':
|
elif mode == 'context_menu':
|
||||||
result = self.process_plex_node(
|
ContextMenu(kodi_id=params['kodi_id'],
|
||||||
params.get('key'),
|
kodi_type=params['kodi_type'])
|
||||||
params.get('view_offset'),
|
|
||||||
directplay=True if params.get('play_directly') else False,
|
|
||||||
node=False if params.get('node') == 'false' else True)
|
|
||||||
elif mode == 'context_menu':
|
|
||||||
ContextMenu()
|
|
||||||
result = Playback_Successful()
|
|
||||||
except:
|
|
||||||
log.error('Error encountered for mode %s, params %s'
|
|
||||||
% (mode, params))
|
|
||||||
import traceback
|
|
||||||
log.error(traceback.format_exc())
|
|
||||||
# Let default.py know!
|
|
||||||
pickle_me(None)
|
|
||||||
else:
|
|
||||||
pickle_me(result)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
queue = self.mgr.command_pipeline.playback_queue
|
queue = state.COMMAND_PIPELINE_QUEUE
|
||||||
log.info("----===## Starting Playback_Starter ##===----")
|
LOG.info("----===## Starting Playback_Starter ##===----")
|
||||||
while True:
|
while True:
|
||||||
item = queue.get()
|
item = queue.get()
|
||||||
if item is None:
|
if item is None:
|
||||||
|
@ -167,4 +60,4 @@ class Playback_Starter(Thread):
|
||||||
else:
|
else:
|
||||||
self.triage(item)
|
self.triage(item)
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
log.info("----===## Playback_Starter stopped ##===----")
|
LOG.info("----===## Playback_Starter stopped ##===----")
|
||||||
|
|
|
@ -1,364 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from urllib import urlencode
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from xbmc import getCondVisibility, Player
|
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
import playutils as putils
|
|
||||||
from utils import window, settings, tryEncode, tryDecode, language as lang
|
|
||||||
import downloadutils
|
|
||||||
|
|
||||||
from PlexAPI import API
|
|
||||||
from PlexFunctions import init_plex_playqueue
|
|
||||||
from PKC_listitem import PKC_ListItem as ListItem, convert_PKC_to_listitem
|
|
||||||
from playlist_func import add_item_to_kodi_playlist, \
|
|
||||||
get_playlist_details_from_xml, add_listitem_to_Kodi_playlist, \
|
|
||||||
add_listitem_to_playlist, remove_from_Kodi_playlist
|
|
||||||
from pickler import Playback_Successful
|
|
||||||
from plexdb_functions import Get_Plex_DB
|
|
||||||
import variables as v
|
|
||||||
import state
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class PlaybackUtils():
|
|
||||||
|
|
||||||
def __init__(self, xml, playqueue):
|
|
||||||
self.xml = xml
|
|
||||||
self.playqueue = playqueue
|
|
||||||
|
|
||||||
def play(self, plex_id, kodi_id=None, plex_lib_UUID=None):
|
|
||||||
"""
|
|
||||||
plex_lib_UUID: xml attribute 'librarySectionUUID', needed for posting
|
|
||||||
to the PMS
|
|
||||||
"""
|
|
||||||
log.info("Playbackutils called")
|
|
||||||
item = self.xml[0]
|
|
||||||
api = API(item)
|
|
||||||
playqueue = self.playqueue
|
|
||||||
xml = None
|
|
||||||
result = Playback_Successful()
|
|
||||||
listitem = ListItem()
|
|
||||||
playutils = putils.PlayUtils(item)
|
|
||||||
playurl = playutils.getPlayUrl()
|
|
||||||
if not playurl:
|
|
||||||
log.error('No playurl found, aborting')
|
|
||||||
return
|
|
||||||
|
|
||||||
if kodi_id in (None, 'plextrailer', 'plexnode'):
|
|
||||||
# Item is not in Kodi database, is a trailer/clip or plex redirect
|
|
||||||
# e.g. plex.tv watch later
|
|
||||||
api.CreateListItemFromPlexItem(listitem)
|
|
||||||
api.set_listitem_artwork(listitem)
|
|
||||||
if kodi_id == 'plexnode':
|
|
||||||
# Need to get yet another xml to get final url
|
|
||||||
window('plex_%s.playmethod' % playurl, clear=True)
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(
|
|
||||||
'{server}%s' % item[0][0].attrib.get('key'))
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
log.error('Could not download %s'
|
|
||||||
% item[0][0].attrib.get('key'))
|
|
||||||
return
|
|
||||||
playurl = tryEncode(xml[0].attrib.get('key'))
|
|
||||||
window('plex_%s.playmethod' % playurl, value='DirectStream')
|
|
||||||
|
|
||||||
playmethod = window('plex_%s.playmethod' % playurl)
|
|
||||||
if playmethod == "Transcode":
|
|
||||||
playutils.audioSubsPref(listitem, tryDecode(playurl))
|
|
||||||
listitem.setPath(playurl)
|
|
||||||
api.set_playback_win_props(playurl, listitem)
|
|
||||||
result.listitem = listitem
|
|
||||||
return result
|
|
||||||
|
|
||||||
kodi_type = v.KODITYPE_FROM_PLEXTYPE[api.getType()]
|
|
||||||
kodi_id = int(kodi_id)
|
|
||||||
|
|
||||||
# ORGANIZE CURRENT PLAYLIST ################
|
|
||||||
contextmenu_play = window('plex_contextplay') == 'true'
|
|
||||||
window('plex_contextplay', clear=True)
|
|
||||||
homeScreen = getCondVisibility('Window.IsActive(home)')
|
|
||||||
sizePlaylist = len(playqueue.items)
|
|
||||||
if contextmenu_play:
|
|
||||||
# Need to start with the items we're inserting here
|
|
||||||
startPos = sizePlaylist
|
|
||||||
else:
|
|
||||||
# Can return -1
|
|
||||||
startPos = max(playqueue.kodi_pl.getposition(), 0)
|
|
||||||
self.currentPosition = startPos
|
|
||||||
|
|
||||||
propertiesPlayback = window('plex_playbackProps') == "true"
|
|
||||||
introsPlaylist = False
|
|
||||||
dummyPlaylist = False
|
|
||||||
|
|
||||||
log.info("Playing from contextmenu: %s" % contextmenu_play)
|
|
||||||
log.info("Playlist start position: %s" % startPos)
|
|
||||||
log.info("Playlist plugin position: %s" % self.currentPosition)
|
|
||||||
log.info("Playlist size: %s" % sizePlaylist)
|
|
||||||
|
|
||||||
# RESUME POINT ################
|
|
||||||
seektime, runtime = api.getRuntime()
|
|
||||||
if window('plex_customplaylist.seektime'):
|
|
||||||
# Already got seektime, e.g. from playqueue & Plex companion
|
|
||||||
seektime = int(window('plex_customplaylist.seektime'))
|
|
||||||
|
|
||||||
# We need to ensure we add the intro and additional parts only once.
|
|
||||||
# Otherwise we get a loop.
|
|
||||||
if not propertiesPlayback:
|
|
||||||
window('plex_playbackProps', value="true")
|
|
||||||
log.info("Setting up properties in playlist.")
|
|
||||||
# Where will the player need to start?
|
|
||||||
# Do we need to get trailers?
|
|
||||||
trailers = False
|
|
||||||
if (api.getType() == v.PLEX_TYPE_MOVIE and
|
|
||||||
not seektime and
|
|
||||||
sizePlaylist < 2 and
|
|
||||||
settings('enableCinema') == "true"):
|
|
||||||
if settings('askCinema') == "true":
|
|
||||||
trailers = xbmcgui.Dialog().yesno(
|
|
||||||
lang(29999),
|
|
||||||
"Play trailers?")
|
|
||||||
trailers = True if trailers else False
|
|
||||||
else:
|
|
||||||
trailers = True
|
|
||||||
# Post to the PMS. REUSE THE PLAYQUEUE!
|
|
||||||
xml = init_plex_playqueue(plex_id,
|
|
||||||
plex_lib_UUID,
|
|
||||||
mediatype=api.getType(),
|
|
||||||
trailers=trailers)
|
|
||||||
try:
|
|
||||||
get_playlist_details_from_xml(playqueue, xml=xml)
|
|
||||||
except KeyError:
|
|
||||||
return
|
|
||||||
|
|
||||||
if (not homeScreen and not seektime and sizePlaylist < 2 and
|
|
||||||
window('plex_customplaylist') != "true" and
|
|
||||||
not contextmenu_play):
|
|
||||||
# Need to add a dummy file because the first item will fail
|
|
||||||
log.debug("Adding dummy file to playlist.")
|
|
||||||
dummyPlaylist = True
|
|
||||||
add_listitem_to_Kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
startPos,
|
|
||||||
xbmcgui.ListItem(),
|
|
||||||
playurl,
|
|
||||||
xml[0])
|
|
||||||
# Remove the original item from playlist
|
|
||||||
remove_from_Kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
startPos+1)
|
|
||||||
# Readd the original item to playlist - via jsonrpc so we have
|
|
||||||
# full metadata
|
|
||||||
add_item_to_kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
self.currentPosition+1,
|
|
||||||
kodi_id=kodi_id,
|
|
||||||
kodi_type=kodi_type,
|
|
||||||
file=playurl)
|
|
||||||
self.currentPosition += 1
|
|
||||||
|
|
||||||
# -- ADD TRAILERS ################
|
|
||||||
if trailers:
|
|
||||||
for i, item in enumerate(xml):
|
|
||||||
if i == len(xml) - 1:
|
|
||||||
# Don't add the main movie itself
|
|
||||||
break
|
|
||||||
self.add_trailer(item)
|
|
||||||
introsPlaylist = True
|
|
||||||
|
|
||||||
# -- ADD MAIN ITEM ONLY FOR HOMESCREEN ##############
|
|
||||||
if homeScreen and not seektime and not sizePlaylist:
|
|
||||||
# Extend our current playlist with the actual item to play
|
|
||||||
# only if there's no playlist first
|
|
||||||
log.info("Adding main item to playlist.")
|
|
||||||
add_item_to_kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
self.currentPosition,
|
|
||||||
kodi_id,
|
|
||||||
kodi_type)
|
|
||||||
|
|
||||||
elif contextmenu_play:
|
|
||||||
if state.DIRECT_PATHS:
|
|
||||||
# Cannot add via JSON with full metadata because then we
|
|
||||||
# Would be using the direct path
|
|
||||||
log.debug("Adding contextmenu item for direct paths")
|
|
||||||
if window('plex_%s.playmethod' % playurl) == "Transcode":
|
|
||||||
playutils.audioSubsPref(listitem, tryDecode(playurl))
|
|
||||||
api.CreateListItemFromPlexItem(listitem)
|
|
||||||
api.set_playback_win_props(playurl, listitem)
|
|
||||||
api.set_listitem_artwork(listitem)
|
|
||||||
add_listitem_to_Kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
self.currentPosition+1,
|
|
||||||
convert_PKC_to_listitem(listitem),
|
|
||||||
file=playurl,
|
|
||||||
kodi_item={'id': kodi_id, 'type': kodi_type})
|
|
||||||
else:
|
|
||||||
# Full metadata$
|
|
||||||
add_item_to_kodi_playlist(
|
|
||||||
playqueue,
|
|
||||||
self.currentPosition+1,
|
|
||||||
kodi_id,
|
|
||||||
kodi_type)
|
|
||||||
self.currentPosition += 1
|
|
||||||
if seektime:
|
|
||||||
window('plex_customplaylist.seektime', value=str(seektime))
|
|
||||||
|
|
||||||
# Ensure that additional parts are played after the main item
|
|
||||||
self.currentPosition += 1
|
|
||||||
|
|
||||||
# -- CHECK FOR ADDITIONAL PARTS ################
|
|
||||||
if len(item[0]) > 1:
|
|
||||||
self.add_part(item, api, kodi_id, kodi_type)
|
|
||||||
|
|
||||||
if dummyPlaylist:
|
|
||||||
# Added a dummy file to the playlist,
|
|
||||||
# because the first item is going to fail automatically.
|
|
||||||
log.info("Processed as a playlist. First item is skipped.")
|
|
||||||
# Delete the item that's gonna fail!
|
|
||||||
del playqueue.items[startPos]
|
|
||||||
# Don't attach listitem
|
|
||||||
return result
|
|
||||||
|
|
||||||
# We just skipped adding properties. Reset flag for next time.
|
|
||||||
elif propertiesPlayback:
|
|
||||||
log.debug("Resetting properties playback flag.")
|
|
||||||
window('plex_playbackProps', clear=True)
|
|
||||||
|
|
||||||
# SETUP MAIN ITEM ##########
|
|
||||||
# For transcoding only, ask for audio/subs pref
|
|
||||||
if (window('plex_%s.playmethod' % playurl) == "Transcode" and
|
|
||||||
not contextmenu_play):
|
|
||||||
playutils.audioSubsPref(listitem, tryDecode(playurl))
|
|
||||||
|
|
||||||
listitem.setPath(playurl)
|
|
||||||
api.set_playback_win_props(playurl, listitem)
|
|
||||||
api.set_listitem_artwork(listitem)
|
|
||||||
|
|
||||||
# PLAYBACK ################
|
|
||||||
if (homeScreen and seektime and window('plex_customplaylist') != "true"
|
|
||||||
and not contextmenu_play):
|
|
||||||
log.info("Play as a widget item")
|
|
||||||
api.CreateListItemFromPlexItem(listitem)
|
|
||||||
result.listitem = listitem
|
|
||||||
return result
|
|
||||||
|
|
||||||
elif ((introsPlaylist and window('plex_customplaylist') == "true") or
|
|
||||||
(homeScreen and not sizePlaylist) or
|
|
||||||
contextmenu_play):
|
|
||||||
# Playlist was created just now, play it.
|
|
||||||
# Contextmenu plays always need this
|
|
||||||
log.info("Play playlist from starting position %s" % startPos)
|
|
||||||
# Need a separate thread because Player won't return in time
|
|
||||||
thread = Thread(target=Player().play,
|
|
||||||
args=(playqueue.kodi_pl, None, False, startPos))
|
|
||||||
thread.setDaemon(True)
|
|
||||||
thread.start()
|
|
||||||
# Don't attach listitem
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
log.info("Play as a regular item")
|
|
||||||
result.listitem = listitem
|
|
||||||
return result
|
|
||||||
|
|
||||||
def play_all(self):
|
|
||||||
"""
|
|
||||||
Play all items contained in the xml passed in. Called by Plex Companion
|
|
||||||
"""
|
|
||||||
log.info("Playbackutils play_all called")
|
|
||||||
window('plex_playbackProps', value="true")
|
|
||||||
self.currentPosition = 0
|
|
||||||
for item in self.xml:
|
|
||||||
api = API(item)
|
|
||||||
successful = True
|
|
||||||
if api.getType() == v.PLEX_TYPE_CLIP:
|
|
||||||
self.add_trailer(item)
|
|
||||||
else:
|
|
||||||
with Get_Plex_DB() as plex_db:
|
|
||||||
db_item = plex_db.getItem_byId(api.getRatingKey())
|
|
||||||
if db_item is not None:
|
|
||||||
successful = add_item_to_kodi_playlist(
|
|
||||||
self.playqueue,
|
|
||||||
self.currentPosition,
|
|
||||||
kodi_id=db_item[0],
|
|
||||||
kodi_type=db_item[4])
|
|
||||||
if successful is True:
|
|
||||||
self.currentPosition += 1
|
|
||||||
if len(item[0]) > 1:
|
|
||||||
self.add_part(item,
|
|
||||||
api,
|
|
||||||
db_item[0],
|
|
||||||
db_item[4])
|
|
||||||
else:
|
|
||||||
# Item not in Kodi DB
|
|
||||||
self.add_trailer(item)
|
|
||||||
if successful is True:
|
|
||||||
self.playqueue.items[self.currentPosition - 1].ID = item.get(
|
|
||||||
'%sItemID' % self.playqueue.kind)
|
|
||||||
|
|
||||||
def add_trailer(self, item):
|
|
||||||
# Playurl needs to point back so we can get metadata!
|
|
||||||
path = "plugin://plugin.video.plexkodiconnect/movies/"
|
|
||||||
params = {
|
|
||||||
'mode': "play",
|
|
||||||
'dbid': 'plextrailer'
|
|
||||||
}
|
|
||||||
introAPI = API(item)
|
|
||||||
listitem = introAPI.CreateListItemFromPlexItem()
|
|
||||||
params['id'] = introAPI.getRatingKey()
|
|
||||||
params['filename'] = introAPI.getKey()
|
|
||||||
introPlayurl = path + '?' + urlencode(params)
|
|
||||||
introAPI.set_listitem_artwork(listitem)
|
|
||||||
# Overwrite the Plex url
|
|
||||||
listitem.setPath(introPlayurl)
|
|
||||||
log.info("Adding Plex trailer: %s" % introPlayurl)
|
|
||||||
add_listitem_to_Kodi_playlist(
|
|
||||||
self.playqueue,
|
|
||||||
self.currentPosition,
|
|
||||||
listitem,
|
|
||||||
introPlayurl,
|
|
||||||
xml_video_element=item)
|
|
||||||
self.currentPosition += 1
|
|
||||||
|
|
||||||
def add_part(self, item, api, kodi_id, kodi_type):
|
|
||||||
"""
|
|
||||||
Adds an additional part to the playlist
|
|
||||||
"""
|
|
||||||
# Only add to the playlist after intros have played
|
|
||||||
for counter, part in enumerate(item[0]):
|
|
||||||
# Never add first part
|
|
||||||
if counter == 0:
|
|
||||||
continue
|
|
||||||
# Set listitem and properties for each additional parts
|
|
||||||
api.setPartNumber(counter)
|
|
||||||
additionalListItem = xbmcgui.ListItem()
|
|
||||||
playutils = putils.PlayUtils(item)
|
|
||||||
additionalPlayurl = playutils.getPlayUrl(
|
|
||||||
partNumber=counter)
|
|
||||||
log.debug("Adding additional part: %s, url: %s"
|
|
||||||
% (counter, additionalPlayurl))
|
|
||||||
api.CreateListItemFromPlexItem(additionalListItem)
|
|
||||||
api.set_playback_win_props(additionalPlayurl,
|
|
||||||
additionalListItem)
|
|
||||||
api.set_listitem_artwork(additionalListItem)
|
|
||||||
add_listitem_to_playlist(
|
|
||||||
self.playqueue,
|
|
||||||
self.currentPosition,
|
|
||||||
additionalListItem,
|
|
||||||
kodi_id=kodi_id,
|
|
||||||
kodi_type=kodi_type,
|
|
||||||
plex_id=api.getRatingKey(),
|
|
||||||
file=additionalPlayurl)
|
|
||||||
self.currentPosition += 1
|
|
||||||
api.setPartNumber(0)
|
|
|
@ -1,393 +1,156 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
import json
|
import copy
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode
|
|
||||||
import downloadutils
|
|
||||||
import plexdb_functions as plexdb
|
|
||||||
import kodidb_functions as kodidb
|
import kodidb_functions as kodidb
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
from plexbmchelper.subscribers import LOCKER
|
||||||
|
from utils import kodi_time_to_millis, unix_date_to_kodi, unix_timestamp
|
||||||
import variables as v
|
import variables as v
|
||||||
import state
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class Player(xbmc.Player):
|
@LOCKER.lockthis
|
||||||
|
def playback_cleanup(ended=False):
|
||||||
|
"""
|
||||||
|
PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi
|
||||||
|
completely finished playing an item (because we will get and use wrong
|
||||||
|
timing data otherwise)
|
||||||
|
"""
|
||||||
|
LOG.debug('playback_cleanup called')
|
||||||
|
# We might have saved a transient token from a user flinging media via
|
||||||
|
# Companion (if we could not use the playqueue to store the token)
|
||||||
|
state.PLEX_TRANSIENT_TOKEN = None
|
||||||
|
for playerid in state.ACTIVE_PLAYERS:
|
||||||
|
status = state.PLAYER_STATES[playerid]
|
||||||
|
# Remember the last played item later
|
||||||
|
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status)
|
||||||
|
# Stop transcoding
|
||||||
|
if status['playmethod'] == 'Transcode':
|
||||||
|
LOG.debug('Tell the PMS to stop transcoding')
|
||||||
|
DU().downloadUrl(
|
||||||
|
'{server}/video/:/transcode/universal/stop',
|
||||||
|
parameters={'session': v.PKC_MACHINE_IDENTIFIER})
|
||||||
|
if playerid == 1:
|
||||||
|
# Bookmarks might not be pickup up correctly, so let's do them
|
||||||
|
# manually. Applies to addon paths, but direct paths might have
|
||||||
|
# started playback via PMS
|
||||||
|
_record_playstate(status, ended)
|
||||||
|
# Reset the player's status
|
||||||
|
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||||
|
# As all playback has halted, reset the players that have been active
|
||||||
|
state.ACTIVE_PLAYERS = []
|
||||||
|
LOG.debug('Finished PKC playback cleanup')
|
||||||
|
|
||||||
# Borg - multiple instances, shared state
|
|
||||||
_shared_state = {}
|
|
||||||
|
|
||||||
played_info = {}
|
def _record_playstate(status, ended):
|
||||||
playStats = {}
|
if not status['plex_id']:
|
||||||
currentFile = None
|
LOG.debug('No Plex id found to record playstate for status %s', status)
|
||||||
|
return
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
kodi_db_item = plex_db.getItem_byId(status['plex_id'])
|
||||||
|
if kodi_db_item is None:
|
||||||
|
# Item not (yet) in Kodi library
|
||||||
|
LOG.debug('No playstate update due to Plex id not found: %s', status)
|
||||||
|
return
|
||||||
|
totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000
|
||||||
|
if ended:
|
||||||
|
progress = 0.99
|
||||||
|
time = v.IGNORE_SECONDS_AT_START + 1
|
||||||
|
else:
|
||||||
|
time = float(kodi_time_to_millis(status['time'])) / 1000
|
||||||
|
try:
|
||||||
|
progress = time / totaltime
|
||||||
|
except ZeroDivisionError:
|
||||||
|
progress = 0.0
|
||||||
|
LOG.debug('Playback progress %s (%s of %s seconds)',
|
||||||
|
progress, time, totaltime)
|
||||||
|
playcount = status['playcount']
|
||||||
|
last_played = unix_date_to_kodi(unix_timestamp())
|
||||||
|
if playcount is None:
|
||||||
|
LOG.debug('playcount not found, looking it up in the Kodi DB')
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
playcount = kodi_db.get_playcount(kodi_db_item[1])
|
||||||
|
playcount = 0 if playcount is None else playcount
|
||||||
|
if time < v.IGNORE_SECONDS_AT_START:
|
||||||
|
LOG.debug('Ignoring playback less than %s seconds',
|
||||||
|
v.IGNORE_SECONDS_AT_START)
|
||||||
|
# Annoying Plex bug - it'll reset an already watched video to unwatched
|
||||||
|
playcount = None
|
||||||
|
last_played = None
|
||||||
|
time = 0
|
||||||
|
elif progress >= v.MARK_PLAYED_AT:
|
||||||
|
LOG.debug('Recording entirely played video since progress > %s',
|
||||||
|
v.MARK_PLAYED_AT)
|
||||||
|
playcount += 1
|
||||||
|
time = 0
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
kodi_db.addPlaystate(kodi_db_item[1],
|
||||||
|
time,
|
||||||
|
totaltime,
|
||||||
|
playcount,
|
||||||
|
last_played)
|
||||||
|
# Hack to force "in progress" widget to appear if it wasn't visible before
|
||||||
|
if (state.FORCE_RELOAD_SKIN and
|
||||||
|
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
|
||||||
|
LOG.debug('Refreshing skin to update widgets')
|
||||||
|
xbmc.executebuiltin('ReloadSkin()')
|
||||||
|
|
||||||
|
|
||||||
|
class PKC_Player(xbmc.Player):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__dict__ = self._shared_state
|
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
|
||||||
xbmc.Player.__init__(self)
|
xbmc.Player.__init__(self)
|
||||||
log.info("Started playback monitor.")
|
LOG.info("Started playback monitor.")
|
||||||
|
|
||||||
def GetPlayStats(self):
|
|
||||||
return self.playStats
|
|
||||||
|
|
||||||
def onPlayBackStarted(self):
|
def onPlayBackStarted(self):
|
||||||
"""
|
"""
|
||||||
Will be called when xbmc starts playing a file.
|
Will be called when xbmc starts playing a file.
|
||||||
Window values need to have been set in Kodimonitor.py
|
|
||||||
"""
|
"""
|
||||||
self.stopAll()
|
pass
|
||||||
|
|
||||||
# Get current file (in utf-8!)
|
|
||||||
try:
|
|
||||||
currentFile = tryDecode(self.getPlayingFile())
|
|
||||||
xbmc.sleep(300)
|
|
||||||
except:
|
|
||||||
currentFile = ""
|
|
||||||
count = 0
|
|
||||||
while not currentFile:
|
|
||||||
xbmc.sleep(100)
|
|
||||||
try:
|
|
||||||
currentFile = tryDecode(self.getPlayingFile())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if count == 20:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
count += 1
|
|
||||||
if not currentFile:
|
|
||||||
log.warn('Error getting currently playing file; abort reporting')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Save currentFile for cleanup later and for references
|
|
||||||
self.currentFile = currentFile
|
|
||||||
window('plex_lastPlayedFiled', value=currentFile)
|
|
||||||
# We may need to wait for info to be set in kodi monitor
|
|
||||||
itemId = window("plex_%s.itemid" % tryEncode(currentFile))
|
|
||||||
count = 0
|
|
||||||
while not itemId:
|
|
||||||
xbmc.sleep(200)
|
|
||||||
itemId = window("plex_%s.itemid" % tryEncode(currentFile))
|
|
||||||
if count == 5:
|
|
||||||
log.warn("Could not find itemId, cancelling playback report!")
|
|
||||||
return
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId))
|
|
||||||
|
|
||||||
plexitem = "plex_%s" % tryEncode(currentFile)
|
|
||||||
runtime = window("%s.runtime" % plexitem)
|
|
||||||
refresh_id = window("%s.refreshid" % plexitem)
|
|
||||||
playMethod = window("%s.playmethod" % plexitem)
|
|
||||||
itemType = window("%s.type" % plexitem)
|
|
||||||
try:
|
|
||||||
playcount = int(window("%s.playcount" % plexitem))
|
|
||||||
except ValueError:
|
|
||||||
playcount = 0
|
|
||||||
window('plex_skipWatched%s' % itemId, value="true")
|
|
||||||
|
|
||||||
log.debug("Playing itemtype is: %s" % itemType)
|
|
||||||
|
|
||||||
customseek = window('plex_customplaylist.seektime')
|
|
||||||
if customseek:
|
|
||||||
# Start at, when using custom playlist (play to Kodi from
|
|
||||||
# webclient)
|
|
||||||
log.info("Seeking to: %s" % customseek)
|
|
||||||
try:
|
|
||||||
self.seekTime(int(customseek))
|
|
||||||
except:
|
|
||||||
log.error('Could not seek!')
|
|
||||||
window('plex_customplaylist.seektime', clear=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
seekTime = self.getTime()
|
|
||||||
except RuntimeError:
|
|
||||||
log.error('Could not get current seektime from xbmc player')
|
|
||||||
seekTime = 0
|
|
||||||
|
|
||||||
# Get playback volume
|
|
||||||
volume_query = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Application.GetProperties",
|
|
||||||
"params": {
|
|
||||||
"properties": ["volume", "muted"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = xbmc.executeJSONRPC(json.dumps(volume_query))
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
volume = result.get('volume')
|
|
||||||
muted = result.get('muted')
|
|
||||||
|
|
||||||
# Postdata structure to send to plex server
|
|
||||||
url = "{server}/:/timeline?"
|
|
||||||
postdata = {
|
|
||||||
|
|
||||||
'QueueableMediaTypes': "Video",
|
|
||||||
'CanSeek': True,
|
|
||||||
'ItemId': itemId,
|
|
||||||
'MediaSourceId': itemId,
|
|
||||||
'PlayMethod': playMethod,
|
|
||||||
'VolumeLevel': volume,
|
|
||||||
'PositionTicks': int(seekTime * 10000000),
|
|
||||||
'IsMuted': muted
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get the current audio track and subtitles
|
|
||||||
if playMethod == "Transcode":
|
|
||||||
# property set in PlayUtils.py
|
|
||||||
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex"
|
|
||||||
% tryEncode(currentFile))
|
|
||||||
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex"
|
|
||||||
% tryEncode(currentFile))
|
|
||||||
else:
|
|
||||||
# Get the current kodi audio and subtitles and convert to plex equivalent
|
|
||||||
tracks_query = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "Player.GetProperties",
|
|
||||||
"params": {
|
|
||||||
|
|
||||||
"playerid": 1,
|
|
||||||
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
|
|
||||||
result = json.loads(result)
|
|
||||||
result = result.get('result')
|
|
||||||
|
|
||||||
try: # Audio tracks
|
|
||||||
indexAudio = result['currentaudiostream']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexAudio = 0
|
|
||||||
|
|
||||||
try: # Subtitles tracks
|
|
||||||
indexSubs = result['currentsubtitle']['index']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
indexSubs = 0
|
|
||||||
|
|
||||||
try: # If subtitles are enabled
|
|
||||||
subsEnabled = result['subtitleenabled']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
subsEnabled = ""
|
|
||||||
|
|
||||||
# Postdata for the audio
|
|
||||||
postdata['AudioStreamIndex'] = indexAudio + 1
|
|
||||||
|
|
||||||
# Postdata for the subtitles
|
|
||||||
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
|
|
||||||
|
|
||||||
# Number of audiotracks to help get plex Index
|
|
||||||
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
|
|
||||||
mapping = window("%s.indexMapping" % plexitem)
|
|
||||||
|
|
||||||
if mapping: # Set in playbackutils.py
|
|
||||||
|
|
||||||
log.debug("Mapping for external subtitles index: %s"
|
|
||||||
% mapping)
|
|
||||||
externalIndex = json.loads(mapping)
|
|
||||||
|
|
||||||
if externalIndex.get(str(indexSubs)):
|
|
||||||
# If the current subtitle is in the mapping
|
|
||||||
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
|
|
||||||
else:
|
|
||||||
# Internal subtitle currently selected
|
|
||||||
subindex = indexSubs - len(externalIndex) + audioTracks + 1
|
|
||||||
postdata['SubtitleStreamIndex'] = subindex
|
|
||||||
|
|
||||||
else: # Direct paths enabled scenario or no external subtitles set
|
|
||||||
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
|
|
||||||
else:
|
|
||||||
postdata['SubtitleStreamIndex'] = ""
|
|
||||||
|
|
||||||
|
|
||||||
# Post playback to server
|
|
||||||
# log("Sending POST play started: %s." % postdata, 2)
|
|
||||||
# self.doUtils(url, postBody=postdata, type="POST")
|
|
||||||
|
|
||||||
# Ensure we do have a runtime
|
|
||||||
try:
|
|
||||||
runtime = int(runtime)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
runtime = self.getTotalTime()
|
|
||||||
log.error("Runtime is missing, Kodi runtime: %s" % runtime)
|
|
||||||
except:
|
|
||||||
log.error('Could not get kodi runtime, setting to zero')
|
|
||||||
runtime = 0
|
|
||||||
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plex_dbitem = plex_db.getItem_byId(itemId)
|
|
||||||
try:
|
|
||||||
fileid = plex_dbitem[1]
|
|
||||||
except TypeError:
|
|
||||||
log.info("Could not find fileid in plex db.")
|
|
||||||
fileid = None
|
|
||||||
# Save data map for updates and position calls
|
|
||||||
data = {
|
|
||||||
'runtime': runtime,
|
|
||||||
'item_id': itemId,
|
|
||||||
'refresh_id': refresh_id,
|
|
||||||
'currentfile': currentFile,
|
|
||||||
'AudioStreamIndex': postdata['AudioStreamIndex'],
|
|
||||||
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
|
|
||||||
'playmethod': playMethod,
|
|
||||||
'Type': itemType,
|
|
||||||
'currentPosition': int(seekTime),
|
|
||||||
'fileid': fileid,
|
|
||||||
'itemType': itemType,
|
|
||||||
'playcount': playcount
|
|
||||||
}
|
|
||||||
|
|
||||||
self.played_info[currentFile] = data
|
|
||||||
log.info("ADDING_FILE: %s" % data)
|
|
||||||
|
|
||||||
# log some playback stats
|
|
||||||
'''if(itemType != None):
|
|
||||||
if(self.playStats.get(itemType) != None):
|
|
||||||
count = self.playStats.get(itemType) + 1
|
|
||||||
self.playStats[itemType] = count
|
|
||||||
else:
|
|
||||||
self.playStats[itemType] = 1
|
|
||||||
|
|
||||||
if(playMethod != None):
|
|
||||||
if(self.playStats.get(playMethod) != None):
|
|
||||||
count = self.playStats.get(playMethod) + 1
|
|
||||||
self.playStats[playMethod] = count
|
|
||||||
else:
|
|
||||||
self.playStats[playMethod] = 1'''
|
|
||||||
|
|
||||||
def onPlayBackPaused(self):
|
def onPlayBackPaused(self):
|
||||||
|
"""
|
||||||
currentFile = self.currentFile
|
Will be called when playback is paused
|
||||||
log.info("PLAYBACK_PAUSED: %s" % currentFile)
|
"""
|
||||||
|
pass
|
||||||
if self.played_info.get(currentFile):
|
|
||||||
self.played_info[currentFile]['paused'] = True
|
|
||||||
|
|
||||||
def onPlayBackResumed(self):
|
def onPlayBackResumed(self):
|
||||||
|
"""
|
||||||
currentFile = self.currentFile
|
Will be called when playback is resumed
|
||||||
log.info("PLAYBACK_RESUMED: %s" % currentFile)
|
"""
|
||||||
|
pass
|
||||||
if self.played_info.get(currentFile):
|
|
||||||
self.played_info[currentFile]['paused'] = False
|
|
||||||
|
|
||||||
def onPlayBackSeek(self, time, seekOffset):
|
def onPlayBackSeek(self, time, seekOffset):
|
||||||
# Make position when seeking a bit more accurate
|
"""
|
||||||
currentFile = self.currentFile
|
Will be called when user seeks to a certain time during playback
|
||||||
log.info("PLAYBACK_SEEK: %s" % currentFile)
|
"""
|
||||||
|
pass
|
||||||
if self.played_info.get(currentFile):
|
|
||||||
try:
|
|
||||||
position = self.getTime()
|
|
||||||
except RuntimeError:
|
|
||||||
# When Kodi is not playing
|
|
||||||
return
|
|
||||||
self.played_info[currentFile]['currentPosition'] = position
|
|
||||||
|
|
||||||
def onPlayBackStopped(self):
|
def onPlayBackStopped(self):
|
||||||
# Will be called when user stops xbmc playing a file
|
"""
|
||||||
log.info("ONPLAYBACK_STOPPED")
|
Will be called when playback is stopped by the user
|
||||||
|
"""
|
||||||
self.stopAll()
|
LOG.debug("ONPLAYBACK_STOPPED")
|
||||||
|
playback_cleanup()
|
||||||
for item in ('plex_currently_playing_itemid',
|
|
||||||
'plex_customplaylist',
|
|
||||||
'plex_customplaylist.seektime',
|
|
||||||
'plex_playbackProps',
|
|
||||||
'plex_forcetranscode'):
|
|
||||||
window(item, clear=True)
|
|
||||||
# We might have saved a transient token from a user flinging media via
|
|
||||||
# Companion (if we could not use the playqueue to store the token)
|
|
||||||
state.PLEX_TRANSIENT_TOKEN = None
|
|
||||||
log.debug("Cleared playlist properties.")
|
|
||||||
|
|
||||||
def onPlayBackEnded(self):
|
def onPlayBackEnded(self):
|
||||||
# Will be called when xbmc stops playing a file, because the file ended
|
"""
|
||||||
log.info("ONPLAYBACK_ENDED")
|
Will be called when playback ends due to the media file being finished
|
||||||
self.onPlayBackStopped()
|
"""
|
||||||
|
LOG.debug("ONPLAYBACK_ENDED")
|
||||||
def stopAll(self):
|
if state.PKC_CAUSED_STOP is True:
|
||||||
if not self.played_info:
|
state.PKC_CAUSED_STOP = False
|
||||||
return
|
LOG.debug('PKC caused this playback stop - ignoring')
|
||||||
log.info("Played_information: %s" % self.played_info)
|
else:
|
||||||
# Process each items
|
playback_cleanup(ended=True)
|
||||||
for item in self.played_info:
|
|
||||||
data = self.played_info.get(item)
|
|
||||||
if not data:
|
|
||||||
continue
|
|
||||||
log.debug("Item path: %s" % item)
|
|
||||||
log.debug("Item data: %s" % data)
|
|
||||||
|
|
||||||
runtime = data['runtime']
|
|
||||||
currentPosition = data['currentPosition']
|
|
||||||
itemid = data['item_id']
|
|
||||||
refresh_id = data['refresh_id']
|
|
||||||
currentFile = data['currentfile']
|
|
||||||
media_type = data['Type']
|
|
||||||
playMethod = data['playmethod']
|
|
||||||
|
|
||||||
# Prevent manually mark as watched in Kodi monitor
|
|
||||||
window('plex_skipWatched%s' % itemid, value="true")
|
|
||||||
|
|
||||||
if not currentPosition or not runtime:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
percentComplete = float(currentPosition) / float(runtime)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
# Runtime is 0.
|
|
||||||
percentComplete = 0
|
|
||||||
|
|
||||||
markPlayed = 0.90
|
|
||||||
log.info("Percent complete: %s Mark played at: %s"
|
|
||||||
% (percentComplete, markPlayed))
|
|
||||||
if percentComplete >= markPlayed:
|
|
||||||
# Kodi seems to sometimes overwrite our playstate, so wait
|
|
||||||
xbmc.sleep(500)
|
|
||||||
# Tell Kodi that we've finished watching (Plex knows)
|
|
||||||
if (data['fileid'] is not None and
|
|
||||||
data['itemType'] in (v.KODI_TYPE_MOVIE,
|
|
||||||
v.KODI_TYPE_EPISODE)):
|
|
||||||
with kodidb.GetKodiDB('video') as kodi_db:
|
|
||||||
kodi_db.addPlaystate(
|
|
||||||
data['fileid'],
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
data['playcount'] + 1,
|
|
||||||
DateToKodi(getUnixTimestamp()))
|
|
||||||
|
|
||||||
# Clean the WINDOW properties
|
|
||||||
for filename in self.played_info:
|
|
||||||
plex_item = 'plex_%s' % tryEncode(filename)
|
|
||||||
cleanup = (
|
|
||||||
'%s.itemid' % plex_item,
|
|
||||||
'%s.runtime' % plex_item,
|
|
||||||
'%s.refreshid' % plex_item,
|
|
||||||
'%s.playmethod' % plex_item,
|
|
||||||
'%s.type' % plex_item,
|
|
||||||
'%s.runtime' % plex_item,
|
|
||||||
'%s.playcount' % plex_item,
|
|
||||||
'%s.playlistPosition' % plex_item,
|
|
||||||
'%s.subtitle' % plex_item,
|
|
||||||
)
|
|
||||||
for item in cleanup:
|
|
||||||
window(item, clear=True)
|
|
||||||
|
|
||||||
# Stop transcoding
|
|
||||||
if playMethod == "Transcode":
|
|
||||||
log.info("Transcoding for %s terminating" % itemid)
|
|
||||||
self.doUtils(
|
|
||||||
"{server}/video/:/transcode/universal/stop",
|
|
||||||
parameters={'session': window('plex_client_Id')})
|
|
||||||
|
|
||||||
self.played_info.clear()
|
|
||||||
|
|
|
@ -1,100 +1,246 @@
|
||||||
import logging
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Collection of functions associated with Kodi and Plex playlists and playqueues
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
from urlparse import parse_qsl, urlsplit
|
from urlparse import parse_qsl, urlsplit
|
||||||
from re import compile as re_compile
|
from re import compile as re_compile
|
||||||
|
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
from downloadutils import DownloadUtils as DU
|
from downloadutils import DownloadUtils as DU
|
||||||
from utils import JSONRPC, tryEncode, escape_html
|
from utils import try_decode, try_encode
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from PlexFunctions import GetPlexMetadata
|
from PlexFunctions import GetPlexMetadata
|
||||||
|
from kodidb_functions import kodiid_from_filename
|
||||||
|
import json_rpc as js
|
||||||
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
REGEX = re_compile(r'''metadata%2F(\d+)''')
|
REGEX = re_compile(r'''metadata%2F(\d+)''')
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# kodi_item dict:
|
|
||||||
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
|
class PlaylistError(Exception):
|
||||||
|
"""
|
||||||
|
Exception for our playlist constructs
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Playlist_Object_Baseclase(object):
|
class PlaylistObjectBaseclase(object):
|
||||||
playlistid = None # Kodi playlist ID, [int]
|
"""
|
||||||
type = None # Kodi type: 'audio', 'video', 'picture'
|
Base class
|
||||||
kodi_pl = None # Kodi xbmc.PlayList object
|
"""
|
||||||
items = [] # list of PLAYLIST_ITEMS
|
def __init__(self):
|
||||||
old_kodi_pl = [] # to store old Kodi JSON result with all pl items
|
self.playlistid = None
|
||||||
ID = None # Plex id, e.g. playQueueID
|
self.type = None
|
||||||
version = None # Plex version, [int]
|
self.kodi_pl = None
|
||||||
selectedItemID = None
|
|
||||||
selectedItemOffset = None
|
|
||||||
shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ???
|
|
||||||
repeat = 0 # [int], 0: not repeated, 1: ??? 2: ???
|
|
||||||
# If Companion playback is initiated by another user
|
|
||||||
plex_transient_token = None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
answ = "<%s: " % (self.__class__.__name__)
|
|
||||||
# For some reason, can't use dir directly
|
|
||||||
answ += "ID: %s, " % self.ID
|
|
||||||
answ += "items: %s, " % self.items
|
|
||||||
for key in self.__dict__:
|
|
||||||
if key not in ("ID", 'items'):
|
|
||||||
if type(getattr(self, key)) in (str, unicode):
|
|
||||||
answ += '%s: %s, ' % (key, tryEncode(getattr(self, key)))
|
|
||||||
else:
|
|
||||||
# e.g. int
|
|
||||||
answ += '%s: %s, ' % (key, str(getattr(self, key)))
|
|
||||||
return answ[:-2] + ">"
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""
|
|
||||||
Resets the playlist object to an empty playlist
|
|
||||||
"""
|
|
||||||
# Clear Kodi playlist object
|
|
||||||
self.kodi_pl.clear()
|
|
||||||
self.items = []
|
self.items = []
|
||||||
self.old_kodi_pl = []
|
self.id = None
|
||||||
self.ID = None
|
|
||||||
self.version = None
|
self.version = None
|
||||||
self.selectedItemID = None
|
self.selectedItemID = None
|
||||||
self.selectedItemOffset = None
|
self.selectedItemOffset = None
|
||||||
self.shuffled = 0
|
self.shuffled = 0
|
||||||
self.repeat = 0
|
self.repeat = 0
|
||||||
self.plex_transient_token = None
|
self.plex_transient_token = None
|
||||||
log.debug('Playlist cleared: %s' % self)
|
# Need a hack for detecting swaps of elements
|
||||||
|
self.old_kodi_pl = []
|
||||||
|
# Workaround to avoid endless loops of detecting PL clears
|
||||||
|
self._clear_list = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""
|
||||||
|
Print the playlist, e.g. to log. Returns utf-8 encoded string
|
||||||
|
"""
|
||||||
|
answ = u'{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
|
||||||
|
# For some reason, can't use dir directly
|
||||||
|
for key in self.__dict__:
|
||||||
|
if key in ('id', 'items', 'kodi_pl'):
|
||||||
|
continue
|
||||||
|
if isinstance(getattr(self, key), str):
|
||||||
|
answ += '\'%s\': \'%s\', ' % (key,
|
||||||
|
try_decode(getattr(self, key)))
|
||||||
|
else:
|
||||||
|
# e.g. int
|
||||||
|
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
|
||||||
|
return try_encode(answ + '\'items\': %s}}') % self.items
|
||||||
|
|
||||||
|
def is_pkc_clear(self):
|
||||||
|
"""
|
||||||
|
Returns True if PKC has cleared the Kodi playqueue just recently.
|
||||||
|
Then this clear will be ignored from now on
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._clear_list.pop()
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self, kodi=True):
|
||||||
|
"""
|
||||||
|
Resets the playlist object to an empty playlist.
|
||||||
|
|
||||||
|
Pass kodi=False in order to NOT clear the Kodi playqueue
|
||||||
|
"""
|
||||||
|
# kodi monitor's on_clear method will only be called if there were some
|
||||||
|
# items to begin with
|
||||||
|
if kodi and self.kodi_pl.size() != 0:
|
||||||
|
self._clear_list.append(None)
|
||||||
|
self.kodi_pl.clear() # Clear Kodi playlist object
|
||||||
|
self.items = []
|
||||||
|
self.id = None
|
||||||
|
self.version = None
|
||||||
|
self.selectedItemID = None
|
||||||
|
self.selectedItemOffset = None
|
||||||
|
self.shuffled = 0
|
||||||
|
self.repeat = 0
|
||||||
|
self.plex_transient_token = None
|
||||||
|
self.old_kodi_pl = []
|
||||||
|
LOG.debug('Playlist cleared: %s', self)
|
||||||
|
|
||||||
|
|
||||||
class Playlist_Object(Playlist_Object_Baseclase):
|
class Playlist_Object(PlaylistObjectBaseclase):
|
||||||
|
"""
|
||||||
|
To be done for synching Plex playlists to Kodi
|
||||||
|
"""
|
||||||
kind = 'playList'
|
kind = 'playList'
|
||||||
|
|
||||||
|
|
||||||
class Playqueue_Object(Playlist_Object_Baseclase):
|
class Playqueue_Object(PlaylistObjectBaseclase):
|
||||||
|
"""
|
||||||
|
PKC object to represent PMS playQueues and Kodi playlist for queueing
|
||||||
|
|
||||||
|
playlistid = None [int] Kodi playlist id (0, 1, 2)
|
||||||
|
type = None [str] Kodi type: 'audio', 'video', 'picture'
|
||||||
|
kodi_pl = None Kodi xbmc.PlayList object
|
||||||
|
items = [] [list] of Playlist_Items
|
||||||
|
id = None [str] Plex playQueueID, unique Plex identifier
|
||||||
|
version = None [int] Plex version of the playQueue
|
||||||
|
selectedItemID = None
|
||||||
|
[str] Plex selectedItemID, playing element in queue
|
||||||
|
selectedItemOffset = None
|
||||||
|
[str] Offset of the playing element in queue
|
||||||
|
shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
|
||||||
|
repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
|
||||||
|
|
||||||
|
If Companion playback is initiated by another user:
|
||||||
|
plex_transient_token = None
|
||||||
|
"""
|
||||||
kind = 'playQueue'
|
kind = 'playQueue'
|
||||||
|
|
||||||
|
|
||||||
class Playlist_Item(object):
|
class Playlist_Item(object):
|
||||||
ID = None # Plex playlist/playqueue id, e.g. playQueueItemID
|
"""
|
||||||
plex_id = None # Plex unique item id, "ratingKey"
|
Object to fill our playqueues and playlists with.
|
||||||
plex_type = None # Plex type, e.g. 'movie', 'clip'
|
|
||||||
plex_UUID = None # Plex librarySectionUUID
|
id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID
|
||||||
kodi_id = None # Kodi unique kodi id (unique only within type!)
|
plex_id = None [str] Plex unique item id, "ratingKey"
|
||||||
kodi_type = None # Kodi type: 'movie'
|
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
|
||||||
file = None # Path to the item's file. STRING!!
|
plex_uuid = None [str] Plex librarySectionUUID
|
||||||
uri = None # Weird Plex uri path involving plex_UUID. STRING!
|
kodi_id = None Kodi unique kodi id (unique only within type!)
|
||||||
guid = None # Weird Plex guid
|
kodi_type = None [str] Kodi type: 'movie'
|
||||||
|
file = None [str] Path to the item's file. STRING!!
|
||||||
|
uri = None [str] Weird Plex uri path involving plex_uuid. STRING!
|
||||||
|
guid = None [str] Weird Plex guid
|
||||||
|
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
|
||||||
|
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
|
||||||
|
playcount = None [int] how many times the item has already been played
|
||||||
|
offset = None [int] the item's view offset UPON START in Plex time
|
||||||
|
part = 0 [int] part number if Plex video consists of mult. parts
|
||||||
|
force_transcode [bool] defaults to False
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.id = None
|
||||||
|
self.plex_id = None
|
||||||
|
self.plex_type = None
|
||||||
|
self.plex_uuid = None
|
||||||
|
self.kodi_id = None
|
||||||
|
self.kodi_type = None
|
||||||
|
self.file = None
|
||||||
|
self.uri = None
|
||||||
|
self.guid = None
|
||||||
|
self.xml = None
|
||||||
|
self.playmethod = None
|
||||||
|
self.playcount = None
|
||||||
|
self.offset = None
|
||||||
|
# If Plex video consists of several parts; part number
|
||||||
|
self.part = 0
|
||||||
|
self.force_transcode = False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
answ = "<%s: " % (self.__class__.__name__)
|
"""
|
||||||
|
Print the playlist item, e.g. to log. Returns utf-8 encoded string
|
||||||
|
"""
|
||||||
|
answ = (u'{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
|
||||||
|
% (self.__class__.__name__, self.id, self.plex_id))
|
||||||
for key in self.__dict__:
|
for key in self.__dict__:
|
||||||
if type(getattr(self, key)) in (str, unicode):
|
if key in ('id', 'plex_id', 'xml'):
|
||||||
answ += '%s: %s, ' % (key, tryEncode(getattr(self, key)))
|
continue
|
||||||
|
if isinstance(getattr(self, key), str):
|
||||||
|
answ += '\'%s\': \'%s\', ' % (key,
|
||||||
|
try_decode(getattr(self, key)))
|
||||||
else:
|
else:
|
||||||
# e.g. int
|
# e.g. int
|
||||||
answ += '%s: %s, ' % (key, str(getattr(self, key)))
|
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
|
||||||
return answ[:-2] + ">"
|
if self.xml is None:
|
||||||
|
answ += '\'xml\': None}}'
|
||||||
|
else:
|
||||||
|
answ += '\'xml\': \'%s\'}}' % self.xml.tag
|
||||||
|
return try_encode(answ)
|
||||||
|
|
||||||
|
def plex_stream_index(self, kodi_stream_index, stream_type):
|
||||||
|
"""
|
||||||
|
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||||
|
index.
|
||||||
|
|
||||||
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
|
|
||||||
|
Returns None if unsuccessful
|
||||||
|
"""
|
||||||
|
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
||||||
|
count = 0
|
||||||
|
# Kodi indexes differently than Plex
|
||||||
|
for stream in self.xml[0][self.part]:
|
||||||
|
if (stream.attrib['streamType'] == stream_type and
|
||||||
|
'key' in stream.attrib):
|
||||||
|
if count == kodi_stream_index:
|
||||||
|
return stream.attrib['id']
|
||||||
|
count += 1
|
||||||
|
for stream in self.xml[0][self.part]:
|
||||||
|
if (stream.attrib['streamType'] == stream_type and
|
||||||
|
'key' not in stream.attrib):
|
||||||
|
if count == kodi_stream_index:
|
||||||
|
return stream.attrib['id']
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
def kodi_stream_index(self, plex_stream_index, stream_type):
|
||||||
|
"""
|
||||||
|
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||||
|
index.
|
||||||
|
|
||||||
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
|
|
||||||
|
Returns None if unsuccessful
|
||||||
|
"""
|
||||||
|
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
||||||
|
count = 0
|
||||||
|
for stream in self.xml[0][self.part]:
|
||||||
|
if (stream.attrib['streamType'] == stream_type and
|
||||||
|
'key' in stream.attrib):
|
||||||
|
if stream.attrib['id'] == plex_stream_index:
|
||||||
|
return count
|
||||||
|
count += 1
|
||||||
|
for stream in self.xml[0][self.part]:
|
||||||
|
if (stream.attrib['streamType'] == stream_type and
|
||||||
|
'key' not in stream.attrib):
|
||||||
|
if stream.attrib['id'] == plex_stream_index:
|
||||||
|
return count
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
def playlist_item_from_kodi(kodi_item):
|
def playlist_item_from_kodi(kodi_item):
|
||||||
|
@ -114,7 +260,7 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
try:
|
try:
|
||||||
item.plex_id = plex_dbitem[0]
|
item.plex_id = plex_dbitem[0]
|
||||||
item.plex_type = plex_dbitem[2]
|
item.plex_type = plex_dbitem[2]
|
||||||
item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-)
|
item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
item.file = kodi_item.get('file')
|
item.file = kodi_item.get('file')
|
||||||
|
@ -122,16 +268,52 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
query = dict(parse_qsl(urlsplit(item.file).query))
|
query = dict(parse_qsl(urlsplit(item.file).query))
|
||||||
item.plex_id = query.get('plex_id')
|
item.plex_id = query.get('plex_id')
|
||||||
item.plex_type = query.get('itemType')
|
item.plex_type = query.get('itemType')
|
||||||
if item.plex_id is None:
|
if item.plex_id is None and item.file is not None:
|
||||||
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
|
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
|
||||||
else:
|
else:
|
||||||
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
||||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||||
(item.plex_UUID, item.plex_id))
|
(item.plex_uuid, item.plex_id))
|
||||||
log.debug('Made playlist item from Kodi: %s' % item)
|
LOG.debug('Made playlist item from Kodi: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def verify_kodi_item(plex_id, kodi_item):
|
||||||
|
"""
|
||||||
|
Tries to lookup kodi_id and kodi_type for kodi_item (with kodi_item['file']
|
||||||
|
supplied) - if and only if plex_id is None.
|
||||||
|
|
||||||
|
Returns the kodi_item with kodi_item['id'] and kodi_item['type'] possibly
|
||||||
|
set to None if unsuccessful.
|
||||||
|
|
||||||
|
Will raise a PlaylistError if plex_id is None and kodi_item['file'] starts
|
||||||
|
with either 'plugin' or 'http'
|
||||||
|
"""
|
||||||
|
if plex_id is not None or kodi_item.get('id') is not None:
|
||||||
|
# Got all the info we need
|
||||||
|
return kodi_item
|
||||||
|
# Need more info since we don't have kodi_id nor type. Use file path.
|
||||||
|
if (kodi_item['file'].startswith('plugin') or
|
||||||
|
kodi_item['file'].startswith('http')):
|
||||||
|
raise PlaylistError('kodi_item cannot be used for Plex playback')
|
||||||
|
LOG.debug('Starting research for Kodi id since we didnt get one: %s',
|
||||||
|
kodi_item)
|
||||||
|
kodi_id = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_MOVIE)
|
||||||
|
kodi_item['type'] = v.KODI_TYPE_MOVIE
|
||||||
|
if kodi_id is None:
|
||||||
|
kodi_id = kodiid_from_filename(kodi_item['file'],
|
||||||
|
v.KODI_TYPE_EPISODE)
|
||||||
|
kodi_item['type'] = v.KODI_TYPE_EPISODE
|
||||||
|
if kodi_id is None:
|
||||||
|
kodi_id = kodiid_from_filename(kodi_item['file'],
|
||||||
|
v.KODI_TYPE_SONG)
|
||||||
|
kodi_item['type'] = v.KODI_TYPE_SONG
|
||||||
|
kodi_item['id'] = kodi_id
|
||||||
|
kodi_item['type'] = None if kodi_id is None else kodi_item['type']
|
||||||
|
LOG.debug('Research results for kodi_item: %s', kodi_item)
|
||||||
|
return kodi_item
|
||||||
|
|
||||||
|
|
||||||
def playlist_item_from_plex(plex_id):
|
def playlist_item_from_plex(plex_id):
|
||||||
"""
|
"""
|
||||||
Returns a playlist element providing the plex_id ("ratingKey")
|
Returns a playlist element providing the plex_id ("ratingKey")
|
||||||
|
@ -146,72 +328,79 @@ def playlist_item_from_plex(plex_id):
|
||||||
item.plex_type = plex_dbitem[5]
|
item.plex_type = plex_dbitem[5]
|
||||||
item.kodi_id = plex_dbitem[0]
|
item.kodi_id = plex_dbitem[0]
|
||||||
item.kodi_type = plex_dbitem[4]
|
item.kodi_type = plex_dbitem[4]
|
||||||
except:
|
except (TypeError, IndexError):
|
||||||
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
||||||
item.plex_UUID = plex_id
|
item.plex_uuid = plex_id
|
||||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||||
(item.plex_UUID, plex_id))
|
(item.plex_uuid, plex_id))
|
||||||
log.debug('Made playlist item from plex: %s' % item)
|
LOG.debug('Made playlist item from plex: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def playlist_item_from_xml(playlist, xml_video_element):
|
def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
||||||
"""
|
"""
|
||||||
Returns a playlist element for the playqueue using the Plex xml
|
Returns a playlist element for the playqueue using the Plex xml
|
||||||
|
|
||||||
|
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
|
||||||
"""
|
"""
|
||||||
item = Playlist_Item()
|
item = Playlist_Item()
|
||||||
api = API(xml_video_element)
|
api = API(xml_video_element)
|
||||||
item.plex_id = api.getRatingKey()
|
item.plex_id = api.plex_id()
|
||||||
item.plex_type = api.getType()
|
item.plex_type = api.plex_type()
|
||||||
item.ID = xml_video_element.attrib['%sItemID' % playlist.kind]
|
# item.id will only be set if you passed in an xml_video_element from e.g.
|
||||||
item.guid = xml_video_element.attrib.get('guid')
|
# a playQueue
|
||||||
if item.guid is not None:
|
item.id = api.item_id()
|
||||||
item.guid = escape_html(item.guid)
|
if kodi_id is not None:
|
||||||
if item.plex_id:
|
item.kodi_id = kodi_id
|
||||||
|
item.kodi_type = kodi_type
|
||||||
|
elif item.plex_id is not None:
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
db_element = plex_db.getItem_byId(item.plex_id)
|
db_element = plex_db.getItem_byId(item.plex_id)
|
||||||
try:
|
try:
|
||||||
item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4]
|
item.kodi_id, item.kodi_type = db_element[0], db_element[4]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
log.debug('Created new playlist item from xml: %s' % item)
|
item.guid = api.guid_html_escaped()
|
||||||
|
item.playcount = api.viewcount()
|
||||||
|
item.offset = api.resume_point()
|
||||||
|
item.xml = xml_video_element
|
||||||
|
LOG.debug('Created new playlist item from xml: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def _get_playListVersion_from_xml(playlist, xml):
|
def _get_playListVersion_from_xml(playlist, xml):
|
||||||
"""
|
"""
|
||||||
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
||||||
playQueueVersion). Returns True if successful, False otherwise
|
playQueueVersion).
|
||||||
|
|
||||||
|
Raises PlaylistError if unsuccessful
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
|
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
|
||||||
except (TypeError, AttributeError, KeyError):
|
except (TypeError, AttributeError, KeyError):
|
||||||
log.error('Could not get new playlist Version for playlist %s'
|
raise PlaylistError('Could not get new playlist Version for playlist '
|
||||||
% playlist)
|
'%s' % playlist)
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_details_from_xml(playlist, xml):
|
def get_playlist_details_from_xml(playlist, xml):
|
||||||
"""
|
"""
|
||||||
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
|
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
|
||||||
playlist.ID with the XML's playQueueID
|
playlist.id with the XML's playQueueID
|
||||||
|
|
||||||
|
Raises PlaylistError if something went wrong.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
playlist.ID = xml.attrib['%sID' % playlist.kind]
|
playlist.id = xml.attrib['%sID' % playlist.kind]
|
||||||
playlist.version = xml.attrib['%sVersion' % playlist.kind]
|
playlist.version = xml.attrib['%sVersion' % playlist.kind]
|
||||||
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
|
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
|
||||||
playlist.selectedItemID = xml.attrib.get(
|
playlist.selectedItemID = xml.attrib.get(
|
||||||
'%sSelectedItemID' % playlist.kind)
|
'%sSelectedItemID' % playlist.kind)
|
||||||
playlist.selectedItemOffset = xml.attrib.get(
|
playlist.selectedItemOffset = xml.attrib.get(
|
||||||
'%sSelectedItemOffset' % playlist.kind)
|
'%sSelectedItemOffset' % playlist.kind)
|
||||||
except:
|
LOG.debug('Updated playlist from xml: %s', playlist)
|
||||||
log.error('Could not parse xml answer from PMS for playlist %s'
|
except (TypeError, KeyError, AttributeError) as msg:
|
||||||
% playlist)
|
raise PlaylistError('Could not get playlist details from xml: %s',
|
||||||
import traceback
|
msg)
|
||||||
log.error(traceback.format_exc())
|
|
||||||
raise KeyError
|
|
||||||
log.debug('Updated playlist from xml: %s' % playlist)
|
|
||||||
|
|
||||||
|
|
||||||
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
||||||
|
@ -226,11 +415,7 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
||||||
# Clear our existing playlist and the associated Kodi playlist
|
# Clear our existing playlist and the associated Kodi playlist
|
||||||
playlist.clear()
|
playlist.clear()
|
||||||
# Set new values
|
# Set new values
|
||||||
try:
|
get_playlist_details_from_xml(playlist, xml)
|
||||||
get_playlist_details_from_xml(playlist, xml)
|
|
||||||
except KeyError:
|
|
||||||
log.error('Could not update playlist from PMS')
|
|
||||||
return
|
|
||||||
for plex_item in xml:
|
for plex_item in xml:
|
||||||
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
|
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
|
||||||
if playlist_item is not None:
|
if playlist_item is not None:
|
||||||
|
@ -240,10 +425,13 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
||||||
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
|
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
|
||||||
"""
|
"""
|
||||||
Initializes the Plex side without changing the Kodi playlists
|
Initializes the Plex side without changing the Kodi playlists
|
||||||
|
WILL ALSO UPDATE OUR PLAYLISTS.
|
||||||
|
|
||||||
WILL ALSO UPDATE OUR PLAYLISTS
|
Returns the first PKC playlist item or raises PlaylistError
|
||||||
"""
|
"""
|
||||||
log.debug('Initializing the playlist %s on the Plex side' % playlist)
|
LOG.debug('Initializing the playlist on the Plex side: %s', playlist)
|
||||||
|
playlist.clear(kodi=False)
|
||||||
|
verify_kodi_item(plex_id, kodi_item)
|
||||||
try:
|
try:
|
||||||
if plex_id:
|
if plex_id:
|
||||||
item = playlist_item_from_plex(plex_id)
|
item = playlist_item_from_plex(plex_id)
|
||||||
|
@ -258,11 +446,14 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
|
||||||
action_type="POST",
|
action_type="POST",
|
||||||
parameters=params)
|
parameters=params)
|
||||||
get_playlist_details_from_xml(playlist, xml)
|
get_playlist_details_from_xml(playlist, xml)
|
||||||
except KeyError:
|
# Need to get the details for the playlist item
|
||||||
log.error('Could not init Plex playlist')
|
item = playlist_item_from_xml(xml[0])
|
||||||
return
|
except (KeyError, IndexError, TypeError):
|
||||||
|
raise PlaylistError('Could not init Plex playlist with plex_id %s and '
|
||||||
|
'kodi_item %s' % (plex_id, kodi_item))
|
||||||
playlist.items.append(item)
|
playlist.items.append(item)
|
||||||
log.debug('Initialized the playlist on the Plex side: %s' % playlist)
|
LOG.debug('Initialized the playlist on the Plex side: %s', playlist)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
|
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
|
||||||
|
@ -274,10 +465,10 @@ def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
|
||||||
|
|
||||||
file: str!!
|
file: str!!
|
||||||
"""
|
"""
|
||||||
log.debug('add_listitem_to_playlist at position %s. Playlist before add: '
|
LOG.debug('add_listitem_to_playlist at position %s. Playlist before add: '
|
||||||
'%s' % (pos, playlist))
|
'%s', pos, playlist)
|
||||||
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
||||||
if playlist.ID is None:
|
if playlist.id is None:
|
||||||
init_Plex_playlist(playlist, plex_id, kodi_item)
|
init_Plex_playlist(playlist, plex_id, kodi_item)
|
||||||
else:
|
else:
|
||||||
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
||||||
|
@ -300,51 +491,58 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
plex_id=None, file=None):
|
plex_id=None, file=None):
|
||||||
"""
|
"""
|
||||||
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
|
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
|
||||||
|
file: str!
|
||||||
|
|
||||||
file: str!
|
Raises PlaylistError if something went wrong
|
||||||
"""
|
"""
|
||||||
log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist)
|
LOG.debug('add_item_to_playlist. Playlist before adding: %s', playlist)
|
||||||
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
|
||||||
if playlist.ID is None:
|
if playlist.id is None:
|
||||||
init_Plex_playlist(playlist, plex_id, kodi_item)
|
item = init_Plex_playlist(playlist, plex_id, kodi_item)
|
||||||
else:
|
else:
|
||||||
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
item = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
|
||||||
kodi_id = playlist.items[pos].kodi_id
|
params = {
|
||||||
kodi_type = playlist.items[pos].kodi_type
|
'playlistid': playlist.playlistid,
|
||||||
file = playlist.items[pos].file
|
'position': pos
|
||||||
add_item_to_kodi_playlist(playlist, pos, kodi_id, kodi_type, file)
|
}
|
||||||
|
if item.kodi_id is not None:
|
||||||
|
params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)}
|
||||||
|
else:
|
||||||
|
params['item'] = {'file': item.file}
|
||||||
|
reply = js.playlist_insert(params)
|
||||||
|
if reply.get('error') is not None:
|
||||||
|
raise PlaylistError('Could not add item to playlist. Kodi reply. %s'
|
||||||
|
% reply)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
|
def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
"""
|
"""
|
||||||
Adds a new item to the playlist at position pos [int] only on the Plex
|
Adds a new item to the playlist at position pos [int] only on the Plex
|
||||||
side of things (e.g. because the user changed the Kodi side)
|
side of things (e.g. because the user changed the Kodi side)
|
||||||
|
|
||||||
WILL ALSO UPDATE OUR PLAYLISTS
|
WILL ALSO UPDATE OUR PLAYLISTS
|
||||||
|
|
||||||
|
Returns the PKC PlayList item or raises PlaylistError
|
||||||
"""
|
"""
|
||||||
|
verify_kodi_item(plex_id, kodi_item)
|
||||||
if plex_id:
|
if plex_id:
|
||||||
try:
|
item = playlist_item_from_plex(plex_id)
|
||||||
item = playlist_item_from_plex(plex_id)
|
|
||||||
except KeyError:
|
|
||||||
log.error('Could not add new item to the PMS playlist')
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
item = playlist_item_from_kodi(kodi_item)
|
item = playlist_item_from_kodi(kodi_item)
|
||||||
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri)
|
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri)
|
||||||
# Will always put the new item at the end of the Plex playlist
|
# Will always put the new item at the end of the Plex playlist
|
||||||
xml = DU().downloadUrl(url, action_type="PUT")
|
xml = DU().downloadUrl(url, action_type="PUT")
|
||||||
try:
|
try:
|
||||||
item.ID = xml[-1].attrib['%sItemID' % playlist.kind]
|
xml[-1].attrib
|
||||||
except IndexError:
|
except (TypeError, AttributeError, KeyError, IndexError):
|
||||||
log.info('Could not get playlist children. Adding a dummy')
|
raise PlaylistError('Could not add item %s to playlist %s'
|
||||||
except (TypeError, AttributeError, KeyError):
|
% (kodi_item, playlist))
|
||||||
log.error('Could not add item %s to playlist %s'
|
api = API(xml[-1])
|
||||||
% (kodi_item, playlist))
|
item.xml = xml[-1]
|
||||||
return
|
item.id = api.item_id()
|
||||||
# Get the guid for this item
|
item.guid = api.guid_html_escaped()
|
||||||
for plex_item in xml:
|
item.offset = api.resume_point()
|
||||||
if plex_item.attrib['%sItemID' % playlist.kind] == item.ID:
|
item.playcount = api.viewcount()
|
||||||
item.guid = escape_html(plex_item.attrib['guid'])
|
|
||||||
playlist.items.append(item)
|
playlist.items.append(item)
|
||||||
if pos == len(playlist.items) - 1:
|
if pos == len(playlist.items) - 1:
|
||||||
# Item was added at the end
|
# Item was added at the end
|
||||||
|
@ -354,21 +552,22 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
move_playlist_item(playlist,
|
move_playlist_item(playlist,
|
||||||
len(playlist.items) - 1,
|
len(playlist.items) - 1,
|
||||||
pos)
|
pos)
|
||||||
log.debug('Successfully added item on the Plex side: %s' % playlist)
|
LOG.debug('Successfully added item on the Plex side: %s', playlist)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
file=None):
|
file=None, xml_video_element=None):
|
||||||
"""
|
"""
|
||||||
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
|
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
|
||||||
|
|
||||||
Returns False if unsuccessful
|
Returns the playlist item that was just added or raises PlaylistError
|
||||||
|
|
||||||
file: str!
|
file: str!
|
||||||
"""
|
"""
|
||||||
log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
|
LOG.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
|
||||||
'only at position %s for %s'
|
'only at position %s for %s',
|
||||||
% (kodi_id, kodi_type, file, pos, playlist))
|
kodi_id, kodi_type, file, pos, playlist)
|
||||||
params = {
|
params = {
|
||||||
'playlistid': playlist.playlistid,
|
'playlistid': playlist.playlistid,
|
||||||
'position': pos
|
'position': pos
|
||||||
|
@ -377,43 +576,50 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
|
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
|
||||||
else:
|
else:
|
||||||
params['item'] = {'file': file}
|
params['item'] = {'file': file}
|
||||||
reply = JSONRPC('Playlist.Insert').execute(params)
|
reply = js.playlist_insert(params)
|
||||||
if reply.get('error') is not None:
|
if reply.get('error') is not None:
|
||||||
log.error('Could not add item to playlist. Kodi reply. %s' % reply)
|
raise PlaylistError('Could not add item to playlist. Kodi reply. %s',
|
||||||
return False
|
reply)
|
||||||
else:
|
if xml_video_element is not None:
|
||||||
playlist.items.insert(pos, playlist_item_from_kodi(
|
item = playlist_item_from_xml(xml_video_element)
|
||||||
{'id': kodi_id, 'type': kodi_type, 'file': file}))
|
item.kodi_id = kodi_id
|
||||||
return True
|
item.kodi_type = kodi_type
|
||||||
|
item.file = file
|
||||||
|
elif kodi_id is not None:
|
||||||
|
item = playlist_item_from_kodi(
|
||||||
|
{'id': kodi_id, 'type': kodi_type, 'file': file})
|
||||||
|
if item.plex_id is not None:
|
||||||
|
xml = GetPlexMetadata(item.plex_id)
|
||||||
|
item.xml = xml[-1]
|
||||||
|
playlist.items.insert(pos, item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def move_playlist_item(playlist, before_pos, after_pos):
|
def move_playlist_item(playlist, before_pos, after_pos):
|
||||||
"""
|
"""
|
||||||
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
|
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
|
||||||
|
|
||||||
WILL ALSO CHANGE OUR PLAYLISTS. Returns True if successful
|
WILL ALSO CHANGE OUR PLAYLISTS.
|
||||||
"""
|
"""
|
||||||
log.debug('Moving item from %s to %s on the Plex side for %s'
|
LOG.debug('Moving item from %s to %s on the Plex side for %s',
|
||||||
% (before_pos, after_pos, playlist))
|
before_pos, after_pos, playlist)
|
||||||
if after_pos == 0:
|
if after_pos == 0:
|
||||||
url = "{server}/%ss/%s/items/%s/move?after=0" % \
|
url = "{server}/%ss/%s/items/%s/move?after=0" % \
|
||||||
(playlist.kind,
|
(playlist.kind,
|
||||||
playlist.ID,
|
playlist.id,
|
||||||
playlist.items[before_pos].ID)
|
playlist.items[before_pos].id)
|
||||||
else:
|
else:
|
||||||
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
|
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
|
||||||
(playlist.kind,
|
(playlist.kind,
|
||||||
playlist.ID,
|
playlist.id,
|
||||||
playlist.items[before_pos].ID,
|
playlist.items[before_pos].id,
|
||||||
playlist.items[after_pos - 1].ID)
|
playlist.items[after_pos - 1].id)
|
||||||
# We need to increment the playlistVersion
|
# We need to increment the playlistVersion
|
||||||
if _get_playListVersion_from_xml(
|
_get_playListVersion_from_xml(
|
||||||
playlist, DU().downloadUrl(url, action_type="PUT")) is False:
|
playlist, DU().downloadUrl(url, action_type="PUT"))
|
||||||
return False
|
|
||||||
# Move our item's position in our internal playlist
|
# Move our item's position in our internal playlist
|
||||||
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
|
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
|
||||||
log.debug('Done moving for %s' % playlist)
|
LOG.debug('Done moving for %s', playlist)
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_PMS_playlist(playlist, playlist_id=None):
|
def get_PMS_playlist(playlist, playlist_id=None):
|
||||||
|
@ -423,7 +629,7 @@ def get_PMS_playlist(playlist, playlist_id=None):
|
||||||
|
|
||||||
Returns None if something went wrong
|
Returns None if something went wrong
|
||||||
"""
|
"""
|
||||||
playlist_id = playlist_id if playlist_id else playlist.ID
|
playlist_id = playlist_id if playlist_id else playlist.id
|
||||||
xml = DU().downloadUrl(
|
xml = DU().downloadUrl(
|
||||||
"{server}/%ss/%s" % (playlist.kind, playlist_id),
|
"{server}/%ss/%s" % (playlist.kind, playlist_id),
|
||||||
headerOptions={'Accept': 'application/xml'})
|
headerOptions={'Accept': 'application/xml'})
|
||||||
|
@ -439,63 +645,24 @@ def refresh_playlist_from_PMS(playlist):
|
||||||
Only updates the selected item from the PMS side (e.g.
|
Only updates the selected item from the PMS side (e.g.
|
||||||
playQueueSelectedItemID). Will NOT check whether items still make sense.
|
playQueueSelectedItemID). Will NOT check whether items still make sense.
|
||||||
"""
|
"""
|
||||||
xml = get_PMS_playlist(playlist)
|
get_playlist_details_from_xml(playlist, get_PMS_playlist(playlist))
|
||||||
try:
|
|
||||||
get_playlist_details_from_xml(playlist, xml)
|
|
||||||
except KeyError:
|
|
||||||
log.error('Could not refresh playlist from PMS')
|
|
||||||
|
|
||||||
|
|
||||||
def delete_playlist_item_from_PMS(playlist, pos):
|
def delete_playlist_item_from_PMS(playlist, pos):
|
||||||
"""
|
"""
|
||||||
Delete the item at position pos [int] on the Plex side and our playlists
|
Delete the item at position pos [int] on the Plex side and our playlists
|
||||||
"""
|
"""
|
||||||
log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist))
|
LOG.debug('Deleting position %s for %s on the Plex side', pos, playlist)
|
||||||
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
|
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
|
||||||
(playlist.kind,
|
(playlist.kind,
|
||||||
playlist.ID,
|
playlist.id,
|
||||||
playlist.items[pos].ID,
|
playlist.items[pos].id,
|
||||||
playlist.repeat),
|
playlist.repeat),
|
||||||
action_type="DELETE")
|
action_type="DELETE")
|
||||||
_get_playListVersion_from_xml(playlist, xml)
|
_get_playListVersion_from_xml(playlist, xml)
|
||||||
del playlist.items[pos]
|
del playlist.items[pos]
|
||||||
|
|
||||||
|
|
||||||
def get_kodi_playlist_items(playlist):
|
|
||||||
"""
|
|
||||||
Returns a list of the current Kodi playlist items using JSON
|
|
||||||
|
|
||||||
E.g.:
|
|
||||||
[{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file':
|
|
||||||
u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}]
|
|
||||||
"""
|
|
||||||
answ = JSONRPC('Playlist.GetItems').execute({
|
|
||||||
'playlistid': playlist.playlistid,
|
|
||||||
'properties': ["title", "file"]
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
answ = answ['result']['items']
|
|
||||||
except KeyError:
|
|
||||||
answ = []
|
|
||||||
return answ
|
|
||||||
|
|
||||||
|
|
||||||
def get_kodi_playqueues():
|
|
||||||
"""
|
|
||||||
Example return: [{u'playlistid': 0, u'type': u'audio'},
|
|
||||||
{u'playlistid': 1, u'type': u'video'},
|
|
||||||
{u'playlistid': 2, u'type': u'picture'}]
|
|
||||||
"""
|
|
||||||
queues = JSONRPC('Playlist.GetPlaylists').execute()
|
|
||||||
try:
|
|
||||||
queues = queues['result']
|
|
||||||
except KeyError:
|
|
||||||
log.error('Could not get Kodi playqueues. JSON Result was: %s'
|
|
||||||
% queues)
|
|
||||||
queues = []
|
|
||||||
return queues
|
|
||||||
|
|
||||||
|
|
||||||
# Functions operating on the Kodi playlist objects ##########
|
# Functions operating on the Kodi playlist objects ##########
|
||||||
|
|
||||||
def add_to_Kodi_playlist(playlist, xml_video_element):
|
def add_to_Kodi_playlist(playlist, xml_video_element):
|
||||||
|
@ -503,23 +670,18 @@ def add_to_Kodi_playlist(playlist, xml_video_element):
|
||||||
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
|
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
|
||||||
Pass in the PMS xml's video element (one level underneath MediaContainer).
|
Pass in the PMS xml's video element (one level underneath MediaContainer).
|
||||||
|
|
||||||
Returns a Playlist_Item or None if it did not work
|
Returns a Playlist_Item or raises PlaylistError
|
||||||
"""
|
"""
|
||||||
item = playlist_item_from_xml(playlist, xml_video_element)
|
item = playlist_item_from_xml(xml_video_element)
|
||||||
params = {
|
|
||||||
'playlistid': playlist.playlistid
|
|
||||||
}
|
|
||||||
if item.kodi_id:
|
if item.kodi_id:
|
||||||
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
|
json_item = {'%sid' % item.kodi_type: item.kodi_id}
|
||||||
else:
|
else:
|
||||||
params['item'] = {'file': item.file}
|
json_item = {'file': item.file}
|
||||||
reply = JSONRPC('Playlist.Add').execute(params)
|
reply = js.playlist_add(playlist.playlistid, json_item)
|
||||||
if reply.get('error') is not None:
|
if reply.get('error') is not None:
|
||||||
log.error('Could not add item %s to Kodi playlist. Error: %s'
|
raise PlaylistError('Could not add item %s to Kodi playlist. Error: '
|
||||||
% (xml_video_element, reply))
|
'%s', xml_video_element, reply)
|
||||||
return None
|
return item
|
||||||
else:
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
|
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
|
||||||
|
@ -531,41 +693,38 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
|
||||||
|
|
||||||
file: string!
|
file: string!
|
||||||
"""
|
"""
|
||||||
log.debug('Insert listitem at position %s for Kodi only for %s'
|
LOG.debug('Insert listitem at position %s for Kodi only for %s',
|
||||||
% (pos, playlist))
|
pos, playlist)
|
||||||
# Add the item into Kodi playlist
|
# Add the item into Kodi playlist
|
||||||
playlist.kodi_pl.add(file, listitem, index=pos)
|
playlist.kodi_pl.add(url=file, listitem=listitem, index=pos)
|
||||||
# We need to add this to our internal queue as well
|
# We need to add this to our internal queue as well
|
||||||
if xml_video_element is not None:
|
if xml_video_element is not None:
|
||||||
item = playlist_item_from_xml(playlist, xml_video_element)
|
item = playlist_item_from_xml(xml_video_element)
|
||||||
else:
|
else:
|
||||||
item = playlist_item_from_kodi(kodi_item)
|
item = playlist_item_from_kodi(kodi_item)
|
||||||
if file is not None:
|
if file is not None:
|
||||||
item.file = file
|
item.file = file
|
||||||
playlist.items.insert(pos, item)
|
playlist.items.insert(pos, item)
|
||||||
log.debug('Done inserting for %s' % playlist)
|
LOG.debug('Done inserting for %s', playlist)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def remove_from_Kodi_playlist(playlist, pos):
|
def remove_from_kodi_playlist(playlist, pos):
|
||||||
"""
|
"""
|
||||||
Removes the item at position pos from the Kodi playlist using JSON.
|
Removes the item at position pos from the Kodi playlist using JSON.
|
||||||
|
|
||||||
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
|
||||||
"""
|
"""
|
||||||
log.debug('Removing position %s from Kodi only from %s' % (pos, playlist))
|
LOG.debug('Removing position %s from Kodi only from %s', pos, playlist)
|
||||||
reply = JSONRPC('Playlist.Remove').execute({
|
reply = js.playlist_remove(playlist.playlistid, pos)
|
||||||
'playlistid': playlist.playlistid,
|
|
||||||
'position': pos
|
|
||||||
})
|
|
||||||
if reply.get('error') is not None:
|
if reply.get('error') is not None:
|
||||||
log.error('Could not delete the item from the playlist. Error: %s'
|
LOG.error('Could not delete the item from the playlist. Error: %s',
|
||||||
% reply)
|
reply)
|
||||||
return
|
return
|
||||||
else:
|
try:
|
||||||
try:
|
del playlist.items[pos]
|
||||||
del playlist.items[pos]
|
except IndexError:
|
||||||
except IndexError:
|
LOG.error('Cannot delete position %s for %s', pos, playlist)
|
||||||
log.error('Cannot delete position %s for %s' % (pos, playlist))
|
|
||||||
|
|
||||||
|
|
||||||
def get_pms_playqueue(playqueue_id):
|
def get_pms_playqueue(playqueue_id):
|
||||||
|
@ -578,7 +737,7 @@ def get_pms_playqueue(playqueue_id):
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error('Could not download Plex playqueue %s' % playqueue_id)
|
LOG.error('Could not download Plex playqueue %s', playqueue_id)
|
||||||
xml = None
|
xml = None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
@ -593,12 +752,12 @@ def get_plextype_from_xml(xml):
|
||||||
try:
|
try:
|
||||||
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0]
|
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
log.error('Could not get plex_id from xml: %s' % xml.attrib)
|
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
|
||||||
return
|
return
|
||||||
new_xml = GetPlexMetadata(plex_id)
|
new_xml = GetPlexMetadata(plex_id)
|
||||||
try:
|
try:
|
||||||
new_xml[0].attrib
|
new_xml[0].attrib
|
||||||
except (TypeError, IndexError, AttributeError):
|
except (TypeError, IndexError, AttributeError):
|
||||||
log.error('Could not get plex metadata for plex id %s' % plex_id)
|
LOG.error('Could not get plex metadata for plex id %s', plex_id)
|
||||||
return
|
return
|
||||||
return new_xml[0].attrib.get('type')
|
return new_xml[0].attrib.get('type')
|
||||||
|
|
|
@ -1,229 +1,242 @@
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
###############################################################################
|
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
||||||
import logging
|
"""
|
||||||
from threading import RLock, Thread
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
from re import compile as re_compile
|
||||||
|
|
||||||
from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
|
from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep
|
||||||
|
|
||||||
from utils import window, thread_methods
|
from utils import thread_methods
|
||||||
import playlist_func as PL
|
import playlist_func as PL
|
||||||
from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren
|
from PlexFunctions import GetAllPlexChildren
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from playbackutils import PlaybackUtils
|
from plexbmchelper.subscribers import LOCK
|
||||||
|
from playback import play_xml
|
||||||
|
import json_rpc as js
|
||||||
import variables as v
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
# Lock used for playqueue manipulations
|
|
||||||
lock = RLock()
|
|
||||||
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
||||||
|
REGEX = re_compile(r'''plex_id=(\d+)''')
|
||||||
|
|
||||||
|
# Our PKC playqueues (3 instances of Playqueue_Object())
|
||||||
|
PLAYQUEUES = []
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
@thread_methods(add_suspends=['PMS_STATUS'])
|
def init_playqueues():
|
||||||
class Playqueue(Thread):
|
|
||||||
"""
|
"""
|
||||||
Monitors Kodi's playqueues for changes on the Kodi side
|
Call this once on startup to initialize the PKC playqueue objects in
|
||||||
|
the list PLAYQUEUES
|
||||||
"""
|
"""
|
||||||
# Borg - multiple instances, shared state
|
if PLAYQUEUES:
|
||||||
__shared_state = {}
|
LOG.debug('Playqueues have already been initialized')
|
||||||
playqueues = None
|
return
|
||||||
|
# Initialize Kodi playqueues
|
||||||
def __init__(self, callback=None):
|
with LOCK:
|
||||||
self.__dict__ = self.__shared_state
|
for i in (0, 1, 2):
|
||||||
if self.playqueues is not None:
|
# Just in case the Kodi response is not sorted correctly
|
||||||
log.debug('Playqueue thread has already been initialized')
|
for queue in js.get_playlists():
|
||||||
Thread.__init__(self)
|
if queue['playlistid'] != i:
|
||||||
return
|
continue
|
||||||
self.mgr = callback
|
|
||||||
|
|
||||||
# Initialize Kodi playqueues
|
|
||||||
with lock:
|
|
||||||
self.playqueues = []
|
|
||||||
for queue in PL.get_kodi_playqueues():
|
|
||||||
playqueue = PL.Playqueue_Object()
|
playqueue = PL.Playqueue_Object()
|
||||||
playqueue.playlistid = queue['playlistid']
|
playqueue.playlistid = i
|
||||||
playqueue.type = queue['type']
|
playqueue.type = queue['type']
|
||||||
# Initialize each Kodi playlist
|
# Initialize each Kodi playlist
|
||||||
if playqueue.type == 'audio':
|
if playqueue.type == v.KODI_TYPE_AUDIO:
|
||||||
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
|
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
|
||||||
elif playqueue.type == 'video':
|
elif playqueue.type == v.KODI_TYPE_VIDEO:
|
||||||
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
||||||
else:
|
else:
|
||||||
# Currently, only video or audio playqueues available
|
# Currently, only video or audio playqueues available
|
||||||
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
||||||
# Overwrite 'picture' with 'photo'
|
# Overwrite 'picture' with 'photo'
|
||||||
playqueue.type = v.KODI_TYPE_PHOTO
|
playqueue.type = v.KODI_TYPE_PHOTO
|
||||||
self.playqueues.append(playqueue)
|
PLAYQUEUES.append(playqueue)
|
||||||
# sort the list by their playlistid, just in case
|
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
|
||||||
self.playqueues = sorted(
|
|
||||||
self.playqueues, key=lambda i: i.playlistid)
|
|
||||||
log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def get_playqueue_from_type(self, typus):
|
|
||||||
"""
|
|
||||||
Returns the playqueue according to the typus ('video', 'audio',
|
|
||||||
'picture') passed in
|
|
||||||
"""
|
|
||||||
with lock:
|
|
||||||
for playqueue in self.playqueues:
|
|
||||||
if playqueue.type == typus:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError('Wrong playlist type passed in: %s' % typus)
|
|
||||||
return playqueue
|
|
||||||
|
|
||||||
def init_playqueue_from_plex_children(self, plex_id):
|
def get_playqueue_from_type(kodi_playlist_type):
|
||||||
"""
|
"""
|
||||||
Init a new playqueue e.g. from an album. Alexa does this
|
Returns the playqueue according to the kodi_playlist_type ('video',
|
||||||
|
'audio', 'picture') passed in
|
||||||
Returns the Playlist_Object
|
"""
|
||||||
"""
|
with LOCK:
|
||||||
xml = GetAllPlexChildren(plex_id)
|
for playqueue in PLAYQUEUES:
|
||||||
try:
|
if playqueue.type == kodi_playlist_type:
|
||||||
xml[0].attrib
|
break
|
||||||
except (TypeError, IndexError, AttributeError):
|
else:
|
||||||
log.error('Could not download the PMS xml for %s' % plex_id)
|
raise ValueError('Wrong playlist type passed in: %s',
|
||||||
return
|
kodi_playlist_type)
|
||||||
playqueue = self.get_playqueue_from_type(
|
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
|
||||||
playqueue.clear()
|
|
||||||
for i, child in enumerate(xml):
|
|
||||||
api = API(child)
|
|
||||||
PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey())
|
|
||||||
log.debug('Firing up Kodi player')
|
|
||||||
Player().play(playqueue.kodi_pl, None, False, 0)
|
|
||||||
return playqueue
|
return playqueue
|
||||||
|
|
||||||
def update_playqueue_from_PMS(self,
|
|
||||||
playqueue,
|
|
||||||
playqueue_id=None,
|
|
||||||
repeat=None,
|
|
||||||
offset=None):
|
|
||||||
"""
|
|
||||||
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
|
|
||||||
in playqueue_id if we need to fetch a new playqueue
|
|
||||||
|
|
||||||
repeat = 0, 1, 2
|
def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||||
offset = time offset in Plextime (milliseconds)
|
"""
|
||||||
"""
|
Init a new playqueue e.g. from an album. Alexa does this
|
||||||
log.info('New playqueue %s received from Plex companion with offset '
|
|
||||||
'%s, repeat %s' % (playqueue_id, offset, repeat))
|
|
||||||
with lock:
|
|
||||||
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
|
||||||
playqueue.clear()
|
|
||||||
try:
|
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
|
||||||
except KeyError:
|
|
||||||
log.error('Could not get playqueue ID %s' % playqueue_id)
|
|
||||||
return
|
|
||||||
PlaybackUtils(xml, playqueue).play_all()
|
|
||||||
playqueue.repeat = 0 if not repeat else int(repeat)
|
|
||||||
window('plex_customplaylist', value="true")
|
|
||||||
if offset not in (None, "0"):
|
|
||||||
window('plex_customplaylist.seektime',
|
|
||||||
str(ConvertPlexToKodiTime(offset)))
|
|
||||||
for startpos, item in enumerate(playqueue.items):
|
|
||||||
if item.ID == playqueue.selectedItemID:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
startpos = 0
|
|
||||||
# Start playback. Player does not return in time
|
|
||||||
log.debug('Playqueues after Plex Companion update are now: %s'
|
|
||||||
% self.playqueues)
|
|
||||||
thread = Thread(target=Player().play,
|
|
||||||
args=(playqueue.kodi_pl,
|
|
||||||
None,
|
|
||||||
False,
|
|
||||||
startpos))
|
|
||||||
thread.setDaemon(True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
|
Returns the Playlist_Object
|
||||||
|
"""
|
||||||
|
xml = GetAllPlexChildren(plex_id)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
LOG.error('Could not download the PMS xml for %s', plex_id)
|
||||||
|
return
|
||||||
|
playqueue = get_playqueue_from_type(
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
|
||||||
|
playqueue.clear()
|
||||||
|
for i, child in enumerate(xml):
|
||||||
|
api = API(child)
|
||||||
|
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
|
||||||
|
playqueue.plex_transient_token = transient_token
|
||||||
|
LOG.debug('Firing up Kodi player')
|
||||||
|
Player().play(playqueue.kodi_pl, None, False, 0)
|
||||||
|
return playqueue
|
||||||
|
|
||||||
|
|
||||||
|
def update_playqueue_from_PMS(playqueue,
|
||||||
|
playqueue_id=None,
|
||||||
|
repeat=None,
|
||||||
|
offset=None,
|
||||||
|
transient_token=None):
|
||||||
|
"""
|
||||||
|
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
|
||||||
|
in playqueue_id if we need to fetch a new playqueue
|
||||||
|
|
||||||
|
repeat = 0, 1, 2
|
||||||
|
offset = time offset in Plextime (milliseconds)
|
||||||
|
"""
|
||||||
|
LOG.info('New playqueue %s received from Plex companion with offset '
|
||||||
|
'%s, repeat %s', playqueue_id, offset, repeat)
|
||||||
|
# Safe transient token from being deleted
|
||||||
|
if transient_token is None:
|
||||||
|
transient_token = playqueue.plex_transient_token
|
||||||
|
with LOCK:
|
||||||
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
||||||
|
playqueue.clear()
|
||||||
|
try:
|
||||||
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
|
except PL.PlaylistError:
|
||||||
|
LOG.error('Could not get playqueue ID %s', playqueue_id)
|
||||||
|
return
|
||||||
|
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||||
|
playqueue.plex_transient_token = transient_token
|
||||||
|
play_xml(playqueue, xml, offset)
|
||||||
|
|
||||||
|
|
||||||
|
@thread_methods(add_suspends=['PMS_STATUS'])
|
||||||
|
class PlayqueueMonitor(Thread):
|
||||||
|
"""
|
||||||
|
Unfortunately, Kodi does not tell if items within a Kodi playqueue
|
||||||
|
(playlist) are swapped. This is what this monitor is for. Don't replace
|
||||||
|
this mechanism till Kodi's implementation of playlists has improved
|
||||||
|
"""
|
||||||
def _compare_playqueues(self, playqueue, new):
|
def _compare_playqueues(self, playqueue, new):
|
||||||
"""
|
"""
|
||||||
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
||||||
"""
|
"""
|
||||||
old = list(playqueue.items)
|
old = list(playqueue.items)
|
||||||
index = list(range(0, len(old)))
|
index = list(range(0, len(old)))
|
||||||
log.debug('Comparing new Kodi playqueue %s with our play queue %s'
|
LOG.debug('Comparing new Kodi playqueue %s with our play queue %s',
|
||||||
% (new, old))
|
new, old)
|
||||||
if self.thread_stopped():
|
|
||||||
# Chances are that we got an empty Kodi playlist due to
|
|
||||||
# Kodi exit
|
|
||||||
return
|
|
||||||
for i, new_item in enumerate(new):
|
for i, new_item in enumerate(new):
|
||||||
if (new_item['file'].startswith('plugin://') and
|
if (new_item['file'].startswith('plugin://') and
|
||||||
not new_item['file'].startswith(PLUGIN)):
|
not new_item['file'].startswith(PLUGIN)):
|
||||||
# Ignore new media added by other addons
|
# Ignore new media added by other addons
|
||||||
continue
|
continue
|
||||||
for j, old_item in enumerate(old):
|
for j, old_item in enumerate(old):
|
||||||
|
if self.stopped():
|
||||||
|
# Chances are that we got an empty Kodi playlist due to
|
||||||
|
# Kodi exit
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
if (old_item.file.startswith('plugin://') and
|
if (old_item.file.startswith('plugin://') and
|
||||||
not old_item['file'].startswith(PLUGIN)):
|
not old_item.file.startswith(PLUGIN)):
|
||||||
# Ignore media by other addons
|
# Ignore media by other addons
|
||||||
continue
|
continue
|
||||||
except (TypeError, AttributeError):
|
except AttributeError:
|
||||||
# were not passed a filename; ignore
|
# were not passed a filename; ignore
|
||||||
pass
|
pass
|
||||||
if new_item.get('id') is None:
|
if 'id' in new_item:
|
||||||
identical = old_item.file == new_item['file']
|
|
||||||
else:
|
|
||||||
identical = (old_item.kodi_id == new_item['id'] and
|
identical = (old_item.kodi_id == new_item['id'] and
|
||||||
old_item.kodi_type == new_item['type'])
|
old_item.kodi_type == new_item['type'])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
plex_id = REGEX.findall(new_item['file'])[0]
|
||||||
|
except IndexError:
|
||||||
|
LOG.debug('Comparing paths directly as a fallback')
|
||||||
|
identical = old_item.file == new_item['file']
|
||||||
|
else:
|
||||||
|
identical = plex_id == old_item.plex_id
|
||||||
if j == 0 and identical:
|
if j == 0 and identical:
|
||||||
del old[j], index[j]
|
del old[j], index[j]
|
||||||
break
|
break
|
||||||
elif identical:
|
elif identical:
|
||||||
log.debug('Detected playqueue item %s moved to position %s'
|
LOG.debug('Detected playqueue item %s moved to position %s',
|
||||||
% (i+j, i))
|
i + j, i)
|
||||||
PL.move_playlist_item(playqueue, i + j, i)
|
with LOCK:
|
||||||
|
PL.move_playlist_item(playqueue, i + j, i)
|
||||||
del old[j], index[j]
|
del old[j], index[j]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
log.debug('Detected new Kodi element at position %s: %s '
|
LOG.debug('Detected new Kodi element at position %s: %s ',
|
||||||
% (i, new_item))
|
i, new_item)
|
||||||
if playqueue.ID is None:
|
with LOCK:
|
||||||
PL.init_Plex_playlist(playqueue,
|
try:
|
||||||
kodi_item=new_item)
|
if playqueue.id is None:
|
||||||
else:
|
PL.init_Plex_playlist(playqueue, kodi_item=new_item)
|
||||||
PL.add_item_to_PMS_playlist(playqueue,
|
else:
|
||||||
i,
|
PL.add_item_to_PMS_playlist(playqueue,
|
||||||
kodi_item=new_item)
|
i,
|
||||||
for j in range(i, len(index)):
|
kodi_item=new_item)
|
||||||
index[j] += 1
|
except PL.PlaylistError:
|
||||||
|
# Could not add the element
|
||||||
|
pass
|
||||||
|
except IndexError:
|
||||||
|
# This is really a hack - happens when using Addon Paths
|
||||||
|
# and repeatedly starting the same element. Kodi will
|
||||||
|
# then not pass kodi id nor file path AND will also not
|
||||||
|
# start-up playback. Hence kodimonitor kicks off
|
||||||
|
# playback. Also see kodimonitor.py - _playlist_onadd()
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for j in range(i, len(index)):
|
||||||
|
index[j] += 1
|
||||||
for i in reversed(index):
|
for i in reversed(index):
|
||||||
log.debug('Detected deletion of playqueue element at pos %s' % i)
|
if self.stopped():
|
||||||
PL.delete_playlist_item_from_PMS(playqueue, i)
|
# Chances are that we got an empty Kodi playlist due to
|
||||||
log.debug('Done comparing playqueues')
|
# Kodi exit
|
||||||
|
return
|
||||||
|
LOG.debug('Detected deletion of playqueue element at pos %s', i)
|
||||||
|
with LOCK:
|
||||||
|
PL.delete_playlist_item_from_PMS(playqueue, i)
|
||||||
|
LOG.debug('Done comparing playqueues')
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
log.info("----===## Starting PlayQueue client ##===----")
|
LOG.info("----===## Starting PlayqueueMonitor ##===----")
|
||||||
# Initialize the playqueues, if Kodi already got items in them
|
while not stopped():
|
||||||
for playqueue in self.playqueues:
|
while suspended():
|
||||||
for i, item in enumerate(PL.get_kodi_playlist_items(playqueue)):
|
if stopped():
|
||||||
if i == 0:
|
|
||||||
PL.init_Plex_playlist(playqueue, kodi_item=item)
|
|
||||||
else:
|
|
||||||
PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item)
|
|
||||||
while not thread_stopped():
|
|
||||||
while thread_suspended():
|
|
||||||
if thread_stopped():
|
|
||||||
break
|
break
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
with lock:
|
for playqueue in PLAYQUEUES:
|
||||||
for playqueue in self.playqueues:
|
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
||||||
kodi_playqueue = PL.get_kodi_playlist_items(playqueue)
|
if playqueue.old_kodi_pl != kodi_pl:
|
||||||
if playqueue.old_kodi_pl != kodi_playqueue:
|
if playqueue.id is None and (not state.DIRECT_PATHS or
|
||||||
|
state.CONTEXT_MENU_PLAY):
|
||||||
|
# Only initialize if directly fired up using direct
|
||||||
|
# paths. Otherwise let default.py do its magic
|
||||||
|
LOG.debug('Not yet initiating playback')
|
||||||
|
else:
|
||||||
# compare old and new playqueue
|
# compare old and new playqueue
|
||||||
self._compare_playqueues(playqueue, kodi_playqueue)
|
self._compare_playqueues(playqueue, kodi_pl)
|
||||||
playqueue.old_kodi_pl = list(kodi_playqueue)
|
playqueue.old_kodi_pl = list(kodi_pl)
|
||||||
# Still sleep a bit so Kodi does not become
|
|
||||||
# unresponsive
|
|
||||||
sleep(10)
|
|
||||||
continue
|
|
||||||
sleep(200)
|
sleep(200)
|
||||||
log.info("----===## PlayQueue client stopped ##===----")
|
LOG.info("----===## PlayqueueMonitor stopped ##===----")
|
||||||
|
|
|
@ -1,69 +1,56 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
|
||||||
import logging
|
from utils import window, settings, language as lang, dialog, try_encode
|
||||||
from downloadutils import DownloadUtils
|
|
||||||
|
|
||||||
from utils import window, settings, tryEncode, language as lang, dialog
|
|
||||||
import variables as v
|
import variables as v
|
||||||
import PlexAPI
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class PlayUtils():
|
class PlayUtils():
|
||||||
|
|
||||||
def __init__(self, item):
|
def __init__(self, api, playqueue_item):
|
||||||
|
|
||||||
self.item = item
|
|
||||||
self.API = PlexAPI.API(item)
|
|
||||||
self.doUtils = DownloadUtils().downloadUrl
|
|
||||||
|
|
||||||
self.machineIdentifier = window('plex_machineIdentifier')
|
|
||||||
|
|
||||||
def getPlayUrl(self, partNumber=None):
|
|
||||||
"""
|
"""
|
||||||
Returns the playurl for the part with number partNumber
|
init with api (PlexAPI wrapper of the PMS xml element) and
|
||||||
|
playqueue_item (Playlist_Item())
|
||||||
|
"""
|
||||||
|
self.api = api
|
||||||
|
self.item = playqueue_item
|
||||||
|
|
||||||
|
def getPlayUrl(self):
|
||||||
|
"""
|
||||||
|
Returns the playurl for the part
|
||||||
(movie might consist of several files)
|
(movie might consist of several files)
|
||||||
|
|
||||||
playurl is utf-8 encoded!
|
playurl is in unicode!
|
||||||
"""
|
"""
|
||||||
self.API.setPartNumber(partNumber)
|
self.api.mediastream_number()
|
||||||
self.API.getMediastreamNumber()
|
|
||||||
playurl = self.isDirectPlay()
|
playurl = self.isDirectPlay()
|
||||||
|
|
||||||
if playurl is not None:
|
if playurl is not None:
|
||||||
log.info("File is direct playing.")
|
LOG.info("File is direct playing.")
|
||||||
playurl = tryEncode(playurl)
|
self.item.playmethod = 'DirectPlay'
|
||||||
# Set playmethod property
|
|
||||||
window('plex_%s.playmethod' % playurl, "DirectPlay")
|
|
||||||
|
|
||||||
elif self.isDirectStream():
|
elif self.isDirectStream():
|
||||||
log.info("File is direct streaming.")
|
LOG.info("File is direct streaming.")
|
||||||
playurl = tryEncode(
|
playurl = self.api.transcode_video_path('DirectStream')
|
||||||
self.API.getTranscodeVideoPath('DirectStream'))
|
self.item.playmethod = 'DirectStream'
|
||||||
# Set playmethod property
|
|
||||||
window('plex_%s.playmethod' % playurl, "DirectStream")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.info("File is transcoding.")
|
LOG.info("File is transcoding.")
|
||||||
playurl = tryEncode(self.API.getTranscodeVideoPath(
|
playurl = self.api.transcode_video_path(
|
||||||
'Transcode',
|
'Transcode',
|
||||||
quality={
|
quality={
|
||||||
'maxVideoBitrate': self.get_bitrate(),
|
'maxVideoBitrate': self.get_bitrate(),
|
||||||
'videoResolution': self.get_resolution(),
|
'videoResolution': self.get_resolution(),
|
||||||
'videoQuality': '100',
|
'videoQuality': '100',
|
||||||
'mediaBufferSize': int(settings('kodi_video_cache'))/1024,
|
'mediaBufferSize': int(settings('kodi_video_cache'))/1024,
|
||||||
}))
|
})
|
||||||
# Set playmethod property
|
self.item.playmethod = 'Transcode'
|
||||||
window('plex_%s.playmethod' % playurl, value="Transcode")
|
LOG.info("The playurl is: %s", playurl)
|
||||||
|
self.item.file = playurl
|
||||||
log.info("The playurl is: %s" % playurl)
|
|
||||||
return playurl
|
return playurl
|
||||||
|
|
||||||
def isDirectPlay(self):
|
def isDirectPlay(self):
|
||||||
|
@ -71,45 +58,28 @@ class PlayUtils():
|
||||||
Returns the path/playurl if we can direct play, None otherwise
|
Returns the path/playurl if we can direct play, None otherwise
|
||||||
"""
|
"""
|
||||||
# True for e.g. plex.tv watch later
|
# True for e.g. plex.tv watch later
|
||||||
if self.API.shouldStream() is True:
|
if self.api.should_stream() is True:
|
||||||
log.info("Plex item optimized for direct streaming")
|
LOG.info("Plex item optimized for direct streaming")
|
||||||
return
|
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'
|
# set to either 'Direct Stream=1' or 'Transcode=2'
|
||||||
# and NOT to 'Direct Play=0'
|
# and NOT to 'Direct Play=0'
|
||||||
if settings('playType') != "0":
|
if settings('playType') != "0":
|
||||||
# User forcing to play via HTTP
|
# User forcing to play via HTTP
|
||||||
log.info("User chose to not direct play")
|
LOG.info("User chose to not direct play")
|
||||||
return
|
return
|
||||||
if self.mustTranscode():
|
if self.mustTranscode():
|
||||||
return
|
return
|
||||||
return self.API.validatePlayurl(self.API.getFilePath(),
|
return self.api.validate_playurl(path,
|
||||||
self.API.getType(),
|
self.api.plex_type(),
|
||||||
forceCheck=True)
|
force_check=True)
|
||||||
|
|
||||||
def directPlay(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
playurl = self.item['MediaSources'][0]['Path']
|
|
||||||
except (IndexError, KeyError):
|
|
||||||
playurl = self.item['Path']
|
|
||||||
|
|
||||||
if self.item.get('VideoType'):
|
|
||||||
# Specific format modification
|
|
||||||
if self.item['VideoType'] == "Dvd":
|
|
||||||
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
|
|
||||||
elif self.item['VideoType'] == "BluRay":
|
|
||||||
playurl = "%s/BDMV/index.bdmv" % playurl
|
|
||||||
|
|
||||||
# Assign network protocol
|
|
||||||
if playurl.startswith('\\\\'):
|
|
||||||
playurl = playurl.replace("\\\\", "smb://")
|
|
||||||
playurl = playurl.replace("\\", "/")
|
|
||||||
|
|
||||||
if "apple.com" in playurl:
|
|
||||||
USER_AGENT = "QuickTime/7.7.4"
|
|
||||||
playurl += "?|User-Agent=%s" % USER_AGENT
|
|
||||||
|
|
||||||
return playurl
|
|
||||||
|
|
||||||
def mustTranscode(self):
|
def mustTranscode(self):
|
||||||
"""
|
"""
|
||||||
|
@ -117,46 +87,48 @@ class PlayUtils():
|
||||||
- codec is in h265
|
- codec is in h265
|
||||||
- 10bit video codec
|
- 10bit video codec
|
||||||
- HEVC codec
|
- HEVC codec
|
||||||
- window variable 'plex_forcetranscode' set to 'true'
|
- playqueue_item force_transcode is set to True
|
||||||
|
- state variable FORCE_TRANSCODE set to True
|
||||||
(excepting trailers etc.)
|
(excepting trailers etc.)
|
||||||
- video bitrate above specified settings bitrate
|
- video bitrate above specified settings bitrate
|
||||||
if the corresponding file settings are set to 'true'
|
if the corresponding file settings are set to 'true'
|
||||||
"""
|
"""
|
||||||
if self.API.getType() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
if self.api.plex_type() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
||||||
log.info('Plex clip or music track, not transcoding')
|
LOG.info('Plex clip or music track, not transcoding')
|
||||||
return False
|
return False
|
||||||
videoCodec = self.API.getVideoCodec()
|
videoCodec = self.api.video_codec()
|
||||||
log.info("videoCodec: %s" % videoCodec)
|
LOG.info("videoCodec: %s" % videoCodec)
|
||||||
if window('plex_forcetranscode') == 'true':
|
if self.item.force_transcode is True:
|
||||||
log.info('User chose to force-transcode')
|
LOG.info('User chose to force-transcode')
|
||||||
return True
|
|
||||||
if (settings('transcodeHi10P') == 'true' and
|
|
||||||
videoCodec['bitDepth'] == '10'):
|
|
||||||
log.info('Option to transcode 10bit video content enabled.')
|
|
||||||
return True
|
return True
|
||||||
codec = videoCodec['videocodec']
|
codec = videoCodec['videocodec']
|
||||||
if codec is None:
|
if codec is None:
|
||||||
# e.g. trailers. Avoids TypeError with "'h265' in codec"
|
# e.g. trailers. Avoids TypeError with "'h265' in codec"
|
||||||
log.info('No codec from PMS, not transcoding.')
|
LOG.info('No codec from PMS, not transcoding.')
|
||||||
return False
|
return False
|
||||||
|
if ((settings('transcodeHi10P') == 'true' and
|
||||||
|
videoCodec['bitDepth'] == '10') and
|
||||||
|
('h264' in codec)):
|
||||||
|
LOG.info('Option to transcode 10bit h264 video content enabled.')
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
bitrate = int(videoCodec['bitrate'])
|
bitrate = int(videoCodec['bitrate'])
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
log.info('No video bitrate from PMS, not transcoding.')
|
LOG.info('No video bitrate from PMS, not transcoding.')
|
||||||
return False
|
return False
|
||||||
if bitrate > self.get_max_bitrate():
|
if bitrate > self.get_max_bitrate():
|
||||||
log.info('Video bitrate of %s is higher than the maximal video'
|
LOG.info('Video bitrate of %s is higher than the maximal video'
|
||||||
'bitrate of %s that the user chose. Transcoding'
|
'bitrate of %s that the user chose. Transcoding'
|
||||||
% (bitrate, self.get_max_bitrate()))
|
% (bitrate, self.get_max_bitrate()))
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
resolution = int(videoCodec['resolution'])
|
resolution = int(videoCodec['resolution'])
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
log.info('No video resolution from PMS, not transcoding.')
|
LOG.info('No video resolution from PMS, not transcoding.')
|
||||||
return False
|
return False
|
||||||
if 'h265' in codec or 'hevc' in codec:
|
if 'h265' in codec or 'hevc' in codec:
|
||||||
if resolution >= self.getH265():
|
if resolution >= self.getH265():
|
||||||
log.info("Option to transcode h265/HEVC enabled. Resolution "
|
LOG.info("Option to transcode h265/HEVC enabled. Resolution "
|
||||||
"of the media: %s, transcoding limit resolution: %s"
|
"of the media: %s, transcoding limit resolution: %s"
|
||||||
% (str(resolution), str(self.getH265())))
|
% (str(resolution), str(self.getH265())))
|
||||||
return True
|
return True
|
||||||
|
@ -164,12 +136,12 @@ class PlayUtils():
|
||||||
|
|
||||||
def isDirectStream(self):
|
def isDirectStream(self):
|
||||||
# Never transcode Music
|
# Never transcode Music
|
||||||
if self.API.getType() == 'track':
|
if self.api.plex_type() == 'track':
|
||||||
return True
|
return True
|
||||||
# set to 'Transcode=2'
|
# set to 'Transcode=2'
|
||||||
if settings('playType') == "2":
|
if settings('playType') == "2":
|
||||||
# User forcing to play via HTTP
|
# User forcing to play via HTTP
|
||||||
log.info("User chose to transcode")
|
LOG.info("User chose to transcode")
|
||||||
return False
|
return False
|
||||||
if self.mustTranscode():
|
if self.mustTranscode():
|
||||||
return False
|
return False
|
||||||
|
@ -251,7 +223,7 @@ class PlayUtils():
|
||||||
}
|
}
|
||||||
return res[chosen]
|
return res[chosen]
|
||||||
|
|
||||||
def audioSubsPref(self, listitem, url, part=None):
|
def audio_subtitle_prefs(self, listitem):
|
||||||
"""
|
"""
|
||||||
For transcoding only
|
For transcoding only
|
||||||
|
|
||||||
|
@ -259,15 +231,13 @@ class PlayUtils():
|
||||||
stream by a PUT request to the PMS
|
stream by a PUT request to the PMS
|
||||||
"""
|
"""
|
||||||
# Set media and part where we're at
|
# Set media and part where we're at
|
||||||
if self.API.mediastream is None:
|
if self.api.mediastream is None:
|
||||||
self.API.getMediastreamNumber()
|
self.api.mediastream_number()
|
||||||
if part is None:
|
|
||||||
part = 0
|
|
||||||
try:
|
try:
|
||||||
mediastreams = self.item[self.API.mediastream][part]
|
mediastreams = self.api.plex_media_streams()
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
log.error('Could not get media %s, part %s'
|
LOG.error('Could not get media %s, part %s',
|
||||||
% (self.API.mediastream, part))
|
self.api.mediastream, self.api.part)
|
||||||
return
|
return
|
||||||
part_id = mediastreams.attrib['id']
|
part_id = mediastreams.attrib['id']
|
||||||
audio_streams_list = []
|
audio_streams_list = []
|
||||||
|
@ -292,19 +262,19 @@ class PlayUtils():
|
||||||
# Audio
|
# Audio
|
||||||
if typus == "2":
|
if typus == "2":
|
||||||
codec = stream.attrib.get('codec')
|
codec = stream.attrib.get('codec')
|
||||||
channelLayout = stream.attrib.get('audioChannelLayout', "")
|
channellayout = stream.attrib.get('audioChannelLayout', "")
|
||||||
try:
|
try:
|
||||||
track = "%s %s - %s %s" % (audio_numb+1,
|
track = "%s %s - %s %s" % (audio_numb+1,
|
||||||
stream.attrib['language'],
|
stream.attrib['language'],
|
||||||
codec,
|
codec,
|
||||||
channelLayout)
|
channellayout)
|
||||||
except:
|
except KeyError:
|
||||||
track = "%s %s - %s %s" % (audio_numb+1,
|
track = "%s %s - %s %s" % (audio_numb+1,
|
||||||
lang(39707), # unknown
|
lang(39707), # unknown
|
||||||
codec,
|
codec,
|
||||||
channelLayout)
|
channellayout)
|
||||||
audio_streams_list.append(index)
|
audio_streams_list.append(index)
|
||||||
audio_streams.append(tryEncode(track))
|
audio_streams.append(try_encode(track))
|
||||||
audio_numb += 1
|
audio_numb += 1
|
||||||
|
|
||||||
# Subtitles
|
# Subtitles
|
||||||
|
@ -326,17 +296,17 @@ class PlayUtils():
|
||||||
if downloadable:
|
if downloadable:
|
||||||
# We do know the language - temporarily download
|
# We do know the language - temporarily download
|
||||||
if 'language' in stream.attrib:
|
if 'language' in stream.attrib:
|
||||||
path = self.API.download_external_subtitles(
|
path = self.api.download_external_subtitles(
|
||||||
'{server}%s' % stream.attrib['key'],
|
'{server}%s' % stream.attrib['key'],
|
||||||
"subtitle.%s.%s" % (stream.attrib['language'],
|
"subtitle.%s.%s" % (stream.attrib['languageCode'],
|
||||||
stream.attrib['codec']))
|
stream.attrib['codec']))
|
||||||
# We don't know the language - no need to download
|
# We don't know the language - no need to download
|
||||||
else:
|
else:
|
||||||
path = self.API.addPlexCredentialsToUrl(
|
path = self.api.attach_plex_token_to_url(
|
||||||
"%s%s" % (window('pms_server'),
|
"%s%s" % (window('pms_server'),
|
||||||
stream.attrib['key']))
|
stream.attrib['key']))
|
||||||
downloadable_streams.append(index)
|
downloadable_streams.append(index)
|
||||||
download_subs.append(tryEncode(path))
|
download_subs.append(try_encode(path))
|
||||||
else:
|
else:
|
||||||
track = "%s (%s)" % (track, lang(39710)) # burn-in
|
track = "%s (%s)" % (track, lang(39710)) # burn-in
|
||||||
if stream.attrib.get('selected') == '1' and downloadable:
|
if stream.attrib.get('selected') == '1' and downloadable:
|
||||||
|
@ -345,7 +315,7 @@ class PlayUtils():
|
||||||
default_sub = index
|
default_sub = index
|
||||||
|
|
||||||
subtitle_streams_list.append(index)
|
subtitle_streams_list.append(index)
|
||||||
subtitle_streams.append(tryEncode(track))
|
subtitle_streams.append(try_encode(track))
|
||||||
sub_num += 1
|
sub_num += 1
|
||||||
|
|
||||||
if audio_numb > 1:
|
if audio_numb > 1:
|
||||||
|
@ -356,9 +326,9 @@ class PlayUtils():
|
||||||
'audioStreamID': audio_streams_list[resp],
|
'audioStreamID': audio_streams_list[resp],
|
||||||
'allParts': 1
|
'allParts': 1
|
||||||
}
|
}
|
||||||
self.doUtils('{server}/library/parts/%s' % part_id,
|
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||||
action_type='PUT',
|
action_type='PUT',
|
||||||
parameters=args)
|
parameters=args)
|
||||||
|
|
||||||
if sub_num == 1:
|
if sub_num == 1:
|
||||||
# No subtitles
|
# No subtitles
|
||||||
|
@ -367,7 +337,7 @@ class PlayUtils():
|
||||||
select_subs_index = None
|
select_subs_index = None
|
||||||
if (settings('pickPlexSubtitles') == 'true' and
|
if (settings('pickPlexSubtitles') == 'true' and
|
||||||
default_sub is not None):
|
default_sub is not None):
|
||||||
log.info('Using default Plex subtitle: %s' % default_sub)
|
LOG.info('Using default Plex subtitle: %s', default_sub)
|
||||||
select_subs_index = default_sub
|
select_subs_index = default_sub
|
||||||
else:
|
else:
|
||||||
resp = dialog('select', lang(33014), subtitle_streams)
|
resp = dialog('select', lang(33014), subtitle_streams)
|
||||||
|
@ -377,26 +347,18 @@ class PlayUtils():
|
||||||
# User selected no subtitles or backed out of dialog
|
# User selected no subtitles or backed out of dialog
|
||||||
select_subs_index = ''
|
select_subs_index = ''
|
||||||
|
|
||||||
log.debug('Adding external subtitles: %s' % download_subs)
|
LOG.debug('Adding external subtitles: %s', download_subs)
|
||||||
# Enable Kodi to switch autonomously to downloadable subtitles
|
# Enable Kodi to switch autonomously to downloadable subtitles
|
||||||
if download_subs:
|
if download_subs:
|
||||||
listitem.setSubtitles(download_subs)
|
listitem.setSubtitles(download_subs)
|
||||||
|
# Don't additionally burn in subtitles
|
||||||
if select_subs_index in downloadable_streams:
|
if select_subs_index in downloadable_streams:
|
||||||
for i, stream in enumerate(downloadable_streams):
|
|
||||||
if stream == select_subs_index:
|
|
||||||
# Set the correct subtitle
|
|
||||||
window('plex_%s.subtitle' % tryEncode(url), value=str(i))
|
|
||||||
break
|
|
||||||
# Don't additionally burn in subtitles
|
|
||||||
select_subs_index = ''
|
select_subs_index = ''
|
||||||
else:
|
# Now prep the PMS for our choice
|
||||||
window('plex_%s.subtitle' % tryEncode(url), value='None')
|
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
'subtitleStreamID': select_subs_index,
|
'subtitleStreamID': select_subs_index,
|
||||||
'allParts': 1
|
'allParts': 1
|
||||||
}
|
}
|
||||||
self.doUtils('{server}/library/parts/%s' % part_id,
|
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||||
action_type='PUT',
|
action_type='PUT',
|
||||||
parameters=args)
|
parameters=args)
|
||||||
|
|
338
resources/lib/plex_tv.py
Normal file
338
resources/lib/plex_tv.py
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from xbmc import sleep, executebuiltin
|
||||||
|
|
||||||
|
from downloadutils import DownloadUtils as DU
|
||||||
|
from utils import dialog, language as lang, settings, try_encode
|
||||||
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def choose_home_user(token):
|
||||||
|
"""
|
||||||
|
Let's user choose from a list of Plex home users. Will switch to that
|
||||||
|
user accordingly.
|
||||||
|
|
||||||
|
Returns a dict:
|
||||||
|
{
|
||||||
|
'username': Unicode
|
||||||
|
'userid': '' Plex ID of the user
|
||||||
|
'token': '' User's token
|
||||||
|
'protected': True if PIN is needed, else False
|
||||||
|
}
|
||||||
|
|
||||||
|
Will return False if something went wrong (wrong PIN, no connection)
|
||||||
|
"""
|
||||||
|
# Get list of Plex home users
|
||||||
|
users = list_home_users(token)
|
||||||
|
if not users:
|
||||||
|
LOG.error("User download failed.")
|
||||||
|
return False
|
||||||
|
userlist = []
|
||||||
|
userlist_coded = []
|
||||||
|
for user in users:
|
||||||
|
username = user['title']
|
||||||
|
userlist.append(username)
|
||||||
|
# To take care of non-ASCII usernames
|
||||||
|
userlist_coded.append(try_encode(username))
|
||||||
|
usernumber = len(userlist)
|
||||||
|
username = ''
|
||||||
|
usertoken = ''
|
||||||
|
trials = 0
|
||||||
|
while trials < 3:
|
||||||
|
if usernumber > 1:
|
||||||
|
# Select user
|
||||||
|
user_select = dialog('select', lang(29999) + lang(39306),
|
||||||
|
userlist_coded)
|
||||||
|
if user_select == -1:
|
||||||
|
LOG.info("No user selected.")
|
||||||
|
settings('username', value='')
|
||||||
|
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
||||||
|
return False
|
||||||
|
# Only 1 user received, choose that one
|
||||||
|
else:
|
||||||
|
user_select = 0
|
||||||
|
selected_user = userlist[user_select]
|
||||||
|
LOG.info("Selected user: %s", selected_user)
|
||||||
|
user = users[user_select]
|
||||||
|
# Ask for PIN, if protected:
|
||||||
|
pin = None
|
||||||
|
if user['protected'] == '1':
|
||||||
|
LOG.debug('Asking for users PIN')
|
||||||
|
pin = dialog('input',
|
||||||
|
lang(39307) + selected_user,
|
||||||
|
'',
|
||||||
|
type='{numeric}',
|
||||||
|
option='{hide}')
|
||||||
|
# User chose to cancel
|
||||||
|
# Plex bug: don't call url for protected user with empty PIN
|
||||||
|
if not pin:
|
||||||
|
trials += 1
|
||||||
|
continue
|
||||||
|
# Switch to this Plex Home user, if applicable
|
||||||
|
result = switch_home_user(user['id'],
|
||||||
|
pin,
|
||||||
|
token,
|
||||||
|
settings('plex_machineIdentifier'))
|
||||||
|
if result:
|
||||||
|
# Successfully retrieved username: break out of while loop
|
||||||
|
username = result['username']
|
||||||
|
usertoken = result['usertoken']
|
||||||
|
break
|
||||||
|
# Couldn't get user auth
|
||||||
|
else:
|
||||||
|
trials += 1
|
||||||
|
# Could not login user, please try again
|
||||||
|
if not dialog('yesno',
|
||||||
|
heading='{plex}',
|
||||||
|
line1=lang(39308) + selected_user,
|
||||||
|
line2=lang(39309)):
|
||||||
|
# User chose to cancel
|
||||||
|
break
|
||||||
|
if not username:
|
||||||
|
LOG.error('Failed signing in a user to plex.tv')
|
||||||
|
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
||||||
|
return False
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'userid': user['id'],
|
||||||
|
'protected': True if user['protected'] == '1' else False,
|
||||||
|
'token': usertoken
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def switch_home_user(userid, pin, token, machineIdentifier):
|
||||||
|
"""
|
||||||
|
Retrieves Plex home token for a Plex home user.
|
||||||
|
Returns False if unsuccessful
|
||||||
|
|
||||||
|
Input:
|
||||||
|
userid id of the Plex home user
|
||||||
|
pin PIN of the Plex home user, if protected
|
||||||
|
token token for plex.tv
|
||||||
|
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
'username'
|
||||||
|
'usertoken' Might be empty strings if no token found
|
||||||
|
for the machineIdentifier that was chosen
|
||||||
|
}
|
||||||
|
|
||||||
|
settings('userid') and settings('username') with new plex token
|
||||||
|
"""
|
||||||
|
LOG.info('Switching to user %s', userid)
|
||||||
|
url = 'https://plex.tv/api/home/users/' + userid + '/switch'
|
||||||
|
if pin:
|
||||||
|
url += '?pin=' + pin
|
||||||
|
answer = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
action_type="POST",
|
||||||
|
headerOptions={'X-Plex-Token': token})
|
||||||
|
try:
|
||||||
|
answer.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Error: plex.tv switch HomeUser change failed')
|
||||||
|
return False
|
||||||
|
|
||||||
|
username = answer.attrib.get('title', '')
|
||||||
|
token = answer.attrib.get('authenticationToken', '')
|
||||||
|
|
||||||
|
# Write to settings file
|
||||||
|
settings('username', username)
|
||||||
|
settings('accessToken', token)
|
||||||
|
settings('userid', answer.attrib.get('id', ''))
|
||||||
|
settings('plex_restricteduser',
|
||||||
|
'true' if answer.attrib.get('restricted', '0') == '1'
|
||||||
|
else 'false')
|
||||||
|
state.RESTRICTED_USER = True if \
|
||||||
|
answer.attrib.get('restricted', '0') == '1' else False
|
||||||
|
|
||||||
|
# Get final token to the PMS we've chosen
|
||||||
|
url = 'https://plex.tv/api/resources?includeHttps=1'
|
||||||
|
xml = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
headerOptions={'X-Plex-Token': token})
|
||||||
|
try:
|
||||||
|
xml.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Answer from plex.tv not as excepted')
|
||||||
|
# Set to empty iterable list for loop
|
||||||
|
xml = []
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
LOG.debug('Our machineIdentifier is %s', machineIdentifier)
|
||||||
|
for device in xml:
|
||||||
|
identifier = device.attrib.get('clientIdentifier')
|
||||||
|
LOG.debug('Found a Plex machineIdentifier: %s', identifier)
|
||||||
|
if identifier == machineIdentifier:
|
||||||
|
found += 1
|
||||||
|
token = device.attrib.get('accessToken')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'username': username,
|
||||||
|
}
|
||||||
|
if found == 0:
|
||||||
|
LOG.info('No tokens found for your server! Using empty string')
|
||||||
|
result['usertoken'] = ''
|
||||||
|
else:
|
||||||
|
result['usertoken'] = token
|
||||||
|
LOG.info('Plex.tv switch HomeUser change successfull for user %s',
|
||||||
|
username)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_home_users(token):
|
||||||
|
"""
|
||||||
|
Returns a list for myPlex home users for the current plex.tv account.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
token for plex.tv
|
||||||
|
Output:
|
||||||
|
List of users, where one entry is of the form:
|
||||||
|
"id": userId,
|
||||||
|
"admin": '1'/'0',
|
||||||
|
"guest": '1'/'0',
|
||||||
|
"restricted": '1'/'0',
|
||||||
|
"protected": '1'/'0',
|
||||||
|
"email": email,
|
||||||
|
"title": title,
|
||||||
|
"username": username,
|
||||||
|
"thumb": thumb_url
|
||||||
|
}
|
||||||
|
If any value is missing, None is returned instead (or "" from plex.tv)
|
||||||
|
If an error is encountered, False is returned
|
||||||
|
"""
|
||||||
|
xml = DU().downloadUrl('https://plex.tv/api/home/users/',
|
||||||
|
authenticate=False,
|
||||||
|
headerOptions={'X-Plex-Token': token})
|
||||||
|
try:
|
||||||
|
xml.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Download of Plex home users failed.')
|
||||||
|
return False
|
||||||
|
users = []
|
||||||
|
for user in xml:
|
||||||
|
users.append(user.attrib)
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def sign_in_with_pin():
|
||||||
|
"""
|
||||||
|
Prompts user to sign in by visiting https://plex.tv/pin
|
||||||
|
|
||||||
|
Writes to Kodi settings file. Also returns:
|
||||||
|
{
|
||||||
|
'plexhome': 'true' if Plex Home, 'false' otherwise
|
||||||
|
'username':
|
||||||
|
'avatar': URL to user avator
|
||||||
|
'token':
|
||||||
|
'plexid': Plex user ID
|
||||||
|
'homesize': Number of Plex home users (defaults to '1')
|
||||||
|
}
|
||||||
|
Returns False if authentication did not work.
|
||||||
|
"""
|
||||||
|
code, identifier = get_pin()
|
||||||
|
if not code:
|
||||||
|
# Problems trying to contact plex.tv. Try again later
|
||||||
|
dialog('ok', heading='{plex}', line1=lang(39303))
|
||||||
|
return False
|
||||||
|
# Go to https://plex.tv/pin and enter the code:
|
||||||
|
# Or press No to cancel the sign in.
|
||||||
|
answer = dialog('yesno',
|
||||||
|
heading='{plex}',
|
||||||
|
line1=lang(39304) + "\n\n",
|
||||||
|
line2=code + "\n\n",
|
||||||
|
line3=lang(39311))
|
||||||
|
if not answer:
|
||||||
|
return False
|
||||||
|
count = 0
|
||||||
|
# Wait for approx 30 seconds (since the PIN is not visible anymore :-))
|
||||||
|
while count < 30:
|
||||||
|
xml = check_pin(identifier)
|
||||||
|
if xml is not False:
|
||||||
|
break
|
||||||
|
# Wait for 1 seconds
|
||||||
|
sleep(1000)
|
||||||
|
count += 1
|
||||||
|
if xml is False:
|
||||||
|
# Could not sign in to plex.tv Try again later
|
||||||
|
dialog('ok', heading='{plex}', line1=lang(39305))
|
||||||
|
return False
|
||||||
|
# Parse xml
|
||||||
|
userid = xml.attrib.get('id')
|
||||||
|
home = xml.get('home', '0')
|
||||||
|
if home == '1':
|
||||||
|
home = 'true'
|
||||||
|
else:
|
||||||
|
home = 'false'
|
||||||
|
username = xml.get('username', '')
|
||||||
|
avatar = xml.get('thumb', '')
|
||||||
|
token = xml.findtext('authentication-token')
|
||||||
|
home_size = xml.get('homeSize', '1')
|
||||||
|
result = {
|
||||||
|
'plexhome': home,
|
||||||
|
'username': username,
|
||||||
|
'avatar': avatar,
|
||||||
|
'token': token,
|
||||||
|
'plexid': userid,
|
||||||
|
'homesize': home_size
|
||||||
|
}
|
||||||
|
settings('plexLogin', username)
|
||||||
|
settings('plexToken', token)
|
||||||
|
settings('plexhome', home)
|
||||||
|
settings('plexid', userid)
|
||||||
|
settings('plexAvatar', avatar)
|
||||||
|
settings('plexHomeSize', home_size)
|
||||||
|
# Let Kodi log into plex.tv on startup from now on
|
||||||
|
settings('myplexlogin', 'true')
|
||||||
|
settings('plex_status', value=lang(39227))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_pin():
|
||||||
|
"""
|
||||||
|
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
|
||||||
|
"""
|
||||||
|
code = None
|
||||||
|
identifier = None
|
||||||
|
# Download
|
||||||
|
xml = DU().downloadUrl('https://plex.tv/pins.xml',
|
||||||
|
authenticate=False,
|
||||||
|
action_type="POST")
|
||||||
|
try:
|
||||||
|
xml.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error("Error, no PIN from plex.tv provided")
|
||||||
|
return None, None
|
||||||
|
code = xml.find('code').text
|
||||||
|
identifier = xml.find('id').text
|
||||||
|
LOG.info('Successfully retrieved code and id from plex.tv')
|
||||||
|
return code, identifier
|
||||||
|
|
||||||
|
|
||||||
|
def check_pin(identifier):
|
||||||
|
"""
|
||||||
|
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
|
||||||
|
|
||||||
|
Returns False if not yet done so, or the XML response file as etree
|
||||||
|
"""
|
||||||
|
# Try to get a temporary token
|
||||||
|
xml = DU().downloadUrl('https://plex.tv/pins/%s.xml' % identifier,
|
||||||
|
authenticate=False)
|
||||||
|
try:
|
||||||
|
temp_token = xml.find('auth_token').text
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error("Could not find token in plex.tv answer")
|
||||||
|
return False
|
||||||
|
if not temp_token:
|
||||||
|
return False
|
||||||
|
# Use temp token to get the final plex credentials
|
||||||
|
xml = DU().downloadUrl('https://plex.tv/users/account',
|
||||||
|
authenticate=False,
|
||||||
|
parameters={'X-Plex-Token': temp_token})
|
||||||
|
return xml
|
|
@ -1,244 +0,0 @@
|
||||||
import logging
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import string
|
|
||||||
|
|
||||||
import xbmc
|
|
||||||
|
|
||||||
import plexdb_functions as plexdb
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def xbmc_photo():
|
|
||||||
return "photo"
|
|
||||||
|
|
||||||
|
|
||||||
def xbmc_video():
|
|
||||||
return "video"
|
|
||||||
|
|
||||||
|
|
||||||
def xbmc_audio():
|
|
||||||
return "audio"
|
|
||||||
|
|
||||||
|
|
||||||
def plex_photo():
|
|
||||||
return "photo"
|
|
||||||
|
|
||||||
|
|
||||||
def plex_video():
|
|
||||||
return "video"
|
|
||||||
|
|
||||||
|
|
||||||
def plex_audio():
|
|
||||||
return "music"
|
|
||||||
|
|
||||||
|
|
||||||
def xbmc_type(plex_type):
|
|
||||||
if plex_type == plex_photo():
|
|
||||||
return xbmc_photo()
|
|
||||||
elif plex_type == plex_video():
|
|
||||||
return xbmc_video()
|
|
||||||
elif plex_type == plex_audio():
|
|
||||||
return xbmc_audio()
|
|
||||||
|
|
||||||
|
|
||||||
def plex_type(xbmc_type):
|
|
||||||
if xbmc_type == xbmc_photo():
|
|
||||||
return plex_photo()
|
|
||||||
elif xbmc_type == xbmc_video():
|
|
||||||
return plex_video()
|
|
||||||
elif xbmc_type == xbmc_audio():
|
|
||||||
return plex_audio()
|
|
||||||
|
|
||||||
|
|
||||||
def getXMLHeader():
|
|
||||||
return '<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
||||||
|
|
||||||
|
|
||||||
def getOKMsg():
|
|
||||||
return getXMLHeader() + '<Response code="200" status="OK" />'
|
|
||||||
|
|
||||||
|
|
||||||
def timeToMillis(time):
|
|
||||||
return (time['hours']*3600 +
|
|
||||||
time['minutes']*60 +
|
|
||||||
time['seconds'])*1000 + time['milliseconds']
|
|
||||||
|
|
||||||
|
|
||||||
def millisToTime(t):
|
|
||||||
millis = int(t)
|
|
||||||
seconds = millis / 1000
|
|
||||||
minutes = seconds / 60
|
|
||||||
hours = minutes / 60
|
|
||||||
seconds = seconds % 60
|
|
||||||
minutes = minutes % 60
|
|
||||||
millis = millis % 1000
|
|
||||||
return {'hours': hours,
|
|
||||||
'minutes': minutes,
|
|
||||||
'seconds': seconds,
|
|
||||||
'milliseconds': millis}
|
|
||||||
|
|
||||||
|
|
||||||
def textFromXml(element):
|
|
||||||
return element.firstChild.data
|
|
||||||
|
|
||||||
|
|
||||||
class jsonClass():
|
|
||||||
|
|
||||||
def __init__(self, requestMgr, settings):
|
|
||||||
self.settings = settings
|
|
||||||
self.requestMgr = requestMgr
|
|
||||||
|
|
||||||
def jsonrpc(self, action, arguments={}):
|
|
||||||
""" put some JSON together for the JSON-RPC APIv6 """
|
|
||||||
if action.lower() == "sendkey":
|
|
||||||
request = json.dumps({
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "Input.SendText",
|
|
||||||
"params": {
|
|
||||||
"text": arguments[0],
|
|
||||||
"done": False
|
|
||||||
}
|
|
||||||
})
|
|
||||||
elif action.lower() == "ping":
|
|
||||||
request = json.dumps({
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "JSONRPC.Ping"
|
|
||||||
})
|
|
||||||
elif arguments:
|
|
||||||
request = json.dumps({
|
|
||||||
"id": 1,
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": action,
|
|
||||||
"params": arguments})
|
|
||||||
else:
|
|
||||||
request = json.dumps({
|
|
||||||
"id": 1,
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": action
|
|
||||||
})
|
|
||||||
|
|
||||||
result = self.parseJSONRPC(xbmc.executeJSONRPC(request))
|
|
||||||
|
|
||||||
if not result and self.settings['webserver_enabled']:
|
|
||||||
# xbmc.executeJSONRPC appears to fail on the login screen, but
|
|
||||||
# going through the network stack works, so let's try the request
|
|
||||||
# again
|
|
||||||
result = self.parseJSONRPC(self.requestMgr.post(
|
|
||||||
"127.0.0.1",
|
|
||||||
self.settings['port'],
|
|
||||||
"/jsonrpc",
|
|
||||||
request,
|
|
||||||
{'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Basic %s' % string.strip(
|
|
||||||
base64.encodestring('%s:%s'
|
|
||||||
% (self.settings['user'],
|
|
||||||
self.settings['passwd'])))
|
|
||||||
}))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def skipTo(self, plexId, typus):
|
|
||||||
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
|
|
||||||
# playerId = self.
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plexdb_item = plex_db.getItem_byId(plexId)
|
|
||||||
try:
|
|
||||||
dbid = plexdb_item[0]
|
|
||||||
mediatype = plexdb_item[4]
|
|
||||||
except TypeError:
|
|
||||||
log.info('Couldnt find item %s in Kodi db' % plexId)
|
|
||||||
return
|
|
||||||
log.debug('plexid: %s, kodi id: %s, type: %s'
|
|
||||||
% (plexId, dbid, mediatype))
|
|
||||||
|
|
||||||
def getPlexHeaders(self):
|
|
||||||
h = {
|
|
||||||
"Content-type": "text/xml",
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"X-Plex-Version": self.settings['version'],
|
|
||||||
"X-Plex-Client-Identifier": self.settings['uuid'],
|
|
||||||
"X-Plex-Provides": "client,controller,player",
|
|
||||||
"X-Plex-Product": "PlexKodiConnect",
|
|
||||||
"X-Plex-Device-Name": self.settings['client_name'],
|
|
||||||
"X-Plex-Platform": "Kodi",
|
|
||||||
"X-Plex-Model": self.settings['platform'],
|
|
||||||
"X-Plex-Device": "PC",
|
|
||||||
}
|
|
||||||
if self.settings['myplex_user']:
|
|
||||||
h["X-Plex-Username"] = self.settings['myplex_user']
|
|
||||||
return h
|
|
||||||
|
|
||||||
def parseJSONRPC(self, jsonraw):
|
|
||||||
if not jsonraw:
|
|
||||||
log.debug("Empty response from Kodi")
|
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
parsed = json.loads(jsonraw)
|
|
||||||
if parsed.get('error', False):
|
|
||||||
log.error("Kodi returned an error: %s" % parsed.get('error'))
|
|
||||||
return parsed.get('result', {})
|
|
||||||
|
|
||||||
def getPlayers(self):
|
|
||||||
info = self.jsonrpc("Player.GetActivePlayers") or []
|
|
||||||
ret = {}
|
|
||||||
for player in info:
|
|
||||||
player['playerid'] = int(player['playerid'])
|
|
||||||
ret[player['type']] = player
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def getPlaylistId(self, typus):
|
|
||||||
"""
|
|
||||||
typus: one of the Kodi types, e.g. audio or video
|
|
||||||
|
|
||||||
Returns None if nothing was found
|
|
||||||
"""
|
|
||||||
for playlist in self.getPlaylists():
|
|
||||||
if playlist.get('type') == typus:
|
|
||||||
return playlist.get('playlistid')
|
|
||||||
|
|
||||||
def getPlaylists(self):
|
|
||||||
"""
|
|
||||||
Returns a list, e.g.
|
|
||||||
[
|
|
||||||
{u'playlistid': 0, u'type': u'audio'},
|
|
||||||
{u'playlistid': 1, u'type': u'video'},
|
|
||||||
{u'playlistid': 2, u'type': u'picture'}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
return self.jsonrpc('Playlist.GetPlaylists')
|
|
||||||
|
|
||||||
def getPlayerIds(self):
|
|
||||||
ret = []
|
|
||||||
for player in self.getPlayers().values():
|
|
||||||
ret.append(player['playerid'])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def getVideoPlayerId(self, players=False):
|
|
||||||
if players is None:
|
|
||||||
players = self.getPlayers()
|
|
||||||
return players.get(xbmc_video(), {}).get('playerid', None)
|
|
||||||
|
|
||||||
def getAudioPlayerId(self, players=False):
|
|
||||||
if players is None:
|
|
||||||
players = self.getPlayers()
|
|
||||||
return players.get(xbmc_audio(), {}).get('playerid', None)
|
|
||||||
|
|
||||||
def getPhotoPlayerId(self, players=False):
|
|
||||||
if players is None:
|
|
||||||
players = self.getPlayers()
|
|
||||||
return players.get(xbmc_photo(), {}).get('playerid', None)
|
|
||||||
|
|
||||||
def getVolume(self):
|
|
||||||
answ = self.jsonrpc('Application.GetProperties',
|
|
||||||
{
|
|
||||||
"properties": ["volume", 'muted']
|
|
||||||
})
|
|
||||||
vol = str(answ.get('volume', 100))
|
|
||||||
mute = ("0", "1")[answ.get('muted', False)]
|
|
||||||
return (vol, mute)
|
|
|
@ -1,4 +1,4 @@
|
||||||
import logging
|
from logging import getLogger
|
||||||
import httplib
|
import httplib
|
||||||
import traceback
|
import traceback
|
||||||
import string
|
import string
|
||||||
|
@ -7,7 +7,7 @@ from socket import error as socket_error
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -17,20 +17,20 @@ class RequestMgr:
|
||||||
self.conns = {}
|
self.conns = {}
|
||||||
|
|
||||||
def getConnection(self, protocol, host, port):
|
def getConnection(self, protocol, host, port):
|
||||||
conn = self.conns.get(protocol+host+str(port), False)
|
conn = self.conns.get(protocol + host + str(port), False)
|
||||||
if not conn:
|
if not conn:
|
||||||
if protocol == "https":
|
if protocol == "https":
|
||||||
conn = httplib.HTTPSConnection(host, port)
|
conn = httplib.HTTPSConnection(host, port)
|
||||||
else:
|
else:
|
||||||
conn = httplib.HTTPConnection(host, port)
|
conn = httplib.HTTPConnection(host, port)
|
||||||
self.conns[protocol+host+str(port)] = conn
|
self.conns[protocol + host + str(port)] = conn
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def closeConnection(self, protocol, host, port):
|
def closeConnection(self, protocol, host, port):
|
||||||
conn = self.conns.get(protocol+host+str(port), False)
|
conn = self.conns.get(protocol + host + str(port), False)
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
|
|
||||||
def dumpConnections(self):
|
def dumpConnections(self):
|
||||||
for conn in self.conns.values():
|
for conn in self.conns.values():
|
||||||
|
@ -45,7 +45,7 @@ class RequestMgr:
|
||||||
conn.request("POST", path, body, header)
|
conn.request("POST", path, body, header)
|
||||||
data = conn.getresponse()
|
data = conn.getresponse()
|
||||||
if int(data.status) >= 400:
|
if int(data.status) >= 400:
|
||||||
log.error("HTTP response error: %s" % str(data.status))
|
LOG.error("HTTP response error: %s" % str(data.status))
|
||||||
# this should return false, but I'm hacking it since iOS
|
# this should return false, but I'm hacking it since iOS
|
||||||
# returns 404 no matter what
|
# returns 404 no matter what
|
||||||
return data.read() or True
|
return data.read() or True
|
||||||
|
@ -56,14 +56,14 @@ class RequestMgr:
|
||||||
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.error("Unable to connect to %s\nReason:" % host)
|
LOG.error("Unable to connect to %s\nReason:" % host)
|
||||||
log.error(traceback.print_exc())
|
LOG.error(traceback.print_exc())
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Exception encountered: %s" % e)
|
LOG.error("Exception encountered: %s", e)
|
||||||
# Close connection just in case
|
# Close connection just in case
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
@ -76,7 +76,7 @@ class RequestMgr:
|
||||||
newpath = path + '?'
|
newpath = path + '?'
|
||||||
pairs = []
|
pairs = []
|
||||||
for key in params:
|
for key in params:
|
||||||
pairs.append(str(key)+'='+str(params[key]))
|
pairs.append(str(key) + '=' + str(params[key]))
|
||||||
newpath += string.join(pairs, '&')
|
newpath += string.join(pairs, '&')
|
||||||
return self.get(host, port, newpath, header, protocol)
|
return self.get(host, port, newpath, header, protocol)
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class RequestMgr:
|
||||||
conn.request("GET", path, headers=header)
|
conn.request("GET", path, headers=header)
|
||||||
data = conn.getresponse()
|
data = conn.getresponse()
|
||||||
if int(data.status) >= 400:
|
if int(data.status) >= 400:
|
||||||
log.error("HTTP response error: %s" % str(data.status))
|
LOG.error("HTTP response error: %s", str(data.status))
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return data.read() or True
|
return data.read() or True
|
||||||
|
@ -96,8 +96,8 @@ class RequestMgr:
|
||||||
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.error("Unable to connect to %s\nReason:" % host)
|
LOG.error("Unable to connect to %s\nReason:", host)
|
||||||
log.error(traceback.print_exc())
|
LOG.error(traceback.print_exc())
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
conn.close()
|
conn.close()
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,53 +1,68 @@
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
import logging
|
Plex Companion listener
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
from re import sub
|
from re import sub
|
||||||
from SocketServer import ThreadingMixIn
|
from SocketServer import ThreadingMixIn
|
||||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||||
from urlparse import urlparse, parse_qs
|
from urlparse import urlparse, parse_qs
|
||||||
|
|
||||||
from xbmc import sleep
|
from xbmc import sleep, Player, Monitor
|
||||||
|
|
||||||
from companion import process_command
|
from companion import process_command
|
||||||
from utils import window
|
import json_rpc as js
|
||||||
|
from clientinfo import getXArgsDeviceInfo
|
||||||
from functions import *
|
import variables as v
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
PLAYER = Player()
|
||||||
|
MONITOR = Monitor()
|
||||||
|
|
||||||
|
# Hack we need in order to keep track of the open connections from Plex Web
|
||||||
|
CLIENT_DICT = {}
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
RESOURCES_XML = ('%s<MediaContainer>\n'
|
||||||
|
' <Player'
|
||||||
|
' title="{title}"'
|
||||||
|
' protocol="plex"'
|
||||||
|
' protocolVersion="1"'
|
||||||
|
' protocolCapabilities="timeline,playback,navigation,playqueues"'
|
||||||
|
' machineIdentifier="{machineIdentifier}"'
|
||||||
|
' product="%s"'
|
||||||
|
' platform="%s"'
|
||||||
|
' platformVersion="%s"'
|
||||||
|
' deviceClass="pc"/>\n'
|
||||||
|
'</MediaContainer>\n') % (v.XML_HEADER,
|
||||||
|
v.ADDON_NAME,
|
||||||
|
v.PLATFORM,
|
||||||
|
v.ADDON_VERSION)
|
||||||
|
|
||||||
class MyHandler(BaseHTTPRequestHandler):
|
class MyHandler(BaseHTTPRequestHandler):
|
||||||
|
"""
|
||||||
|
BaseHTTPRequestHandler implementation of Plex Companion listener
|
||||||
|
"""
|
||||||
protocol_version = 'HTTP/1.1'
|
protocol_version = 'HTTP/1.1'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
|
|
||||||
self.serverlist = []
|
self.serverlist = []
|
||||||
|
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
|
||||||
def getServerByHost(self, host):
|
|
||||||
if len(self.serverlist) == 1:
|
|
||||||
return self.serverlist[0]
|
|
||||||
for server in self.serverlist:
|
|
||||||
if (server.get('serverName') in host or
|
|
||||||
server.get('server') in host):
|
|
||||||
return server
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
log.debug("Serving HEAD request...")
|
LOG.debug("Serving HEAD request...")
|
||||||
self.answer_request(0)
|
self.answer_request(0)
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
log.debug("Serving GET request...")
|
LOG.debug("Serving GET request...")
|
||||||
self.answer_request(1)
|
self.answer_request(1)
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Length', '0')
|
self.send_header('Content-Length', '0')
|
||||||
self.send_header('X-Plex-Client-Identifier',
|
self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER)
|
||||||
self.server.settings['uuid'])
|
|
||||||
self.send_header('Content-Type', 'text/plain')
|
self.send_header('Content-Type', 'text/plain')
|
||||||
self.send_header('Connection', 'close')
|
self.send_header('Connection', 'close')
|
||||||
self.send_header('Access-Control-Max-Age', '1209600')
|
self.send_header('Access-Control-Max-Age', '1209600')
|
||||||
|
@ -66,7 +81,8 @@ class MyHandler(BaseHTTPRequestHandler):
|
||||||
def sendOK(self):
|
def sendOK(self):
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|
||||||
def response(self, body, headers={}, code=200):
|
def response(self, body, headers=None, code=200):
|
||||||
|
headers = {} if headers is None else headers
|
||||||
try:
|
try:
|
||||||
self.send_response(code)
|
self.send_response(code)
|
||||||
for key in headers:
|
for key in headers:
|
||||||
|
@ -79,112 +95,135 @@ class MyHandler(BaseHTTPRequestHandler):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def answer_request(self, sendData):
|
def answer_request(self, send_data):
|
||||||
self.serverlist = self.server.client.getServerList()
|
self.serverlist = self.server.client.getServerList()
|
||||||
subMgr = self.server.subscriptionManager
|
sub_mgr = self.server.subscription_manager
|
||||||
js = self.server.jsonClass
|
|
||||||
settings = self.server.settings
|
|
||||||
|
|
||||||
try:
|
request_path = self.path[1:]
|
||||||
request_path = self.path[1:]
|
request_path = sub(r"\?.*", "", request_path)
|
||||||
request_path = sub(r"\?.*", "", request_path)
|
url = urlparse(self.path)
|
||||||
url = urlparse(self.path)
|
paramarrays = parse_qs(url.query)
|
||||||
paramarrays = parse_qs(url.query)
|
params = {}
|
||||||
params = {}
|
for key in paramarrays:
|
||||||
for key in paramarrays:
|
params[key] = paramarrays[key][0]
|
||||||
params[key] = paramarrays[key][0]
|
LOG.debug("remote request_path: %s", request_path)
|
||||||
log.debug("remote request_path: %s" % request_path)
|
LOG.debug("params received from remote: %s", params)
|
||||||
log.debug("params received from remote: %s" % params)
|
sub_mgr.update_command_id(self.headers.get(
|
||||||
subMgr.updateCommandID(self.headers.get(
|
'X-Plex-Client-Identifier', self.client_address[0]),
|
||||||
'X-Plex-Client-Identifier',
|
params.get('commandID'))
|
||||||
self.client_address[0]),
|
if request_path == "version":
|
||||||
params.get('commandID', False))
|
self.response(
|
||||||
if request_path == "version":
|
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
|
||||||
|
% v.ADDON_VERSION)
|
||||||
|
elif request_path == "verify":
|
||||||
|
self.response("XBMC JSON connection test:\n" + js.ping())
|
||||||
|
elif request_path == 'resources':
|
||||||
|
self.response(
|
||||||
|
RESOURCES_XML.format(
|
||||||
|
title=v.DEVICENAME,
|
||||||
|
machineIdentifier=v.PKC_MACHINE_IDENTIFIER),
|
||||||
|
getXArgsDeviceInfo(include_token=False))
|
||||||
|
elif request_path == 'player/timeline/poll':
|
||||||
|
# Plex web does polling if connected to PKC via Companion
|
||||||
|
# Only reply if there is indeed something playing
|
||||||
|
# Otherwise, all clients seem to keep connection open
|
||||||
|
if params.get('wait') == '1':
|
||||||
|
MONITOR.waitForAbort(0.95)
|
||||||
|
if self.client_address[0] not in CLIENT_DICT:
|
||||||
|
CLIENT_DICT[self.client_address[0]] = []
|
||||||
|
tracker = CLIENT_DICT[self.client_address[0]]
|
||||||
|
tracker.append(self.client_address[1])
|
||||||
|
while (not PLAYER.isPlaying() and
|
||||||
|
not MONITOR.abortRequested() and
|
||||||
|
sub_mgr.stop_sent_to_web and not
|
||||||
|
(len(tracker) >= 4 and
|
||||||
|
tracker[0] == self.client_address[1])):
|
||||||
|
# Keep at most 3 connections open, then drop the first one
|
||||||
|
# Doesn't need to be thread-save
|
||||||
|
# Silly stuff really
|
||||||
|
MONITOR.waitForAbort(1)
|
||||||
|
# Let PKC know that we're releasing this connection
|
||||||
|
tracker.pop(0)
|
||||||
|
msg = sub_mgr.msg(js.get_players()).format(
|
||||||
|
command_id=params.get('commandID', 0))
|
||||||
|
if sub_mgr.isplaying:
|
||||||
self.response(
|
self.response(
|
||||||
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
|
msg,
|
||||||
% settings['version'])
|
|
||||||
elif request_path == "verify":
|
|
||||||
self.response("XBMC JSON connection test:\n" +
|
|
||||||
js.jsonrpc("ping"))
|
|
||||||
elif "resources" == request_path:
|
|
||||||
resp = ('%s'
|
|
||||||
'<MediaContainer>'
|
|
||||||
'<Player'
|
|
||||||
' title="%s"'
|
|
||||||
' protocol="plex"'
|
|
||||||
' protocolVersion="1"'
|
|
||||||
' protocolCapabilities="timeline,playback,navigation,playqueues"'
|
|
||||||
' machineIdentifier="%s"'
|
|
||||||
' product="PlexKodiConnect"'
|
|
||||||
' platform="%s"'
|
|
||||||
' platformVersion="%s"'
|
|
||||||
' deviceClass="pc"'
|
|
||||||
'/>'
|
|
||||||
'</MediaContainer>'
|
|
||||||
% (getXMLHeader(),
|
|
||||||
settings['client_name'],
|
|
||||||
settings['uuid'],
|
|
||||||
settings['platform'],
|
|
||||||
settings['plexbmc_version']))
|
|
||||||
log.debug("crafted resources response: %s" % resp)
|
|
||||||
self.response(resp, js.getPlexHeaders())
|
|
||||||
elif "/subscribe" in request_path:
|
|
||||||
self.response(getOKMsg(), js.getPlexHeaders())
|
|
||||||
protocol = params.get('protocol', False)
|
|
||||||
host = self.client_address[0]
|
|
||||||
port = params.get('port', False)
|
|
||||||
uuid = self.headers.get('X-Plex-Client-Identifier', "")
|
|
||||||
commandID = params.get('commandID', 0)
|
|
||||||
subMgr.addSubscriber(protocol,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
uuid,
|
|
||||||
commandID)
|
|
||||||
elif "/poll" in request_path:
|
|
||||||
if params.get('wait', False) == '1':
|
|
||||||
sleep(950)
|
|
||||||
commandID = params.get('commandID', 0)
|
|
||||||
self.response(
|
|
||||||
sub(r"INSERTCOMMANDID",
|
|
||||||
str(commandID),
|
|
||||||
subMgr.msg(js.getPlayers())),
|
|
||||||
{
|
{
|
||||||
'X-Plex-Client-Identifier': settings['uuid'],
|
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
'X-Plex-Protocol': '1.0',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Max-Age': '1209600',
|
||||||
'Access-Control-Expose-Headers':
|
'Access-Control-Expose-Headers':
|
||||||
'X-Plex-Client-Identifier',
|
'X-Plex-Client-Identifier',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Content-Type': 'text/xml;charset=utf-8'
|
||||||
'Content-Type': 'text/xml'
|
})
|
||||||
|
elif not sub_mgr.stop_sent_to_web:
|
||||||
|
sub_mgr.stop_sent_to_web = True
|
||||||
|
LOG.debug('Signaling STOP to Plex Web')
|
||||||
|
self.response(
|
||||||
|
msg,
|
||||||
|
{
|
||||||
|
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
'X-Plex-Protocol': '1.0',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Max-Age': '1209600',
|
||||||
|
'Access-Control-Expose-Headers':
|
||||||
|
'X-Plex-Client-Identifier',
|
||||||
|
'Content-Type': 'text/xml;charset=utf-8'
|
||||||
})
|
})
|
||||||
elif "/unsubscribe" in request_path:
|
|
||||||
self.response(getOKMsg(), js.getPlexHeaders())
|
|
||||||
uuid = self.headers.get('X-Plex-Client-Identifier', False) \
|
|
||||||
or self.client_address[0]
|
|
||||||
subMgr.removeSubscriber(uuid)
|
|
||||||
else:
|
else:
|
||||||
# Throw it to companion.py
|
# Fail connection with HTTP 500 error - has been open too long
|
||||||
process_command(request_path, params, self.server.queue)
|
self.response(
|
||||||
self.response('', js.getPlexHeaders())
|
'Need to close this connection on the PKC side',
|
||||||
subMgr.notify()
|
{
|
||||||
except:
|
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
|
||||||
log.error('Error encountered. Traceback:')
|
'X-Plex-Protocol': '1.0',
|
||||||
import traceback
|
'Access-Control-Allow-Origin': '*',
|
||||||
log.error(traceback.print_exc())
|
'Access-Control-Max-Age': '1209600',
|
||||||
|
'Access-Control-Expose-Headers':
|
||||||
|
'X-Plex-Client-Identifier',
|
||||||
|
'Content-Type': 'text/xml;charset=utf-8'
|
||||||
|
},
|
||||||
|
code=500)
|
||||||
|
elif "/subscribe" in request_path:
|
||||||
|
self.response(v.COMPANION_OK_MESSAGE,
|
||||||
|
getXArgsDeviceInfo(include_token=False))
|
||||||
|
protocol = params.get('protocol')
|
||||||
|
host = self.client_address[0]
|
||||||
|
port = params.get('port')
|
||||||
|
uuid = self.headers.get('X-Plex-Client-Identifier')
|
||||||
|
command_id = params.get('commandID', 0)
|
||||||
|
sub_mgr.add_subscriber(protocol,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
uuid,
|
||||||
|
command_id)
|
||||||
|
elif "/unsubscribe" in request_path:
|
||||||
|
self.response(v.COMPANION_OK_MESSAGE,
|
||||||
|
getXArgsDeviceInfo(include_token=False))
|
||||||
|
uuid = self.headers.get('X-Plex-Client-Identifier') \
|
||||||
|
or self.client_address[0]
|
||||||
|
sub_mgr.remove_subscriber(uuid)
|
||||||
|
else:
|
||||||
|
# Throw it to companion.py
|
||||||
|
process_command(request_path, params)
|
||||||
|
self.response('', getXArgsDeviceInfo(include_token=False))
|
||||||
|
|
||||||
|
|
||||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
|
"""
|
||||||
|
Using ThreadingMixIn Thread magic
|
||||||
|
"""
|
||||||
daemon_threads = True
|
daemon_threads = True
|
||||||
|
|
||||||
def __init__(self, client, subscriptionManager, jsonClass, settings,
|
def __init__(self, client, subscription_manager, *args, **kwargs):
|
||||||
queue, *args, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
|
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
|
||||||
date serverlist without instantiating anything
|
date serverlist without instantiating anything
|
||||||
|
|
||||||
same for SubscriptionManager and jsonClass
|
same for SubscriptionMgr
|
||||||
"""
|
"""
|
||||||
self.client = client
|
self.client = client
|
||||||
self.subscriptionManager = subscriptionManager
|
self.subscription_manager = subscription_manager
|
||||||
self.jsonClass = jsonClass
|
|
||||||
self.settings = settings
|
|
||||||
self.queue = queue
|
|
||||||
HTTPServer.__init__(self, *args, **kwargs)
|
HTTPServer.__init__(self, *args, **kwargs)
|
||||||
|
|
|
@ -30,6 +30,7 @@ from xbmc import sleep
|
||||||
|
|
||||||
import downloadutils
|
import downloadutils
|
||||||
from utils import window, settings, dialog, language
|
from utils import window, settings, dialog, language
|
||||||
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -44,7 +45,6 @@ class plexgdm:
|
||||||
self.discover_message = 'M-SEARCH * HTTP/1.0'
|
self.discover_message = 'M-SEARCH * HTTP/1.0'
|
||||||
self.client_header = '* HTTP/1.0'
|
self.client_header = '* HTTP/1.0'
|
||||||
self.client_data = None
|
self.client_data = None
|
||||||
self.client_id = None
|
|
||||||
|
|
||||||
self._multicast_address = '239.0.0.250'
|
self._multicast_address = '239.0.0.250'
|
||||||
self.discover_group = (self._multicast_address, 32414)
|
self.discover_group = (self._multicast_address, 32414)
|
||||||
|
@ -60,7 +60,7 @@ class plexgdm:
|
||||||
self.client_registered = False
|
self.client_registered = False
|
||||||
self.download = downloadutils.DownloadUtils().downloadUrl
|
self.download = downloadutils.DownloadUtils().downloadUrl
|
||||||
|
|
||||||
def clientDetails(self, options):
|
def clientDetails(self):
|
||||||
self.client_data = (
|
self.client_data = (
|
||||||
"Content-Type: plex/media-player\n"
|
"Content-Type: plex/media-player\n"
|
||||||
"Resource-Identifier: %s\n"
|
"Resource-Identifier: %s\n"
|
||||||
|
@ -74,13 +74,12 @@ class plexgdm:
|
||||||
"playqueues\n"
|
"playqueues\n"
|
||||||
"Device-Class: HTPC\n"
|
"Device-Class: HTPC\n"
|
||||||
) % (
|
) % (
|
||||||
options['uuid'],
|
v.PKC_MACHINE_IDENTIFIER,
|
||||||
options['client_name'],
|
v.DEVICENAME,
|
||||||
options['myport'],
|
v.COMPANION_PORT,
|
||||||
options['addonName'],
|
v.ADDON_NAME,
|
||||||
options['version']
|
v.ADDON_VERSION
|
||||||
)
|
)
|
||||||
self.client_id = options['uuid']
|
|
||||||
|
|
||||||
def getClientDetails(self):
|
def getClientDetails(self):
|
||||||
return self.client_data
|
return self.client_data
|
||||||
|
@ -211,7 +210,7 @@ class plexgdm:
|
||||||
registered = False
|
registered = False
|
||||||
for client in xml:
|
for client in xml:
|
||||||
if (client.attrib.get('machineIdentifier') ==
|
if (client.attrib.get('machineIdentifier') ==
|
||||||
self.client_id):
|
v.PKC_MACHINE_IDENTIFIER):
|
||||||
registered = True
|
registered = True
|
||||||
if registered:
|
if registered:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import logging
|
|
||||||
from utils import guisettingsXML, settings
|
|
||||||
import variables as v
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def getGUI(name):
|
|
||||||
xml = guisettingsXML()
|
|
||||||
try:
|
|
||||||
ans = list(xml.iter(name))[0].text
|
|
||||||
if ans is None:
|
|
||||||
ans = ''
|
|
||||||
except:
|
|
||||||
ans = ''
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
def getSettings():
|
|
||||||
options = {}
|
|
||||||
|
|
||||||
options['gdm_debug'] = settings('companionGDMDebugging')
|
|
||||||
options['gdm_debug'] = True if options['gdm_debug'] == 'true' else False
|
|
||||||
|
|
||||||
options['client_name'] = v.DEVICENAME
|
|
||||||
|
|
||||||
# XBMC web server options
|
|
||||||
options['webserver_enabled'] = (getGUI('webserver') == "true")
|
|
||||||
log.info('Webserver is set to %s' % options['webserver_enabled'])
|
|
||||||
webserverport = getGUI('webserverport')
|
|
||||||
try:
|
|
||||||
webserverport = int(webserverport)
|
|
||||||
log.info('Using webserver port %s' % str(webserverport))
|
|
||||||
except:
|
|
||||||
log.info('No setting for webserver port found in guisettings.xml.'
|
|
||||||
'Using default fallback port 8080')
|
|
||||||
webserverport = 8080
|
|
||||||
options['port'] = webserverport
|
|
||||||
|
|
||||||
options['user'] = getGUI('webserverusername')
|
|
||||||
options['passwd'] = getGUI('webserverpassword')
|
|
||||||
log.info('Webserver username: %s, password: %s'
|
|
||||||
% (options['user'], options['passwd']))
|
|
||||||
|
|
||||||
options['addonName'] = v.ADDON_NAME
|
|
||||||
options['uuid'] = settings('plex_client_Id')
|
|
||||||
options['platform'] = v.PLATFORM
|
|
||||||
options['version'] = v.ADDON_VERSION
|
|
||||||
options['plexbmc_version'] = options['version']
|
|
||||||
options['myplex_user'] = settings('username')
|
|
||||||
try:
|
|
||||||
options['myport'] = int(settings('companionPort'))
|
|
||||||
log.info('Using Plex Companion Port %s' % str(options['myport']))
|
|
||||||
except:
|
|
||||||
log.error('Error getting Plex Companion Port from file settings. '
|
|
||||||
'Using fallback port 39005')
|
|
||||||
options['myport'] = 39005
|
|
||||||
return options
|
|
|
@ -1,49 +1,135 @@
|
||||||
import logging
|
"""
|
||||||
import re
|
Manages getting playstate from Kodi and sending it to the PMS as well as
|
||||||
import threading
|
subscribed Plex Companion clients.
|
||||||
|
"""
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread, RLock
|
||||||
|
|
||||||
import downloadutils
|
from downloadutils import DownloadUtils as DU
|
||||||
from clientinfo import getXArgsDeviceInfo
|
from utils import window, kodi_time_to_millis, LockFunction
|
||||||
from utils import window
|
|
||||||
import PlexFunctions as pf
|
|
||||||
import state
|
import state
|
||||||
from functions import *
|
import variables as v
|
||||||
|
import json_rpc as js
|
||||||
|
import playqueue as PQ
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
# Need to lock all methods and functions messing with subscribers or state
|
||||||
|
LOCK = RLock()
|
||||||
|
LOCKER = LockFunction(LOCK)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
# What is Companion controllable?
|
||||||
|
CONTROLLABLE = {
|
||||||
|
v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
|
||||||
|
'subtitleStream,seekTo,skipPrevious,skipNext,'
|
||||||
|
'stepBack,stepForward',
|
||||||
|
v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
|
||||||
|
'skipPrevious,skipNext,stepBack,stepForward',
|
||||||
|
v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext'
|
||||||
|
}
|
||||||
|
|
||||||
class SubscriptionManager:
|
STREAM_DETAILS = {
|
||||||
def __init__(self, jsonClass, RequestMgr, player, mgr):
|
'video': 'currentvideostream',
|
||||||
|
'audio': 'currentaudiostream',
|
||||||
|
'subtitle': 'currentsubtitle'
|
||||||
|
}
|
||||||
|
|
||||||
|
XML = ('%s<MediaContainer commandID="{command_id}" location="{location}">\n'
|
||||||
|
' <Timeline {%s}/>\n'
|
||||||
|
' <Timeline {%s}/>\n'
|
||||||
|
' <Timeline {%s}/>\n'
|
||||||
|
'</MediaContainer>\n') % (v.XML_HEADER,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_VIDEO,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_AUDIO,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_PHOTO)
|
||||||
|
|
||||||
|
# Headers are different for Plex Companion - use these for PMS notifications
|
||||||
|
HEADERS_PMS = {
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Accept': 'text/plain, */*; q=0.01',
|
||||||
|
'Accept-Language': 'en',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.PLATFORM,
|
||||||
|
'X-Plex-Device-Name': v.DEVICENAME,
|
||||||
|
# 'X-Plex-Device-Screen-Resolution': '1916x1018,1920x1080',
|
||||||
|
'X-Plex-Model': 'unknown',
|
||||||
|
'X-Plex-Platform': v.PLATFORM,
|
||||||
|
'X-Plex-Platform-Version': 'unknown',
|
||||||
|
'X-Plex-Product': v.ADDON_NAME,
|
||||||
|
'X-Plex-Provider-Version': v.ADDON_VERSION,
|
||||||
|
'X-Plex-Version': v.ADDON_VERSION,
|
||||||
|
'hasMDE': '1',
|
||||||
|
# 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def headers_companion_client():
|
||||||
|
"""
|
||||||
|
Headers are different for Plex Companion - use these for a Plex Companion
|
||||||
|
client
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'Connection': 'Keep-Alive',
|
||||||
|
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
'X-Plex-Device-Name': v.DEVICENAME,
|
||||||
|
'X-Plex-Platform': v.PLATFORM,
|
||||||
|
'X-Plex-Platform-Version': 'unknown',
|
||||||
|
'X-Plex-Product': v.ADDON_NAME,
|
||||||
|
'X-Plex-Version': v.ADDON_VERSION,
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Accept-Language': 'en,*'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_player_info(playerid):
|
||||||
|
"""
|
||||||
|
Updates all player info for playerid [int] in state.py.
|
||||||
|
"""
|
||||||
|
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
||||||
|
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
||||||
|
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionMgr(object):
|
||||||
|
"""
|
||||||
|
Manages Plex companion subscriptions
|
||||||
|
"""
|
||||||
|
def __init__(self, request_mgr, player):
|
||||||
self.serverlist = []
|
self.serverlist = []
|
||||||
self.subscribers = {}
|
self.subscribers = {}
|
||||||
self.info = {}
|
self.info = {}
|
||||||
self.lastkey = ""
|
|
||||||
self.containerKey = ""
|
|
||||||
self.ratingkey = ""
|
|
||||||
self.lastplayers = {}
|
|
||||||
self.lastinfo = {
|
|
||||||
'video': {},
|
|
||||||
'audio': {},
|
|
||||||
'picture': {}
|
|
||||||
}
|
|
||||||
self.volume = 0
|
|
||||||
self.mute = '0'
|
|
||||||
self.server = ""
|
self.server = ""
|
||||||
self.protocol = "http"
|
self.protocol = "http"
|
||||||
self.port = ""
|
self.port = ""
|
||||||
self.playerprops = {}
|
self.isplaying = False
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
# In order to be able to signal a stop at the end
|
||||||
|
self.last_params = {}
|
||||||
|
self.lastplayers = {}
|
||||||
|
# In order to signal a stop to Plex Web ONCE on playback stop
|
||||||
|
self.stop_sent_to_web = True
|
||||||
|
|
||||||
self.xbmcplayer = player
|
self.xbmcplayer = player
|
||||||
self.playqueue = mgr.playqueue
|
self.request_mgr = request_mgr
|
||||||
|
|
||||||
self.js = jsonClass
|
def _server_by_host(self, host):
|
||||||
self.RequestMgr = RequestMgr
|
|
||||||
|
|
||||||
def getServerByHost(self, host):
|
|
||||||
if len(self.serverlist) == 1:
|
if len(self.serverlist) == 1:
|
||||||
return self.serverlist[0]
|
return self.serverlist[0]
|
||||||
for server in self.serverlist:
|
for server in self.serverlist:
|
||||||
|
@ -52,281 +138,347 @@ class SubscriptionManager:
|
||||||
return server
|
return server
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def getVolume(self):
|
@LOCKER.lockthis
|
||||||
self.volume, self.mute = self.js.getVolume()
|
|
||||||
|
|
||||||
def msg(self, players):
|
def msg(self, players):
|
||||||
msg = getXMLHeader()
|
"""
|
||||||
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
|
Returns a timeline xml as str
|
||||||
msg += ' machineIdentifier="%s" location="fullScreenVideo">' % window('plex_client_Id')
|
(xml containing video, audio, photo player state)
|
||||||
msg += self.getTimelineXML(self.js.getAudioPlayerId(players), plex_audio())
|
"""
|
||||||
msg += self.getTimelineXML(self.js.getPhotoPlayerId(players), plex_photo())
|
self.isplaying = False
|
||||||
msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video())
|
answ = str(XML)
|
||||||
msg += "\n</MediaContainer>"
|
timelines = {
|
||||||
return msg
|
v.PLEX_PLAYLIST_TYPE_VIDEO: None,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_AUDIO: None,
|
||||||
|
v.PLEX_PLAYLIST_TYPE_PHOTO: None
|
||||||
|
}
|
||||||
|
for typus in timelines:
|
||||||
|
if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
|
||||||
|
timeline = {
|
||||||
|
'controllable': CONTROLLABLE[typus],
|
||||||
|
'type': typus,
|
||||||
|
'state': 'stopped'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
timeline = self._timeline_dict(players[
|
||||||
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]],
|
||||||
|
typus)
|
||||||
|
timelines[typus] = self._dict_to_xml(timeline)
|
||||||
|
location = 'fullScreenVideo' if self.isplaying else 'navigation'
|
||||||
|
timelines.update({'command_id': '{command_id}', 'location': location})
|
||||||
|
return answ.format(**timelines)
|
||||||
|
|
||||||
def getTimelineXML(self, playerid, ptype):
|
@staticmethod
|
||||||
if playerid is not None:
|
def _dict_to_xml(dictionary):
|
||||||
info = self.getPlayerProperties(playerid)
|
"""
|
||||||
# save this info off so the server update can use it too
|
Returns the string 'key1="value1" key2="value2" ...' for dictionary
|
||||||
self.playerprops[playerid] = info;
|
"""
|
||||||
status = info['state']
|
answ = ''
|
||||||
time = info['time']
|
for key, value in dictionary.iteritems():
|
||||||
else:
|
answ += '%s="%s" ' % (key, value)
|
||||||
status = "stopped"
|
return answ
|
||||||
time = 0
|
|
||||||
ret = "\n"+' <Timeline state="%s" time="%s" type="%s"' % (status, time, ptype)
|
|
||||||
if playerid is None:
|
|
||||||
ret += ' />'
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
def _timeline_dict(self, player, ptype):
|
||||||
|
playerid = player['playerid']
|
||||||
|
info = state.PLAYER_STATES[playerid]
|
||||||
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
|
pos = info['position']
|
||||||
|
try:
|
||||||
|
item = playqueue.items[pos]
|
||||||
|
except IndexError:
|
||||||
|
# E.g. for direct path playback for single item
|
||||||
|
return {
|
||||||
|
'controllable': CONTROLLABLE[ptype],
|
||||||
|
'type': ptype,
|
||||||
|
'state': 'stopped'
|
||||||
|
}
|
||||||
|
self.isplaying = True
|
||||||
|
self.stop_sent_to_web = False
|
||||||
pbmc_server = window('pms_server')
|
pbmc_server = window('pms_server')
|
||||||
if pbmc_server:
|
if pbmc_server:
|
||||||
(self.protocol, self.server, self.port) = \
|
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
||||||
pbmc_server.split(':')
|
|
||||||
self.server = self.server.replace('/', '')
|
self.server = self.server.replace('/', '')
|
||||||
keyid = None
|
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
||||||
count = 0
|
duration = kodi_time_to_millis(info['totaltime'])
|
||||||
while not keyid:
|
shuffle = '1' if info['shuffled'] else '0'
|
||||||
if count > 300:
|
mute = '1' if info['muted'] is True else '0'
|
||||||
break
|
answ = {
|
||||||
keyid = window('plex_currently_playing_itemid')
|
'location': 'fullScreenVideo',
|
||||||
xbmc.sleep(100)
|
'controllable': CONTROLLABLE[ptype],
|
||||||
count += 1
|
'protocol': self.protocol,
|
||||||
if keyid:
|
'address': self.server,
|
||||||
self.lastkey = "/library/metadata/%s" % keyid
|
'port': self.port,
|
||||||
self.ratingkey = keyid
|
'machineIdentifier': window('plex_machineIdentifier'),
|
||||||
ret += ' key="%s"' % self.lastkey
|
'state': status,
|
||||||
ret += ' ratingKey="%s"' % self.ratingkey
|
'type': ptype,
|
||||||
serv = self.getServerByHost(self.server)
|
'itemType': ptype,
|
||||||
if info.get('playQueueID'):
|
'time': kodi_time_to_millis(info['time']),
|
||||||
self.containerKey = "/playQueues/%s" % info.get('playQueueID')
|
'duration': duration,
|
||||||
ret += ' playQueueID="%s"' % info.get('playQueueID')
|
'seekRange': '0-%s' % duration,
|
||||||
ret += ' playQueueVersion="%s"' % info.get('playQueueVersion')
|
'shuffle': shuffle,
|
||||||
ret += ' playQueueItemID="%s"' % info.get('playQueueItemID')
|
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
|
||||||
ret += ' containerKey="%s"' % self.containerKey
|
'volume': info['volume'],
|
||||||
ret += ' guid="%s"' % info['guid']
|
'mute': mute,
|
||||||
elif keyid:
|
'mediaIndex': 0, # Still to implement from here
|
||||||
self.containerKey = self.lastkey
|
'partIndex':0,
|
||||||
ret += ' containerKey="%s"' % self.containerKey
|
'partCount': 1,
|
||||||
|
'providerIdentifier': 'com.plexapp.plugins.library',
|
||||||
ret += ' duration="%s"' % info['duration']
|
}
|
||||||
ret += ' controllable="%s"' % self.controllable()
|
# Get the plex id from the PKC playqueue not info, as Kodi jumps to next
|
||||||
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
|
# playqueue element way BEFORE kodi monitor onplayback is called
|
||||||
ret += ' protocol="%s"' % serv.get('protocol', "http")
|
if item.plex_id:
|
||||||
ret += ' address="%s"' % serv.get('server', self.server)
|
answ['key'] = '/library/metadata/%s' % item.plex_id
|
||||||
ret += ' port="%s"' % serv.get('port', self.port)
|
answ['ratingKey'] = item.plex_id
|
||||||
ret += ' volume="%s"' % info['volume']
|
# PlayQueue stuff
|
||||||
ret += ' shuffle="%s"' % info['shuffle']
|
if info['container_key']:
|
||||||
ret += ' mute="%s"' % self.mute
|
answ['containerKey'] = info['container_key']
|
||||||
ret += ' repeat="%s"' % info['repeat']
|
if (info['container_key'] is not None and
|
||||||
ret += ' itemType="%s"' % info['itemType']
|
info['container_key'].startswith('/playQueues')):
|
||||||
|
answ['playQueueID'] = playqueue.id
|
||||||
|
answ['playQueueVersion'] = playqueue.version
|
||||||
|
answ['playQueueItemID'] = item.id
|
||||||
|
if playqueue.items[pos].guid:
|
||||||
|
answ['guid'] = item.guid
|
||||||
|
# Temp. token set?
|
||||||
if state.PLEX_TRANSIENT_TOKEN:
|
if state.PLEX_TRANSIENT_TOKEN:
|
||||||
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
|
answ['token'] = state.PLEX_TRANSIENT_TOKEN
|
||||||
elif info['plex_transient_token']:
|
elif playqueue.plex_transient_token:
|
||||||
ret += ' token="%s"' % info['plex_transient_token']
|
answ['token'] = playqueue.plex_transient_token
|
||||||
# Might need an update in the future
|
# Process audio and subtitle streams
|
||||||
if ptype == 'video':
|
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
|
||||||
ret += ' subtitleStreamID="-1"'
|
strm_id = self._plex_stream_index(playerid, 'audio')
|
||||||
ret += ' audioStreamID="-1"'
|
if strm_id:
|
||||||
|
answ['audioStreamID'] = strm_id
|
||||||
|
else:
|
||||||
|
LOG.error('We could not select a Plex audiostream')
|
||||||
|
strm_id = self._plex_stream_index(playerid, 'video')
|
||||||
|
if strm_id:
|
||||||
|
answ['videoStreamID'] = strm_id
|
||||||
|
else:
|
||||||
|
LOG.error('We could not select a Plex videostream')
|
||||||
|
if info['subtitleenabled']:
|
||||||
|
try:
|
||||||
|
strm_id = self._plex_stream_index(playerid, 'subtitle')
|
||||||
|
except KeyError:
|
||||||
|
# subtitleenabled can be True while currentsubtitle can
|
||||||
|
# still be {}
|
||||||
|
strm_id = None
|
||||||
|
if strm_id is not None:
|
||||||
|
# If None, then the subtitle is only present on Kodi side
|
||||||
|
answ['subtitleStreamID'] = strm_id
|
||||||
|
return answ
|
||||||
|
|
||||||
ret += '/>'
|
def signal_stop(self):
|
||||||
return ret
|
"""
|
||||||
|
Externally called on PKC shutdown to ensure that PKC signals a stop to
|
||||||
|
the PMS. Otherwise, PKC might be stuck at "currently playing"
|
||||||
|
"""
|
||||||
|
LOG.info('Signaling a complete stop to PMS')
|
||||||
|
# To avoid RuntimeError, don't use self.lastplayers
|
||||||
|
for playerid in (0, 1, 2):
|
||||||
|
self.last_params['state'] = 'stopped'
|
||||||
|
self._send_pms_notification(playerid, self.last_params)
|
||||||
|
|
||||||
def updateCommandID(self, uuid, commandID):
|
def _plex_stream_index(self, playerid, stream_type):
|
||||||
if commandID and self.subscribers.get(uuid, False):
|
"""
|
||||||
self.subscribers[uuid].commandID = int(commandID)
|
Returns the current Plex stream index [str] for the player playerid
|
||||||
|
|
||||||
def notify(self, event=False):
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
self.cleanup()
|
"""
|
||||||
# Don't tell anyone if we don't know a Plex ID and are still playing
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
# (e.g. no stop called). Used for e.g. PVR/TV without PKC usage
|
info = state.PLAYER_STATES[playerid]
|
||||||
if (not window('plex_currently_playing_itemid')
|
return playqueue.items[info['position']].plex_stream_index(
|
||||||
and not self.lastplayers):
|
info[STREAM_DETAILS[stream_type]]['index'], stream_type)
|
||||||
return True
|
|
||||||
players = self.js.getPlayers()
|
@LOCKER.lockthis
|
||||||
# fetch the message, subscribers or not, since the server
|
def update_command_id(self, uuid, command_id):
|
||||||
# will need the info anyway
|
"""
|
||||||
msg = self.msg(players)
|
Updates the Plex Companien client with the machine identifier uuid with
|
||||||
if self.subscribers:
|
command_id
|
||||||
with threading.RLock():
|
"""
|
||||||
for sub in self.subscribers.values():
|
if command_id and self.subscribers.get(uuid):
|
||||||
sub.send_update(msg, len(players) == 0)
|
self.subscribers[uuid].command_id = int(command_id)
|
||||||
self.notifyServer(players)
|
|
||||||
self.lastplayers = players
|
def _playqueue_init_done(self, players):
|
||||||
|
"""
|
||||||
|
update_player_info() can result in values BEFORE kodi monitor is called.
|
||||||
|
Hence we'd have a missmatch between the state.PLAYER_STATES and our
|
||||||
|
playqueues.
|
||||||
|
"""
|
||||||
|
for player in players.values():
|
||||||
|
info = state.PLAYER_STATES[player['playerid']]
|
||||||
|
playqueue = PQ.PLAYQUEUES[player['playerid']]
|
||||||
|
try:
|
||||||
|
item = playqueue.items[info['position']]
|
||||||
|
except IndexError:
|
||||||
|
# E.g. for direct path playback for single item
|
||||||
|
return False
|
||||||
|
if item.plex_id != info['plex_id']:
|
||||||
|
# Kodi playqueue already progressed; need to wait until
|
||||||
|
# everything is loaded
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def notifyServer(self, players):
|
@LOCKER.lockthis
|
||||||
for typus, p in players.iteritems():
|
def notify(self):
|
||||||
info = self.playerprops[p.get('playerid')]
|
"""
|
||||||
self._sendNotification(info, int(p['playerid']))
|
Causes PKC to tell the PMS and Plex Companion players to receive a
|
||||||
self.lastinfo[typus] = info
|
notification what's being played.
|
||||||
# Cross the one of the list
|
"""
|
||||||
|
self._cleanup()
|
||||||
|
# Get all the active/playing Kodi players (video, audio, pictures)
|
||||||
|
players = js.get_players()
|
||||||
|
# Update the PKC info with what's playing on the Kodi side
|
||||||
|
for player in players.values():
|
||||||
|
update_player_info(player['playerid'])
|
||||||
|
# Check whether we can use the CURRENT info or whether PKC is still
|
||||||
|
# initializing
|
||||||
|
if self._playqueue_init_done(players) is False:
|
||||||
|
LOG.debug('PKC playqueue is still initializing - skipping update')
|
||||||
|
return
|
||||||
|
self._notify_server(players)
|
||||||
|
if self.subscribers:
|
||||||
|
msg = self.msg(players)
|
||||||
|
for subscriber in self.subscribers.values():
|
||||||
|
subscriber.send_update(msg)
|
||||||
|
self.lastplayers = players
|
||||||
|
|
||||||
|
def _notify_server(self, players):
|
||||||
|
for typus, player in players.iteritems():
|
||||||
|
self._send_pms_notification(
|
||||||
|
player['playerid'], self._get_pms_params(player['playerid']))
|
||||||
try:
|
try:
|
||||||
del self.lastplayers[typus]
|
del self.lastplayers[typus]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
# Process the players we have left (to signal a stop)
|
# Process the players we have left (to signal a stop)
|
||||||
for typus, p in self.lastplayers.iteritems():
|
for player in self.lastplayers.values():
|
||||||
self.lastinfo[typus]['state'] = 'stopped'
|
self.last_params['state'] = 'stopped'
|
||||||
self._sendNotification(self.lastinfo[typus], int(p['playerid']))
|
self._send_pms_notification(player['playerid'], self.last_params)
|
||||||
|
|
||||||
def _sendNotification(self, info, playerid):
|
def _get_pms_params(self, playerid):
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
info = state.PLAYER_STATES[playerid]
|
||||||
xargs = getXArgsDeviceInfo()
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
|
try:
|
||||||
|
item = playqueue.items[info['position']]
|
||||||
|
except IndexError:
|
||||||
|
return self.last_params
|
||||||
|
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
||||||
params = {
|
params = {
|
||||||
'containerKey': self.containerKey or "/library/metadata/900000",
|
'state': status,
|
||||||
'key': self.lastkey or "/library/metadata/900000",
|
'ratingKey': item.plex_id,
|
||||||
'ratingKey': self.ratingkey or "900000",
|
'key': '/library/metadata/%s' % item.plex_id,
|
||||||
'state': info['state'],
|
'time': kodi_time_to_millis(info['time']),
|
||||||
'time': info['time'],
|
'duration': kodi_time_to_millis(info['totaltime'])
|
||||||
'duration': info['duration']
|
|
||||||
}
|
}
|
||||||
|
if info['container_key'] is not None:
|
||||||
|
# params['containerKey'] = info['container_key']
|
||||||
|
if info['container_key'].startswith('/playQueues/'):
|
||||||
|
# params['playQueueVersion'] = playqueue.version
|
||||||
|
# params['playQueueID'] = playqueue.id
|
||||||
|
params['playQueueItemID'] = item.id
|
||||||
|
self.last_params = params
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _send_pms_notification(self, playerid, params):
|
||||||
|
serv = self._server_by_host(self.server)
|
||||||
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
|
xargs = params_pms()
|
||||||
|
xargs.update(params)
|
||||||
if state.PLEX_TRANSIENT_TOKEN:
|
if state.PLEX_TRANSIENT_TOKEN:
|
||||||
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
||||||
if info.get('playQueueID'):
|
elif state.PMS_TOKEN:
|
||||||
params['containerKey'] = '/playQueues/%s' % info['playQueueID']
|
xargs['X-Plex-Token'] = state.PMS_TOKEN
|
||||||
params['playQueueVersion'] = info['playQueueVersion']
|
|
||||||
params['playQueueItemID'] = info['playQueueItemID']
|
|
||||||
serv = self.getServerByHost(self.server)
|
|
||||||
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
||||||
serv.get('server', 'localhost'),
|
serv.get('server', 'localhost'),
|
||||||
serv.get('port', '32400'))
|
serv.get('port', '32400'))
|
||||||
self.doUtils(url, parameters=params, headerOptions=xargs)
|
DU().downloadUrl(url,
|
||||||
log.debug("Sent server notification with parameters: %s to %s"
|
authenticate=False,
|
||||||
% (params, url))
|
parameters=xargs,
|
||||||
|
headerOverride=HEADERS_PMS)
|
||||||
|
LOG.debug("Sent server notification with parameters: %s to %s",
|
||||||
|
xargs, url)
|
||||||
|
|
||||||
def controllable(self):
|
@LOCKER.lockthis
|
||||||
return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause"
|
def add_subscriber(self, protocol, host, port, uuid, command_id):
|
||||||
|
"""
|
||||||
|
Adds a new Plex Companion subscriber to PKC.
|
||||||
|
"""
|
||||||
|
subscriber = Subscriber(protocol,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
uuid,
|
||||||
|
command_id,
|
||||||
|
self,
|
||||||
|
self.request_mgr)
|
||||||
|
self.subscribers[subscriber.uuid] = subscriber
|
||||||
|
return subscriber
|
||||||
|
|
||||||
def addSubscriber(self, protocol, host, port, uuid, commandID):
|
@LOCKER.lockthis
|
||||||
sub = Subscriber(protocol,
|
def remove_subscriber(self, uuid):
|
||||||
host,
|
"""
|
||||||
port,
|
Removes a connected Plex Companion subscriber with machine identifier
|
||||||
uuid,
|
uuid from PKC notifications.
|
||||||
commandID,
|
(Calls the cleanup() method of the subscriber)
|
||||||
self,
|
"""
|
||||||
self.RequestMgr)
|
for subscriber in self.subscribers.values():
|
||||||
with threading.RLock():
|
if subscriber.uuid == uuid or subscriber.host == uuid:
|
||||||
self.subscribers[sub.uuid] = sub
|
subscriber.cleanup()
|
||||||
return sub
|
del self.subscribers[subscriber.uuid]
|
||||||
|
|
||||||
def removeSubscriber(self, uuid):
|
def _cleanup(self):
|
||||||
with threading.RLock():
|
for subscriber in self.subscribers.values():
|
||||||
for sub in self.subscribers.values():
|
if subscriber.age > 30:
|
||||||
if sub.uuid == uuid or sub.host == uuid:
|
subscriber.cleanup()
|
||||||
sub.cleanup()
|
del self.subscribers[subscriber.uuid]
|
||||||
del self.subscribers[sub.uuid]
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
with threading.RLock():
|
|
||||||
for sub in self.subscribers.values():
|
|
||||||
if sub.age > 30:
|
|
||||||
sub.cleanup()
|
|
||||||
del self.subscribers[sub.uuid]
|
|
||||||
|
|
||||||
def getPlayerProperties(self, playerid):
|
|
||||||
try:
|
|
||||||
# Get the playqueue
|
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
|
||||||
# get info from the player
|
|
||||||
props = self.js.jsonrpc(
|
|
||||||
"Player.GetProperties",
|
|
||||||
{"playerid": playerid,
|
|
||||||
"properties": ["type",
|
|
||||||
"time",
|
|
||||||
"totaltime",
|
|
||||||
"speed",
|
|
||||||
"shuffled",
|
|
||||||
"repeat"]})
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'time': timeToMillis(props['time']),
|
|
||||||
'duration': timeToMillis(props['totaltime']),
|
|
||||||
'state': ("paused", "playing")[int(props['speed'])],
|
|
||||||
'shuffle': ("0", "1")[props.get('shuffled', False)],
|
|
||||||
'repeat': pf.getPlexRepeat(props.get('repeat')),
|
|
||||||
}
|
|
||||||
# Get the playlist position
|
|
||||||
pos = self.js.jsonrpc(
|
|
||||||
"Player.GetProperties",
|
|
||||||
{"playerid": playerid,
|
|
||||||
"properties": ["position"]})['position']
|
|
||||||
try:
|
|
||||||
info['playQueueItemID'] = playqueue.items[pos].ID or 'null'
|
|
||||||
info['guid'] = playqueue.items[pos].guid or 'null'
|
|
||||||
info['playQueueID'] = playqueue.ID or 'null'
|
|
||||||
info['playQueueVersion'] = playqueue.version or 'null'
|
|
||||||
info['itemType'] = playqueue.items[pos].plex_type or 'null'
|
|
||||||
except:
|
|
||||||
info['itemType'] = props.get('type') or 'null'
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
log.error("Traceback:\n%s" % traceback.format_exc())
|
|
||||||
info = {
|
|
||||||
'time': 0,
|
|
||||||
'duration': 0,
|
|
||||||
'state': 'stopped',
|
|
||||||
'shuffle': False,
|
|
||||||
'repeat': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# get the volume from the application
|
|
||||||
info['volume'] = self.volume
|
|
||||||
info['mute'] = self.mute
|
|
||||||
|
|
||||||
info['plex_transient_token'] = playqueue.plex_transient_token
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriber:
|
class Subscriber(object):
|
||||||
def __init__(self, protocol, host, port, uuid, commandID,
|
"""
|
||||||
subMgr, RequestMgr):
|
Plex Companion subscribing device
|
||||||
|
"""
|
||||||
|
def __init__(self, protocol, host, port, uuid, command_id, sub_mgr,
|
||||||
|
request_mgr):
|
||||||
self.protocol = protocol or "http"
|
self.protocol = protocol or "http"
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port or 32400
|
self.port = port or 32400
|
||||||
self.uuid = uuid or host
|
self.uuid = uuid or host
|
||||||
self.commandID = int(commandID) or 0
|
self.command_id = int(command_id) or 0
|
||||||
self.navlocationsent = False
|
|
||||||
self.age = 0
|
self.age = 0
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.sub_mgr = sub_mgr
|
||||||
self.subMgr = subMgr
|
self.request_mgr = request_mgr
|
||||||
self.RequestMgr = RequestMgr
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.uuid == other.uuid
|
return self.uuid == other.uuid
|
||||||
|
|
||||||
def tostr(self):
|
|
||||||
return "uuid=%s,commandID=%i" % (self.uuid, self.commandID)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.RequestMgr.closeConnection(self.protocol, self.host, self.port)
|
|
||||||
|
|
||||||
def send_update(self, msg, is_nav):
|
|
||||||
self.age += 1
|
|
||||||
if not is_nav:
|
|
||||||
self.navlocationsent = False
|
|
||||||
elif self.navlocationsent:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.navlocationsent = True
|
|
||||||
msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg)
|
|
||||||
log.debug("sending xml to subscriber %s:\n%s" % (self.tostr(), msg))
|
|
||||||
url = self.protocol + '://' + self.host + ':' + self.port \
|
|
||||||
+ "/:/timeline"
|
|
||||||
t = threading.Thread(target=self.threadedSend, args=(url, msg))
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def threadedSend(self, url, msg):
|
|
||||||
"""
|
"""
|
||||||
Threaded POST request, because they stall due to PMS response missing
|
Closes the connection to the Plex Companion client
|
||||||
|
"""
|
||||||
|
self.request_mgr.closeConnection(self.protocol, self.host, self.port)
|
||||||
|
|
||||||
|
def send_update(self, msg):
|
||||||
|
"""
|
||||||
|
Sends msg to the Plex Companion client (via .../:/timeline)
|
||||||
|
"""
|
||||||
|
self.age += 1
|
||||||
|
msg = msg.format(command_id=self.command_id)
|
||||||
|
LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
|
||||||
|
self.uuid, self.command_id, msg)
|
||||||
|
url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port)
|
||||||
|
thread = Thread(target=self._threaded_send, args=(url, msg))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _threaded_send(self, url, msg):
|
||||||
|
"""
|
||||||
|
Threaded POST request, because they stall due to response missing
|
||||||
the Content-Length header :-(
|
the Content-Length header :-(
|
||||||
"""
|
"""
|
||||||
response = self.doUtils(url,
|
response = DU().downloadUrl(url,
|
||||||
postBody=msg,
|
action_type="POST",
|
||||||
action_type="POST")
|
postBody=msg,
|
||||||
if response in [False, None, 401]:
|
authenticate=False,
|
||||||
self.subMgr.removeSubscriber(self.uuid)
|
headerOverride=headers_companion_client())
|
||||||
|
if response in (False, None, 401):
|
||||||
|
self.sub_mgr.remove_subscriber(self.uuid)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from utils import kodiSQL
|
from utils import kodi_sql
|
||||||
import logging
|
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class Get_Plex_DB():
|
||||||
and the db gets closed
|
and the db gets closed
|
||||||
"""
|
"""
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.plexconn = kodiSQL('plex')
|
self.plexconn = kodi_sql('plex')
|
||||||
return Plex_DB_Functions(self.plexconn.cursor())
|
return Plex_DB_Functions(self.plexconn.cursor())
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
|
@ -220,17 +220,13 @@ class Plex_DB_Functions():
|
||||||
None if not found
|
None if not found
|
||||||
"""
|
"""
|
||||||
query = '''
|
query = '''
|
||||||
SELECT kodi_id, kodi_fileid, kodi_pathid,
|
SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type,
|
||||||
parent_id, kodi_type, plex_type
|
plex_type
|
||||||
FROM plex
|
FROM plex WHERE plex_id = ?
|
||||||
WHERE plex_id = ?
|
LIMIT 1
|
||||||
'''
|
'''
|
||||||
try:
|
self.plexcursor.execute(query, (plex_id,))
|
||||||
self.plexcursor.execute(query, (plex_id,))
|
return self.plexcursor.fetchone()
|
||||||
item = self.plexcursor.fetchone()
|
|
||||||
return item
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getItem_byWildId(self, plex_id):
|
def getItem_byWildId(self, plex_id):
|
||||||
"""
|
"""
|
||||||
|
@ -272,14 +268,13 @@ class Plex_DB_Functions():
|
||||||
|
|
||||||
def getItem_byParentId(self, parent_id, kodi_type):
|
def getItem_byParentId(self, parent_id, kodi_type):
|
||||||
"""
|
"""
|
||||||
Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id,
|
Returns a list of tuples (plex_id, kodi_id, kodi_fileid) for parent_id,
|
||||||
kodi_type
|
kodi_type
|
||||||
"""
|
"""
|
||||||
query = '''
|
query = '''
|
||||||
SELECT plex_id, kodi_id, kodi_fileid
|
SELECT plex_id, kodi_id, kodi_fileid
|
||||||
FROM plex
|
FROM plex
|
||||||
WHERE parent_id = ?
|
WHERE parent_id = ? AND kodi_type = ?
|
||||||
AND kodi_type = ?"
|
|
||||||
'''
|
'''
|
||||||
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
||||||
return self.plexcursor.fetchall()
|
return self.plexcursor.fetchall()
|
||||||
|
@ -297,7 +292,7 @@ class Plex_DB_Functions():
|
||||||
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
self.plexcursor.execute(query, (parent_id, kodi_type,))
|
||||||
return self.plexcursor.fetchall()
|
return self.plexcursor.fetchall()
|
||||||
|
|
||||||
def getChecksum(self, plex_type):
|
def checksum(self, plex_type):
|
||||||
"""
|
"""
|
||||||
Returns a list of tuples (plex_id, checksum) for plex_type
|
Returns a list of tuples (plex_id, checksum) for plex_type
|
||||||
"""
|
"""
|
||||||
|
@ -383,8 +378,8 @@ class Plex_DB_Functions():
|
||||||
"""
|
"""
|
||||||
Removes the one entry with plex_id
|
Removes the one entry with plex_id
|
||||||
"""
|
"""
|
||||||
query = "DELETE FROM plex WHERE plex_id = ?"
|
self.plexcursor.execute('DELETE FROM plex WHERE plex_id = ?',
|
||||||
self.plexcursor.execute(query, (plex_id,))
|
(plex_id,))
|
||||||
|
|
||||||
def removeWildItem(self, plex_id):
|
def removeWildItem(self, plex_id):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -28,6 +28,10 @@ DIRECT_PATHS = False
|
||||||
INDICATE_MEDIA_VERSIONS = False
|
INDICATE_MEDIA_VERSIONS = False
|
||||||
# Do we need to run a special library scan?
|
# Do we need to run a special library scan?
|
||||||
RUN_LIB_SCAN = None
|
RUN_LIB_SCAN = None
|
||||||
|
# Number of items to fetch and display in widgets
|
||||||
|
FETCH_PMS_ITEM_NUMBER = None
|
||||||
|
# Hack to force Kodi widget for "in progress" to show up if it was empty before
|
||||||
|
FORCE_RELOAD_SKIN = True
|
||||||
|
|
||||||
# Stemming from the PKC settings.xml
|
# Stemming from the PKC settings.xml
|
||||||
# Shall we show Kodi dialogs when synching?
|
# Shall we show Kodi dialogs when synching?
|
||||||
|
@ -38,8 +42,8 @@ KODI_DB_CHECKED = False
|
||||||
ENABLE_MUSIC = True
|
ENABLE_MUSIC = True
|
||||||
# How often shall we sync?
|
# How often shall we sync?
|
||||||
FULL_SYNC_INTERVALL = 0
|
FULL_SYNC_INTERVALL = 0
|
||||||
# Background Sync enabled at all?
|
# Background Sync disabled?
|
||||||
BACKGROUND_SYNC = True
|
BACKGROUND_SYNC_DISABLED = False
|
||||||
# How long shall we wait with synching a new item to make sure Plex got all
|
# How long shall we wait with synching a new item to make sure Plex got all
|
||||||
# metadata?
|
# metadata?
|
||||||
BACKGROUNDSYNC_SAFTYMARGIN = 0
|
BACKGROUNDSYNC_SAFTYMARGIN = 0
|
||||||
|
@ -63,14 +67,93 @@ remapSMBmusicNew = None
|
||||||
remapSMBphotoOrg = None
|
remapSMBphotoOrg = None
|
||||||
remapSMBphotoNew = None
|
remapSMBphotoNew = None
|
||||||
|
|
||||||
|
# Shall we verify SSL certificates?
|
||||||
|
VERIFY_SSL_CERT = False
|
||||||
|
# Do we have an ssl certificate for PKC we need to use?
|
||||||
|
SSL_CERT_PATH = None
|
||||||
# Along with window('plex_authenticated')
|
# Along with window('plex_authenticated')
|
||||||
AUTHENTICATED = False
|
AUTHENTICATED = False
|
||||||
# plex.tv username
|
# plex.tv username
|
||||||
PLEX_USERNAME = None
|
PLEX_USERNAME = None
|
||||||
# Token for that user for plex.tv
|
# Token for that user for plex.tv
|
||||||
PLEX_TOKEN = None
|
PLEX_TOKEN = None
|
||||||
|
# Plex token for the active PMS for the active user
|
||||||
|
# (might be diffent to PLEX_TOKEN)
|
||||||
|
PMS_TOKEN = None
|
||||||
# Plex ID of that user (e.g. for plex.tv) as a STRING
|
# Plex ID of that user (e.g. for plex.tv) as a STRING
|
||||||
PLEX_USER_ID = None
|
PLEX_USER_ID = None
|
||||||
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
||||||
# another user playing something! Token identifies user
|
# another user playing something! Token identifies user
|
||||||
PLEX_TRANSIENT_TOKEN = None
|
PLEX_TRANSIENT_TOKEN = None
|
||||||
|
|
||||||
|
# Plex Companion Queue()
|
||||||
|
COMPANION_QUEUE = None
|
||||||
|
# Command Pipeline Queue()
|
||||||
|
COMMAND_PIPELINE_QUEUE = None
|
||||||
|
# Websocket_client queue to communicate with librarysync
|
||||||
|
WEBSOCKET_QUEUE = None
|
||||||
|
|
||||||
|
# Which Kodi player is/has been active? (either int 1, 2 or 3)
|
||||||
|
ACTIVE_PLAYERS = []
|
||||||
|
# Failsafe for throwing failing ListItems() back to Kodi's setResolvedUrl
|
||||||
|
PKC_CAUSED_STOP = False
|
||||||
|
|
||||||
|
# Kodi player states - here, initial values are set
|
||||||
|
PLAYER_STATES = {
|
||||||
|
0: {},
|
||||||
|
1: {},
|
||||||
|
2: {}
|
||||||
|
}
|
||||||
|
# The LAST playstate once playback is finished
|
||||||
|
OLD_PLAYER_STATES = {
|
||||||
|
0: {},
|
||||||
|
1: {},
|
||||||
|
2: {}
|
||||||
|
}
|
||||||
|
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
|
||||||
|
PLAYSTATE = {
|
||||||
|
'type': None,
|
||||||
|
'time': {
|
||||||
|
'hours': 0,
|
||||||
|
'minutes': 0,
|
||||||
|
'seconds': 0,
|
||||||
|
'milliseconds': 0},
|
||||||
|
'totaltime': {
|
||||||
|
'hours': 0,
|
||||||
|
'minutes': 0,
|
||||||
|
'seconds': 0,
|
||||||
|
'milliseconds': 0},
|
||||||
|
'speed': 0,
|
||||||
|
'shuffled': False,
|
||||||
|
'repeat': 'off',
|
||||||
|
'position': None,
|
||||||
|
'playlistid': None,
|
||||||
|
'currentvideostream': -1,
|
||||||
|
'currentaudiostream': -1,
|
||||||
|
'subtitleenabled': False,
|
||||||
|
'currentsubtitle': -1,
|
||||||
|
'file': None,
|
||||||
|
'kodi_id': None,
|
||||||
|
'kodi_type': None,
|
||||||
|
'plex_id': None,
|
||||||
|
'plex_type': None,
|
||||||
|
'container_key': None,
|
||||||
|
'volume': 100,
|
||||||
|
'muted': False,
|
||||||
|
'playmethod': None,
|
||||||
|
'playcount': None
|
||||||
|
}
|
||||||
|
PLAYED_INFO = {}
|
||||||
|
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
||||||
|
# beginning?
|
||||||
|
RESUME_PLAYBACK = False
|
||||||
|
# Was the playback initiated by the user using the Kodi context menu?
|
||||||
|
CONTEXT_MENU_PLAY = False
|
||||||
|
# Set by context menu - shall we force-transcode the next playing item?
|
||||||
|
FORCE_TRANSCODE = False
|
||||||
|
|
||||||
|
# Kodi webserver details
|
||||||
|
WEBSERVER_PORT = 8080
|
||||||
|
WEBSERVER_USERNAME = 'kodi'
|
||||||
|
WEBSERVER_PASSWORD = ''
|
||||||
|
WEBSERVER_HOST = 'localhost'
|
||||||
|
|
|
@ -1,39 +1,33 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
import threading
|
from threading import Thread
|
||||||
|
|
||||||
import xbmc
|
from xbmc import sleep, executebuiltin, translatePath
|
||||||
import xbmcgui
|
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
from xbmcvfs import exists
|
from xbmcvfs import exists
|
||||||
|
|
||||||
|
from utils import window, settings, language as lang, thread_methods, dialog
|
||||||
from utils import window, settings, language as lang, thread_methods
|
from downloadutils import DownloadUtils as DU
|
||||||
import downloadutils
|
import plex_tv
|
||||||
|
import PlexFunctions as PF
|
||||||
import PlexAPI
|
|
||||||
from PlexFunctions import GetMachineIdentifier
|
|
||||||
import state
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
|
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
|
||||||
class UserClient(threading.Thread):
|
class UserClient(Thread):
|
||||||
|
|
||||||
# Borg - multiple instances, shared state
|
# Borg - multiple instances, shared state
|
||||||
__shared_state = {}
|
__shared_state = {}
|
||||||
|
|
||||||
def __init__(self, callback=None):
|
def __init__(self):
|
||||||
self.__dict__ = self.__shared_state
|
self.__dict__ = self.__shared_state
|
||||||
if callback is not None:
|
|
||||||
self.mgr = callback
|
|
||||||
|
|
||||||
self.auth = True
|
self.auth = True
|
||||||
self.retry = 0
|
self.retry = 0
|
||||||
|
@ -47,9 +41,9 @@ class UserClient(threading.Thread):
|
||||||
self.userSettings = None
|
self.userSettings = None
|
||||||
|
|
||||||
self.addon = xbmcaddon.Addon()
|
self.addon = xbmcaddon.Addon()
|
||||||
self.doUtils = downloadutils.DownloadUtils()
|
self.doUtils = DU()
|
||||||
|
|
||||||
threading.Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def getUsername(self):
|
def getUsername(self):
|
||||||
"""
|
"""
|
||||||
|
@ -57,10 +51,10 @@ class UserClient(threading.Thread):
|
||||||
"""
|
"""
|
||||||
username = settings('username')
|
username = settings('username')
|
||||||
if not username:
|
if not username:
|
||||||
log.debug("No username saved, trying to get Plex username")
|
LOG.debug("No username saved, trying to get Plex username")
|
||||||
username = settings('plexLogin')
|
username = settings('plexLogin')
|
||||||
if not username:
|
if not username:
|
||||||
log.debug("Also no Plex username found")
|
LOG.debug("Also no Plex username found")
|
||||||
return ""
|
return ""
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
@ -75,7 +69,7 @@ class UserClient(threading.Thread):
|
||||||
server = host + ":" + port
|
server = host + ":" + port
|
||||||
|
|
||||||
if not host:
|
if not host:
|
||||||
log.debug("No server information saved.")
|
LOG.debug("No server information saved.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If https is true
|
# If https is true
|
||||||
|
@ -86,11 +80,11 @@ class UserClient(threading.Thread):
|
||||||
server = "http://%s" % server
|
server = "http://%s" % server
|
||||||
# User entered IP; we need to get the machineIdentifier
|
# User entered IP; we need to get the machineIdentifier
|
||||||
if self.machineIdentifier == '' and prefix is True:
|
if self.machineIdentifier == '' and prefix is True:
|
||||||
self.machineIdentifier = GetMachineIdentifier(server)
|
self.machineIdentifier = PF.GetMachineIdentifier(server)
|
||||||
if self.machineIdentifier is None:
|
if self.machineIdentifier is None:
|
||||||
self.machineIdentifier = ''
|
self.machineIdentifier = ''
|
||||||
settings('plex_machineIdentifier', value=self.machineIdentifier)
|
settings('plex_machineIdentifier', value=self.machineIdentifier)
|
||||||
log.debug('Returning active server: %s' % server)
|
LOG.debug('Returning active server: %s', server)
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def getSSLverify(self):
|
def getSSLverify(self):
|
||||||
|
@ -103,10 +97,10 @@ class UserClient(threading.Thread):
|
||||||
else settings('sslcert')
|
else settings('sslcert')
|
||||||
|
|
||||||
def setUserPref(self):
|
def setUserPref(self):
|
||||||
log.debug('Setting user preferences')
|
LOG.debug('Setting user preferences')
|
||||||
# Only try to get user avatar if there is a token
|
# Only try to get user avatar if there is a token
|
||||||
if self.currToken:
|
if self.currToken:
|
||||||
url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser)
|
url = PF.GetUserArtworkURL(self.currUser)
|
||||||
if url:
|
if url:
|
||||||
window('PlexUserImage', value=url)
|
window('PlexUserImage', value=url)
|
||||||
# Set resume point max
|
# Set resume point max
|
||||||
|
@ -118,7 +112,7 @@ class UserClient(threading.Thread):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def loadCurrUser(self, username, userId, usertoken, authenticated=False):
|
def loadCurrUser(self, username, userId, usertoken, authenticated=False):
|
||||||
log.debug('Loading current user')
|
LOG.debug('Loading current user')
|
||||||
doUtils = self.doUtils
|
doUtils = self.doUtils
|
||||||
|
|
||||||
self.currToken = usertoken
|
self.currToken = usertoken
|
||||||
|
@ -129,25 +123,26 @@ class UserClient(threading.Thread):
|
||||||
if authenticated is False:
|
if authenticated is False:
|
||||||
if self.currServer is None:
|
if self.currServer is None:
|
||||||
return False
|
return False
|
||||||
log.debug('Testing validity of current token')
|
LOG.debug('Testing validity of current token')
|
||||||
res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
|
res = PF.check_connection(self.currServer,
|
||||||
token=self.currToken,
|
token=self.currToken,
|
||||||
verifySSL=self.ssl)
|
verifySSL=self.ssl)
|
||||||
if res is False:
|
if res is False:
|
||||||
# PMS probably offline
|
# PMS probably offline
|
||||||
return False
|
return False
|
||||||
elif res == 401:
|
elif res == 401:
|
||||||
log.error('Token is no longer valid')
|
LOG.error('Token is no longer valid')
|
||||||
return 401
|
return 401
|
||||||
elif res >= 400:
|
elif res >= 400:
|
||||||
log.error('Answer from PMS is not as expected. Retrying')
|
LOG.error('Answer from PMS is not as expected. Retrying')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Set to windows property
|
# Set to windows property
|
||||||
state.PLEX_USER_ID = userId or None
|
state.PLEX_USER_ID = userId or None
|
||||||
state.PLEX_USERNAME = username
|
state.PLEX_USERNAME = username
|
||||||
# This is the token for the current PMS (might also be '')
|
# This is the token for the current PMS (might also be '')
|
||||||
window('pms_token', value=self.currToken)
|
window('pms_token', value=usertoken)
|
||||||
|
state.PMS_TOKEN = usertoken
|
||||||
# This is the token for plex.tv for the current user
|
# This is the token for plex.tv for the current user
|
||||||
# Is only '' if user is not signed in to plex.tv
|
# Is only '' if user is not signed in to plex.tv
|
||||||
window('plex_token', value=settings('plexToken'))
|
window('plex_token', value=settings('plexToken'))
|
||||||
|
@ -184,31 +179,29 @@ class UserClient(threading.Thread):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
log.debug('Authenticating user')
|
LOG.debug('Authenticating user')
|
||||||
dialog = xbmcgui.Dialog()
|
|
||||||
|
|
||||||
# Give attempts at entering password / selecting user
|
# Give attempts at entering password / selecting user
|
||||||
if self.retry >= 2:
|
if self.retry >= 2:
|
||||||
log.error("Too many retries to login.")
|
LOG.error("Too many retries to login.")
|
||||||
state.PMS_STATUS = 'Stop'
|
state.PMS_STATUS = 'Stop'
|
||||||
dialog.ok(lang(33001),
|
dialog('ok', lang(33001), lang(39023))
|
||||||
lang(39023))
|
executebuiltin(
|
||||||
xbmc.executebuiltin(
|
|
||||||
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get /profile/addon_data
|
# Get /profile/addon_data
|
||||||
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile'))
|
addondir = translatePath(self.addon.getAddonInfo('profile'))
|
||||||
|
|
||||||
# If there's no settings.xml
|
# If there's no settings.xml
|
||||||
if not exists("%ssettings.xml" % addondir):
|
if not exists("%ssettings.xml" % addondir):
|
||||||
log.error("Error, no settings.xml found.")
|
LOG.error("Error, no settings.xml found.")
|
||||||
self.auth = False
|
self.auth = False
|
||||||
return False
|
return False
|
||||||
server = self.getServer()
|
server = self.getServer()
|
||||||
# If there is no server we can connect to
|
# If there is no server we can connect to
|
||||||
if not server:
|
if not server:
|
||||||
log.info("Missing server information.")
|
LOG.info("Missing server information.")
|
||||||
self.auth = False
|
self.auth = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -219,7 +212,7 @@ class UserClient(threading.Thread):
|
||||||
enforceLogin = settings('enforceUserLogin')
|
enforceLogin = settings('enforceUserLogin')
|
||||||
# Found a user in the settings, try to authenticate
|
# Found a user in the settings, try to authenticate
|
||||||
if username and enforceLogin == 'false':
|
if username and enforceLogin == 'false':
|
||||||
log.debug('Trying to authenticate with old settings')
|
LOG.debug('Trying to authenticate with old settings')
|
||||||
answ = self.loadCurrUser(username,
|
answ = self.loadCurrUser(username,
|
||||||
userId,
|
userId,
|
||||||
usertoken,
|
usertoken,
|
||||||
|
@ -228,21 +221,19 @@ class UserClient(threading.Thread):
|
||||||
# SUCCESS: loaded a user from the settings
|
# SUCCESS: loaded a user from the settings
|
||||||
return True
|
return True
|
||||||
elif answ == 401:
|
elif answ == 401:
|
||||||
log.error("User token no longer valid. Sign user out")
|
LOG.error("User token no longer valid. Sign user out")
|
||||||
settings('username', value='')
|
settings('username', value='')
|
||||||
settings('userid', value='')
|
settings('userid', value='')
|
||||||
settings('accessToken', value='')
|
settings('accessToken', value='')
|
||||||
else:
|
else:
|
||||||
log.debug("Could not yet authenticate user")
|
LOG.debug("Could not yet authenticate user")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
plx = PlexAPI.PlexAPI()
|
|
||||||
|
|
||||||
# Could not use settings - try to get Plex user list from plex.tv
|
# Could not use settings - try to get Plex user list from plex.tv
|
||||||
plextoken = settings('plexToken')
|
plextoken = settings('plexToken')
|
||||||
if plextoken:
|
if plextoken:
|
||||||
log.info("Trying to connect to plex.tv to get a user list")
|
LOG.info("Trying to connect to plex.tv to get a user list")
|
||||||
userInfo = plx.ChoosePlexHomeUser(plextoken)
|
userInfo = plex_tv.choose_home_user(plextoken)
|
||||||
if userInfo is False:
|
if userInfo is False:
|
||||||
# FAILURE: Something went wrong, try again
|
# FAILURE: Something went wrong, try again
|
||||||
self.auth = True
|
self.auth = True
|
||||||
|
@ -252,7 +243,7 @@ class UserClient(threading.Thread):
|
||||||
userId = userInfo['userid']
|
userId = userInfo['userid']
|
||||||
usertoken = userInfo['token']
|
usertoken = userInfo['token']
|
||||||
else:
|
else:
|
||||||
log.info("Trying to authenticate without a token")
|
LOG.info("Trying to authenticate without a token")
|
||||||
username = ''
|
username = ''
|
||||||
userId = ''
|
userId = ''
|
||||||
usertoken = ''
|
usertoken = ''
|
||||||
|
@ -260,20 +251,21 @@ class UserClient(threading.Thread):
|
||||||
if self.loadCurrUser(username, userId, usertoken, authenticated=False):
|
if self.loadCurrUser(username, userId, usertoken, authenticated=False):
|
||||||
# SUCCESS: loaded a user from the settings
|
# SUCCESS: loaded a user from the settings
|
||||||
return True
|
return True
|
||||||
else:
|
# Something went wrong, try again
|
||||||
# FAILUR: Something went wrong, try again
|
self.auth = True
|
||||||
self.auth = True
|
self.retry += 1
|
||||||
self.retry += 1
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
def resetClient(self):
|
def resetClient(self):
|
||||||
log.debug("Reset UserClient authentication.")
|
LOG.debug("Reset UserClient authentication.")
|
||||||
self.doUtils.stopSession()
|
self.doUtils.stopSession()
|
||||||
|
|
||||||
window('plex_authenticated', clear=True)
|
window('plex_authenticated', clear=True)
|
||||||
state.AUTHENTICATED = False
|
state.AUTHENTICATED = False
|
||||||
window('pms_token', clear=True)
|
window('pms_token', clear=True)
|
||||||
state.PLEX_TOKEN = None
|
state.PLEX_TOKEN = None
|
||||||
|
state.PLEX_TRANSIENT_TOKEN = None
|
||||||
|
state.PMS_TOKEN = None
|
||||||
window('plex_token', clear=True)
|
window('plex_token', clear=True)
|
||||||
window('pms_server', clear=True)
|
window('pms_server', clear=True)
|
||||||
window('plex_machineIdentifier', clear=True)
|
window('plex_machineIdentifier', clear=True)
|
||||||
|
@ -287,9 +279,6 @@ class UserClient(threading.Thread):
|
||||||
settings('userid', value='')
|
settings('userid', value='')
|
||||||
settings('accessToken', value='')
|
settings('accessToken', value='')
|
||||||
|
|
||||||
# Reset token in downloads
|
|
||||||
self.doUtils.setToken('')
|
|
||||||
|
|
||||||
self.currToken = None
|
self.currToken = None
|
||||||
self.auth = True
|
self.auth = True
|
||||||
self.currUser = None
|
self.currUser = None
|
||||||
|
@ -297,17 +286,17 @@ class UserClient(threading.Thread):
|
||||||
self.retry = 0
|
self.retry = 0
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
log.info("----===## Starting UserClient ##===----")
|
LOG.info("----===## Starting UserClient ##===----")
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
break
|
break
|
||||||
xbmc.sleep(1000)
|
sleep(1000)
|
||||||
|
|
||||||
if state.PMS_STATUS == "Stop":
|
if state.PMS_STATUS == "Stop":
|
||||||
xbmc.sleep(500)
|
sleep(500)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Verify the connection status to server
|
# Verify the connection status to server
|
||||||
|
@ -320,7 +309,7 @@ class UserClient(threading.Thread):
|
||||||
state.PMS_STATUS = 'Auth'
|
state.PMS_STATUS = 'Auth'
|
||||||
window('plex_serverStatus', value='Auth')
|
window('plex_serverStatus', value='Auth')
|
||||||
self.resetClient()
|
self.resetClient()
|
||||||
xbmc.sleep(3000)
|
sleep(3000)
|
||||||
|
|
||||||
if self.auth and (self.currUser is None):
|
if self.auth and (self.currUser is None):
|
||||||
# Try to authenticate user
|
# Try to authenticate user
|
||||||
|
@ -330,9 +319,9 @@ class UserClient(threading.Thread):
|
||||||
self.auth = False
|
self.auth = False
|
||||||
if self.authenticate():
|
if self.authenticate():
|
||||||
# Successfully authenticated and loaded a user
|
# Successfully authenticated and loaded a user
|
||||||
log.info("Successfully authenticated!")
|
LOG.info("Successfully authenticated!")
|
||||||
log.info("Current user: %s" % self.currUser)
|
LOG.info("Current user: %s", self.currUser)
|
||||||
log.info("Current userId: %s" % state.PLEX_USER_ID)
|
LOG.info("Current userId: %s", state.PLEX_USER_ID)
|
||||||
self.retry = 0
|
self.retry = 0
|
||||||
state.SUSPEND_LIBRARY_THREAD = False
|
state.SUSPEND_LIBRARY_THREAD = False
|
||||||
window('plex_serverStatus', clear=True)
|
window('plex_serverStatus', clear=True)
|
||||||
|
@ -346,10 +335,10 @@ class UserClient(threading.Thread):
|
||||||
# Or retried too many times
|
# Or retried too many times
|
||||||
if server and state.PMS_STATUS != "Stop":
|
if server and state.PMS_STATUS != "Stop":
|
||||||
# Only if there's information found to login
|
# Only if there's information found to login
|
||||||
log.debug("Server found: %s" % server)
|
LOG.debug("Server found: %s", server)
|
||||||
self.auth = True
|
self.auth = True
|
||||||
|
|
||||||
# Minimize CPU load
|
# Minimize CPU load
|
||||||
xbmc.sleep(100)
|
sleep(100)
|
||||||
|
|
||||||
log.info("##===---- UserClient Stopped ----===##")
|
LOG.info("##===---- UserClient Stopped ----===##")
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ from xbmcaddon import Addon
|
||||||
# For any file operations with KODI function, use encoded strings!
|
# For any file operations with KODI function, use encoded strings!
|
||||||
|
|
||||||
|
|
||||||
def tryDecode(string, encoding='utf-8'):
|
def try_decode(string, encoding='utf-8'):
|
||||||
"""
|
"""
|
||||||
Will try to decode string (encoded) using encoding. This possibly
|
Will try to decode string (encoded) using encoding. This possibly
|
||||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||||
|
@ -22,15 +22,23 @@ def tryDecode(string, encoding='utf-8'):
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
# Percent of playback progress for watching item as partially watched. Anything
|
||||||
|
# more and item will NOT be marked as partially, but fully watched
|
||||||
|
MARK_PLAYED_AT = 0.9
|
||||||
|
# How many seconds of playback do we ignore before marking an item as partially
|
||||||
|
# watched?
|
||||||
|
IGNORE_SECONDS_AT_START = 60
|
||||||
|
|
||||||
_ADDON = Addon()
|
_ADDON = Addon()
|
||||||
ADDON_NAME = 'PlexKodiConnect'
|
ADDON_NAME = 'PlexKodiConnect'
|
||||||
ADDON_ID = 'plugin.video.plexkodiconnect'
|
ADDON_ID = 'plugin.video.plexkodiconnect'
|
||||||
ADDON_VERSION = _ADDON.getAddonInfo('version')
|
ADDON_VERSION = _ADDON.getAddonInfo('version')
|
||||||
|
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
|
||||||
|
|
||||||
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
||||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
||||||
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
|
KODI_PROFILE = try_decode(xbmc.translatePath("special://profile"))
|
||||||
|
|
||||||
if xbmc.getCondVisibility('system.platform.osx'):
|
if xbmc.getCondVisibility('system.platform.osx'):
|
||||||
PLATFORM = "MacOSX"
|
PLATFORM = "MacOSX"
|
||||||
|
@ -49,7 +57,7 @@ elif xbmc.getCondVisibility('system.platform.android'):
|
||||||
else:
|
else:
|
||||||
PLATFORM = "Unknown"
|
PLATFORM = "Unknown"
|
||||||
|
|
||||||
DEVICENAME = tryDecode(_ADDON.getSetting('deviceName'))
|
DEVICENAME = try_decode(_ADDON.getSetting('deviceName'))
|
||||||
DEVICENAME = DEVICENAME.replace(":", "")
|
DEVICENAME = DEVICENAME.replace(":", "")
|
||||||
DEVICENAME = DEVICENAME.replace("/", "-")
|
DEVICENAME = DEVICENAME.replace("/", "-")
|
||||||
DEVICENAME = DEVICENAME.replace("\\", "-")
|
DEVICENAME = DEVICENAME.replace("\\", "-")
|
||||||
|
@ -60,7 +68,15 @@ DEVICENAME = DEVICENAME.replace("?", "")
|
||||||
DEVICENAME = DEVICENAME.replace('|', "")
|
DEVICENAME = DEVICENAME.replace('|', "")
|
||||||
DEVICENAME = DEVICENAME.replace('(', "")
|
DEVICENAME = DEVICENAME.replace('(', "")
|
||||||
DEVICENAME = DEVICENAME.replace(')', "")
|
DEVICENAME = DEVICENAME.replace(')', "")
|
||||||
DEVICENAME = DEVICENAME.strip()
|
DEVICENAME = DEVICENAME.replace(' ', "")
|
||||||
|
|
||||||
|
COMPANION_PORT = int(_ADDON.getSetting('companionPort'))
|
||||||
|
|
||||||
|
# Unique ID for this Plex client; also see clientinfo.py
|
||||||
|
PKC_MACHINE_IDENTIFIER = None
|
||||||
|
|
||||||
|
# Minimal PKC version needed for the Kodi database - otherwise need to recreate
|
||||||
|
MIN_DB_VERSION = '2.0.11'
|
||||||
|
|
||||||
# Database paths
|
# Database paths
|
||||||
_DB_VIDEO_VERSION = {
|
_DB_VIDEO_VERSION = {
|
||||||
|
@ -71,7 +87,7 @@ _DB_VIDEO_VERSION = {
|
||||||
17: 107, # Krypton
|
17: 107, # Krypton
|
||||||
18: 108 # Leia
|
18: 108 # Leia
|
||||||
}
|
}
|
||||||
DB_VIDEO_PATH = tryDecode(xbmc.translatePath(
|
DB_VIDEO_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
|
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
|
||||||
|
|
||||||
_DB_MUSIC_VERSION = {
|
_DB_MUSIC_VERSION = {
|
||||||
|
@ -82,7 +98,7 @@ _DB_MUSIC_VERSION = {
|
||||||
17: 60, # Krypton
|
17: 60, # Krypton
|
||||||
18: 62 # Leia
|
18: 62 # Leia
|
||||||
}
|
}
|
||||||
DB_MUSIC_PATH = tryDecode(xbmc.translatePath(
|
DB_MUSIC_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
|
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
|
||||||
|
|
||||||
_DB_TEXTURE_VERSION = {
|
_DB_TEXTURE_VERSION = {
|
||||||
|
@ -93,12 +109,12 @@ _DB_TEXTURE_VERSION = {
|
||||||
17: 13, # Krypton
|
17: 13, # Krypton
|
||||||
18: 13 # Leia
|
18: 13 # Leia
|
||||||
}
|
}
|
||||||
DB_TEXTURE_PATH = tryDecode(xbmc.translatePath(
|
DB_TEXTURE_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]))
|
"special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]))
|
||||||
|
|
||||||
DB_PLEX_PATH = tryDecode(xbmc.translatePath("special://database/plex.db"))
|
DB_PLEX_PATH = try_decode(xbmc.translatePath("special://database/plex.db"))
|
||||||
|
|
||||||
EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath(
|
EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://profile/addon_data/%s/temp/" % ADDON_ID))
|
"special://profile/addon_data/%s/temp/" % ADDON_ID))
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,6 +139,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo'
|
||||||
|
|
||||||
PLEX_TYPE_PHOTO = 'photo'
|
PLEX_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
# Used for /:/timeline XML messages
|
||||||
|
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
||||||
|
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
||||||
|
PLEX_PLAYLIST_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
KODI_PLAYLIST_TYPE_VIDEO = 'video'
|
||||||
|
KODI_PLAYLIST_TYPE_AUDIO = 'audio'
|
||||||
|
KODI_PLAYLIST_TYPE_PHOTO = 'picture'
|
||||||
|
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = {
|
||||||
|
PLEX_PLAYLIST_TYPE_VIDEO: KODI_PLAYLIST_TYPE_VIDEO,
|
||||||
|
PLEX_PLAYLIST_TYPE_AUDIO: KODI_PLAYLIST_TYPE_AUDIO,
|
||||||
|
PLEX_PLAYLIST_TYPE_PHOTO: KODI_PLAYLIST_TYPE_PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
# All the Kodi types as e.g. used in the JSON API
|
# All the Kodi types as e.g. used in the JSON API
|
||||||
KODI_TYPE_VIDEO = 'video'
|
KODI_TYPE_VIDEO = 'video'
|
||||||
|
@ -142,9 +172,6 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
|
||||||
|
|
||||||
KODI_TYPE_PHOTO = 'photo'
|
KODI_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
|
||||||
# Translation tables
|
|
||||||
|
|
||||||
KODI_VIDEOTYPES = (
|
KODI_VIDEOTYPES = (
|
||||||
KODI_TYPE_VIDEO,
|
KODI_TYPE_VIDEO,
|
||||||
KODI_TYPE_MOVIE,
|
KODI_TYPE_MOVIE,
|
||||||
|
@ -154,12 +181,29 @@ KODI_VIDEOTYPES = (
|
||||||
KODI_TYPE_SET
|
KODI_TYPE_SET
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PLEX_VIDEOTYPES = (
|
||||||
|
PLEX_TYPE_MOVIE,
|
||||||
|
PLEX_TYPE_CLIP,
|
||||||
|
PLEX_TYPE_EPISODE,
|
||||||
|
PLEX_TYPE_SEASON,
|
||||||
|
PLEX_TYPE_SHOW
|
||||||
|
)
|
||||||
|
|
||||||
KODI_AUDIOTYPES = (
|
KODI_AUDIOTYPES = (
|
||||||
KODI_TYPE_SONG,
|
KODI_TYPE_SONG,
|
||||||
KODI_TYPE_ALBUM,
|
KODI_TYPE_ALBUM,
|
||||||
KODI_TYPE_ARTIST,
|
KODI_TYPE_ARTIST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Translation tables
|
||||||
|
|
||||||
|
ADDON_TYPE = {
|
||||||
|
PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies',
|
||||||
|
PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies',
|
||||||
|
PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows',
|
||||||
|
PLEX_TYPE_SONG: 'plugin.video.plexkodiconnect'
|
||||||
|
}
|
||||||
|
|
||||||
ITEMTYPE_FROM_PLEXTYPE = {
|
ITEMTYPE_FROM_PLEXTYPE = {
|
||||||
PLEX_TYPE_MOVIE: 'Movies',
|
PLEX_TYPE_MOVIE: 'Movies',
|
||||||
PLEX_TYPE_SEASON: 'TVShows',
|
PLEX_TYPE_SEASON: 'TVShows',
|
||||||
|
@ -193,6 +237,20 @@ KODITYPE_FROM_PLEXTYPE = {
|
||||||
'XXXXXXX': 'genre'
|
'XXXXXXX': 'genre'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PLEX_TYPE_FROM_KODI_TYPE = {
|
||||||
|
KODI_TYPE_VIDEO: PLEX_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_MOVIE: PLEX_TYPE_MOVIE,
|
||||||
|
KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE,
|
||||||
|
KODI_TYPE_SEASON: PLEX_TYPE_SEASON,
|
||||||
|
KODI_TYPE_SHOW: PLEX_TYPE_SHOW,
|
||||||
|
KODI_TYPE_CLIP: PLEX_TYPE_CLIP,
|
||||||
|
KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST,
|
||||||
|
KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM,
|
||||||
|
KODI_TYPE_SONG: PLEX_TYPE_SONG,
|
||||||
|
KODI_TYPE_AUDIO: PLEX_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_PHOTO: PLEX_TYPE_PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
||||||
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
||||||
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
||||||
|
@ -208,6 +266,20 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = {
|
||||||
|
KODI_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_EPISODE: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_SEASON: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_SHOW: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_CLIP: KODI_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_ARTIST: KODI_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_ALBUM: KODI_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_SONG: KODI_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_AUDIO: KODI_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_PHOTO: KODI_TYPE_PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
REMAP_TYPE_FROM_PLEXTYPE = {
|
REMAP_TYPE_FROM_PLEXTYPE = {
|
||||||
PLEX_TYPE_MOVIE: 'movie',
|
PLEX_TYPE_MOVIE: 'movie',
|
||||||
PLEX_TYPE_CLIP: 'clip',
|
PLEX_TYPE_CLIP: 'clip',
|
||||||
|
@ -247,6 +319,40 @@ PLEX_TYPE_FROM_WEBSOCKET = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
KODI_TO_PLEX_ARTWORK = {
|
||||||
|
'poster': 'thumb',
|
||||||
|
'banner': 'banner',
|
||||||
|
'fanart': 'art'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Might be implemented in the future: 'icon', 'landscape' (16:9)
|
||||||
|
ALL_KODI_ARTWORK = (
|
||||||
|
'thumb',
|
||||||
|
'poster',
|
||||||
|
'banner',
|
||||||
|
'clearart',
|
||||||
|
'clearlogo',
|
||||||
|
'fanart',
|
||||||
|
'discart'
|
||||||
|
)
|
||||||
|
|
||||||
|
# we need to use a little mapping between fanart.tv arttypes and kodi artttypes
|
||||||
|
FANART_TV_TO_KODI_TYPE = [
|
||||||
|
('poster', 'poster'),
|
||||||
|
('logo', 'clearlogo'),
|
||||||
|
('musiclogo', 'clearlogo'),
|
||||||
|
('disc', 'discart'),
|
||||||
|
('clearart', 'clearart'),
|
||||||
|
('banner', 'banner'),
|
||||||
|
('clearlogo', 'clearlogo'),
|
||||||
|
('background', 'fanart'),
|
||||||
|
('showbackground', 'fanart'),
|
||||||
|
('characterart', 'characterart')
|
||||||
|
]
|
||||||
|
# How many different backgrounds do we want to load from fanart.tv?
|
||||||
|
MAX_BACKGROUND_COUNT = 10
|
||||||
|
|
||||||
|
|
||||||
# extensions from:
|
# extensions from:
|
||||||
# http://kodi.wiki/view/Features_and_supported_codecs#Format_support (RAW image
|
# http://kodi.wiki/view/Features_and_supported_codecs#Format_support (RAW image
|
||||||
# formats, BMP, JPEG, GIF, PNG, TIFF, MNG, ICO, PCX and Targa/TGA)
|
# formats, BMP, JPEG, GIF, PNG, TIFF, MNG, ICO, PCX and Targa/TGA)
|
||||||
|
@ -360,3 +466,21 @@ SORT_METHODS_ALBUMS = (
|
||||||
'SORT_METHOD_ARTIST',
|
'SORT_METHOD_ARTIST',
|
||||||
'SORT_METHOD_ALBUM',
|
'SORT_METHOD_ALBUM',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
|
||||||
|
COMPANION_OK_MESSAGE = XML_HEADER + '<Response code="200" status="OK" />'
|
||||||
|
|
||||||
|
PLEX_REPEAT_FROM_KODI_REPEAT = {
|
||||||
|
'off': '0',
|
||||||
|
'one': '1',
|
||||||
|
'all': '2' # does this work?!?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stream in PMS xml contains a streamType to distinguish the kind of stream
|
||||||
|
PLEX_STREAM_TYPE_FROM_STREAM_TYPE = {
|
||||||
|
'video': '1',
|
||||||
|
'audio': '2',
|
||||||
|
'subtitle': '3'
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
from shutil import copytree
|
from shutil import copytree
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
from os import makedirs
|
from os import makedirs
|
||||||
|
@ -8,13 +8,14 @@ from os import makedirs
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcvfs import exists
|
from xbmcvfs import exists
|
||||||
|
|
||||||
from utils import window, settings, language as lang, tryEncode, indent, \
|
from utils import window, settings, language as lang, try_encode, indent, \
|
||||||
normalize_nodes, exists_dir, tryDecode
|
normalize_nodes, exists_dir, try_decode
|
||||||
import variables as v
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Paths are strings, NOT unicode!
|
# Paths are strings, NOT unicode!
|
||||||
|
@ -46,7 +47,7 @@ class VideoNodes(object):
|
||||||
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
|
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
|
||||||
# Plex: reassign mediatype due to Kodi inner workings
|
# Plex: reassign mediatype due to Kodi inner workings
|
||||||
# How many items do we get at most?
|
# How many items do we get at most?
|
||||||
limit = window('fetch_pms_item_number')
|
limit = state.FETCH_PMS_ITEM_NUMBER
|
||||||
mediatypes = {
|
mediatypes = {
|
||||||
'movie': 'movies',
|
'movie': 'movies',
|
||||||
'show': 'tvshows',
|
'show': 'tvshows',
|
||||||
|
@ -62,9 +63,9 @@ class VideoNodes(object):
|
||||||
dirname = viewid
|
dirname = viewid
|
||||||
|
|
||||||
# Returns strings
|
# Returns strings
|
||||||
path = tryDecode(xbmc.translatePath(
|
path = try_decode(xbmc.translatePath(
|
||||||
"special://profile/library/video/"))
|
"special://profile/library/video/"))
|
||||||
nodepath = tryDecode(xbmc.translatePath(
|
nodepath = try_decode(xbmc.translatePath(
|
||||||
"special://profile/library/video/Plex-%s/" % dirname))
|
"special://profile/library/video/Plex-%s/" % dirname))
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
|
@ -77,9 +78,9 @@ class VideoNodes(object):
|
||||||
# Verify the video directory
|
# Verify the video directory
|
||||||
if not exists_dir(path):
|
if not exists_dir(path):
|
||||||
copytree(
|
copytree(
|
||||||
src=tryDecode(xbmc.translatePath(
|
src=try_decode(xbmc.translatePath(
|
||||||
"special://xbmc/system/library/video")),
|
"special://xbmc/system/library/video")),
|
||||||
dst=tryDecode(xbmc.translatePath(
|
dst=try_decode(xbmc.translatePath(
|
||||||
"special://profile/library/video")))
|
"special://profile/library/video")))
|
||||||
|
|
||||||
# Create the node directory
|
# Create the node directory
|
||||||
|
@ -292,7 +293,7 @@ class VideoNodes(object):
|
||||||
# To do: add our photos nodes to kodi picture sources somehow
|
# To do: add our photos nodes to kodi picture sources somehow
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if exists(tryEncode(nodeXML)):
|
if exists(try_encode(nodeXML)):
|
||||||
# Don't recreate xml if already exists
|
# Don't recreate xml if already exists
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -378,9 +379,9 @@ class VideoNodes(object):
|
||||||
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
|
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
|
||||||
|
|
||||||
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
||||||
tagname = tryEncode(tagname)
|
tagname = try_encode(tagname)
|
||||||
cleantagname = tryDecode(normalize_nodes(tagname))
|
cleantagname = try_decode(normalize_nodes(tagname))
|
||||||
nodepath = tryDecode(xbmc.translatePath(
|
nodepath = try_decode(xbmc.translatePath(
|
||||||
"special://profile/library/video/"))
|
"special://profile/library/video/"))
|
||||||
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
|
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
|
||||||
path = "library://video/plex_%s.xml" % cleantagname
|
path = "library://video/plex_%s.xml" % cleantagname
|
||||||
|
@ -394,9 +395,9 @@ class VideoNodes(object):
|
||||||
if not exists_dir(nodepath):
|
if not exists_dir(nodepath):
|
||||||
# We need to copy over the default items
|
# We need to copy over the default items
|
||||||
copytree(
|
copytree(
|
||||||
src=tryDecode(xbmc.translatePath(
|
src=try_decode(xbmc.translatePath(
|
||||||
"special://xbmc/system/library/video")),
|
"special://xbmc/system/library/video")),
|
||||||
dst=tryDecode(xbmc.translatePath(
|
dst=try_decode(xbmc.translatePath(
|
||||||
"special://profile/library/video")))
|
"special://profile/library/video")))
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
|
@ -411,7 +412,7 @@ class VideoNodes(object):
|
||||||
window('%s.content' % embynode, value=path)
|
window('%s.content' % embynode, value=path)
|
||||||
window('%s.type' % embynode, value=itemtype)
|
window('%s.type' % embynode, value=itemtype)
|
||||||
|
|
||||||
if exists(tryEncode(nodeXML)):
|
if exists(try_encode(nodeXML)):
|
||||||
# Don't recreate xml if already exists
|
# Don't recreate xml if already exists
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -292,7 +292,7 @@ class ABNF(object):
|
||||||
opcode: operation code. please see OPCODE_XXX.
|
opcode: operation code. please see OPCODE_XXX.
|
||||||
"""
|
"""
|
||||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
||||||
data = utils.tryEncode(data)
|
data = utils.try_encode(data)
|
||||||
# mask must be set if send data from client
|
# mask must be set if send data from client
|
||||||
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
|
@ -504,7 +504,8 @@ class WebSocket(object):
|
||||||
|
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
|
||||||
def _validate_header(self, headers, key):
|
@staticmethod
|
||||||
|
def _validate_header(headers, key):
|
||||||
for k, v in _HEADERS_TO_CHECK.iteritems():
|
for k, v in _HEADERS_TO_CHECK.iteritems():
|
||||||
r = headers.get(k, None)
|
r = headers.get(k, None)
|
||||||
if not r:
|
if not r:
|
||||||
|
@ -598,7 +599,7 @@ class WebSocket(object):
|
||||||
|
|
||||||
return value: string(byte array) value.
|
return value: string(byte array) value.
|
||||||
"""
|
"""
|
||||||
opcode, data = self.recv_data()
|
_, data = self.recv_data()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def recv_data(self):
|
def recv_data(self):
|
||||||
|
@ -620,7 +621,6 @@ class WebSocket(object):
|
||||||
self._cont_data[1] += frame.data
|
self._cont_data[1] += frame.data
|
||||||
else:
|
else:
|
||||||
self._cont_data = [frame.opcode, frame.data]
|
self._cont_data = [frame.opcode, frame.data]
|
||||||
|
|
||||||
if frame.fin:
|
if frame.fin:
|
||||||
data = self._cont_data
|
data = self._cont_data
|
||||||
self._cont_data = None
|
self._cont_data = None
|
||||||
|
@ -740,7 +740,7 @@ class WebSocket(object):
|
||||||
|
|
||||||
def _recv(self, bufsize):
|
def _recv(self, bufsize):
|
||||||
try:
|
try:
|
||||||
bytes = self.sock.recv(bufsize)
|
bytes_ = self.sock.recv(bufsize)
|
||||||
except socket.timeout as e:
|
except socket.timeout as e:
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
raise WebSocketTimeoutException(e.args[0])
|
||||||
except SSLError as e:
|
except SSLError as e:
|
||||||
|
@ -748,17 +748,17 @@ class WebSocket(object):
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
raise WebSocketTimeoutException(e.args[0])
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if not bytes:
|
if not bytes_:
|
||||||
raise WebSocketConnectionClosedException()
|
raise WebSocketConnectionClosedException()
|
||||||
return bytes
|
return bytes_
|
||||||
|
|
||||||
|
|
||||||
def _recv_strict(self, bufsize):
|
def _recv_strict(self, bufsize):
|
||||||
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
|
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
|
||||||
while shortage > 0:
|
while shortage > 0:
|
||||||
bytes = self._recv(shortage)
|
bytes_ = self._recv(shortage)
|
||||||
self._recv_buffer.append(bytes)
|
self._recv_buffer.append(bytes_)
|
||||||
shortage -= len(bytes)
|
shortage -= len(bytes_)
|
||||||
unified = "".join(self._recv_buffer)
|
unified = "".join(self._recv_buffer)
|
||||||
if shortage == 0:
|
if shortage == 0:
|
||||||
self._recv_buffer = []
|
self._recv_buffer = []
|
||||||
|
@ -783,7 +783,7 @@ class WebSocketApp(object):
|
||||||
Higher level of APIs are provided.
|
Higher level of APIs are provided.
|
||||||
The interface is like JavaScript WebSocket object.
|
The interface is like JavaScript WebSocket object.
|
||||||
"""
|
"""
|
||||||
def __init__(self, url, header=[],
|
def __init__(self, url, header=None,
|
||||||
on_open=None, on_message=None, on_error=None,
|
on_open=None, on_message=None, on_error=None,
|
||||||
on_close=None, keep_running=True, get_mask_key=None):
|
on_close=None, keep_running=True, get_mask_key=None):
|
||||||
"""
|
"""
|
||||||
|
@ -807,7 +807,7 @@ class WebSocketApp(object):
|
||||||
docstring for more information
|
docstring for more information
|
||||||
"""
|
"""
|
||||||
self.url = url
|
self.url = url
|
||||||
self.header = header
|
self.header = [] if header is None else header
|
||||||
self.on_open = on_open
|
self.on_open = on_open
|
||||||
self.on_message = on_message
|
self.on_message = on_message
|
||||||
self.on_error = on_error
|
self.on_error = on_error
|
||||||
|
@ -830,12 +830,12 @@ class WebSocketApp(object):
|
||||||
close websocket connection.
|
close websocket connection.
|
||||||
"""
|
"""
|
||||||
self.keep_running = False
|
self.keep_running = False
|
||||||
if(self.sock != None):
|
if self.sock != None:
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
|
|
||||||
def _send_ping(self, interval):
|
def _send_ping(self, interval):
|
||||||
while True:
|
while True:
|
||||||
for i in range(interval):
|
for _ in range(interval):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if not self.keep_running:
|
if not self.keep_running:
|
||||||
return
|
return
|
||||||
|
@ -878,8 +878,7 @@ class WebSocketApp(object):
|
||||||
|
|
||||||
if data is None or self.keep_running == False:
|
if data is None or self.keep_running == False:
|
||||||
break
|
break
|
||||||
self._callback(self.on_message, data)
|
self._callback(self.on_message, data)
|
||||||
|
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
#print str(e.args[0])
|
#print str(e.args[0])
|
||||||
if "timed out" not in e.args[0]:
|
if "timed out" not in e.args[0]:
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
import logging
|
from logging import getLogger
|
||||||
import websocket
|
import websocket
|
||||||
from json import loads
|
from json import loads
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
|
||||||
from ssl import CERT_NONE
|
from ssl import CERT_NONE
|
||||||
|
|
||||||
from xbmc import sleep
|
from xbmc import sleep
|
||||||
|
@ -14,10 +13,11 @@ from xbmc import sleep
|
||||||
from utils import window, settings, thread_methods
|
from utils import window, settings, thread_methods
|
||||||
from companion import process_command
|
from companion import process_command
|
||||||
import state
|
import state
|
||||||
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -25,11 +25,9 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
class WebSocket(Thread):
|
class WebSocket(Thread):
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
def __init__(self, callback=None):
|
def __init__(self):
|
||||||
if callback is not None:
|
|
||||||
self.mgr = callback
|
|
||||||
self.ws = None
|
self.ws = None
|
||||||
Thread.__init__(self)
|
super(WebSocket, self).__init__()
|
||||||
|
|
||||||
def process(self, opcode, message):
|
def process(self, opcode, message):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -56,26 +54,23 @@ class WebSocket(Thread):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
log.info("----===## Starting %s ##===----" % self.__class__.__name__)
|
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
handshake_counter = 0
|
handshake_counter = 0
|
||||||
thread_stopped = self.thread_stopped
|
stopped = self.stopped
|
||||||
thread_suspended = self.thread_suspended
|
suspended = self.suspended
|
||||||
while not thread_stopped():
|
while not stopped():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
while thread_suspended():
|
while suspended():
|
||||||
# Set in service.py
|
# Set in service.py
|
||||||
if self.ws is not None:
|
if self.ws is not None:
|
||||||
try:
|
self.ws.close()
|
||||||
self.ws.shutdown()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.ws = None
|
self.ws = None
|
||||||
if thread_stopped():
|
if stopped():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
log.info("##===---- %s Stopped ----===##"
|
LOG.info("##===---- %s Stopped ----===##",
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
return
|
return
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
|
@ -84,8 +79,8 @@ class WebSocket(Thread):
|
||||||
# No worries if read timed out
|
# No worries if read timed out
|
||||||
pass
|
pass
|
||||||
except websocket.WebSocketConnectionClosedException:
|
except websocket.WebSocketConnectionClosedException:
|
||||||
log.info("%s: connection closed, (re)connecting"
|
LOG.info("%s: connection closed, (re)connecting",
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
uri, sslopt = self.getUri()
|
uri, sslopt = self.getUri()
|
||||||
try:
|
try:
|
||||||
# Low timeout - let's us shut this thread down!
|
# Low timeout - let's us shut this thread down!
|
||||||
|
@ -96,7 +91,7 @@ class WebSocket(Thread):
|
||||||
enable_multithread=True)
|
enable_multithread=True)
|
||||||
except IOError:
|
except IOError:
|
||||||
# Server is probably offline
|
# Server is probably offline
|
||||||
log.info("%s: Error connecting" % self.__class__.__name__)
|
LOG.info("%s: Error connecting", self.__class__.__name__)
|
||||||
self.ws = None
|
self.ws = None
|
||||||
counter += 1
|
counter += 1
|
||||||
if counter > 3:
|
if counter > 3:
|
||||||
|
@ -104,68 +99,54 @@ class WebSocket(Thread):
|
||||||
self.IOError_response()
|
self.IOError_response()
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
except websocket.WebSocketTimeoutException:
|
except websocket.WebSocketTimeoutException:
|
||||||
log.info("%s: Timeout while connecting, trying again"
|
LOG.info("%s: Timeout while connecting, trying again",
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
self.ws = None
|
self.ws = None
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
except websocket.WebSocketException as e:
|
except websocket.WebSocketException as e:
|
||||||
log.info('%s: WebSocketException: %s'
|
LOG.info('%s: WebSocketException: %s',
|
||||||
% (self.__class__.__name__, e))
|
self.__class__.__name__, e)
|
||||||
if 'Handshake Status 401' in e.args:
|
if ('Handshake Status 401' in e.args
|
||||||
|
or 'Handshake Status 403' in e.args):
|
||||||
handshake_counter += 1
|
handshake_counter += 1
|
||||||
if handshake_counter >= 5:
|
if handshake_counter >= 5:
|
||||||
log.info('%s: Error in handshake detected. '
|
LOG.info('%s: Error in handshake detected. '
|
||||||
'Stopping now'
|
'Stopping now', self.__class__.__name__)
|
||||||
% self.__class__.__name__)
|
|
||||||
break
|
break
|
||||||
self.ws = None
|
self.ws = None
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('%s: Unknown exception encountered when '
|
LOG.error('%s: Unknown exception encountered when '
|
||||||
'connecting: %s' % (self.__class__.__name__, e))
|
'connecting: %s', self.__class__.__name__, e)
|
||||||
import traceback
|
import traceback
|
||||||
log.error("%s: Traceback:\n%s"
|
LOG.error("%s: Traceback:\n%s",
|
||||||
% (self.__class__.__name__,
|
self.__class__.__name__, traceback.format_exc())
|
||||||
traceback.format_exc()))
|
|
||||||
self.ws = None
|
self.ws = None
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
else:
|
else:
|
||||||
counter = 0
|
counter = 0
|
||||||
handshake_counter = 0
|
handshake_counter = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("%s: Unknown exception encountered: %s"
|
LOG.error("%s: Unknown exception encountered: %s",
|
||||||
% (self.__class__.__name__, e))
|
self.__class__.__name__, e)
|
||||||
import traceback
|
import traceback
|
||||||
log.error("%s: Traceback:\n%s"
|
LOG.error("%s: Traceback:\n%s",
|
||||||
% (self.__class__.__name__,
|
self.__class__.__name__, traceback.format_exc())
|
||||||
traceback.format_exc()))
|
if self.ws is not None:
|
||||||
try:
|
self.ws.close()
|
||||||
self.ws.shutdown()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.ws = None
|
self.ws = None
|
||||||
log.info("##===---- %s Stopped ----===##" % self.__class__.__name__)
|
# Close websocket connection on shutdown
|
||||||
|
if self.ws is not None:
|
||||||
def stopThread(self):
|
self.ws.close()
|
||||||
"""
|
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
|
||||||
Overwrite this method from thread_methods to close websockets
|
|
||||||
"""
|
|
||||||
log.info("Stopping %s thread." % self.__class__.__name__)
|
|
||||||
self.__threadStopped = True
|
|
||||||
try:
|
|
||||||
self.ws.shutdown()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD'])
|
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||||
|
'BACKGROUND_SYNC_DISABLED'])
|
||||||
class PMS_Websocket(WebSocket):
|
class PMS_Websocket(WebSocket):
|
||||||
"""
|
"""
|
||||||
Websocket connection with the PMS for Plex Companion
|
Websocket connection with the PMS for Plex Companion
|
||||||
"""
|
"""
|
||||||
# Communication with librarysync
|
|
||||||
queue = Queue()
|
|
||||||
|
|
||||||
def getUri(self):
|
def getUri(self):
|
||||||
server = window('pms_server')
|
server = window('pms_server')
|
||||||
# Get the appropriate prefix for the websocket
|
# Get the appropriate prefix for the websocket
|
||||||
|
@ -180,8 +161,8 @@ class PMS_Websocket(WebSocket):
|
||||||
sslopt = {}
|
sslopt = {}
|
||||||
if settings('sslverify') == "false":
|
if settings('sslverify') == "false":
|
||||||
sslopt["cert_reqs"] = CERT_NONE
|
sslopt["cert_reqs"] = CERT_NONE
|
||||||
log.debug("%s: Uri: %s, sslopt: %s"
|
LOG.debug("%s: Uri: %s, sslopt: %s",
|
||||||
% (self.__class__.__name__, uri, sslopt))
|
self.__class__.__name__, uri, sslopt)
|
||||||
return uri, sslopt
|
return uri, sslopt
|
||||||
|
|
||||||
def process(self, opcode, message):
|
def process(self, opcode, message):
|
||||||
|
@ -191,38 +172,38 @@ class PMS_Websocket(WebSocket):
|
||||||
try:
|
try:
|
||||||
message = loads(message)
|
message = loads(message)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.error('%s: Error decoding message from websocket'
|
LOG.error('%s: Error decoding message from websocket',
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
log.error(message)
|
LOG.error(message)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
message = message['NotificationContainer']
|
message = message['NotificationContainer']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.error('%s: Could not parse PMS message: %s'
|
LOG.error('%s: Could not parse PMS message: %s',
|
||||||
% (self.__class__.__name__, message))
|
self.__class__.__name__, message)
|
||||||
return
|
return
|
||||||
# Triage
|
# Triage
|
||||||
typus = message.get('type')
|
typus = message.get('type')
|
||||||
if typus is None:
|
if typus is None:
|
||||||
log.error('%s: No message type, dropping message: %s'
|
LOG.error('%s: No message type, dropping message: %s',
|
||||||
% (self.__class__.__name__, message))
|
self.__class__.__name__, message)
|
||||||
return
|
return
|
||||||
log.debug('%s: Received message from PMS server: %s'
|
LOG.debug('%s: Received message from PMS server: %s',
|
||||||
% (self.__class__.__name__, message))
|
self.__class__.__name__, message)
|
||||||
# Drop everything we're not interested in
|
# Drop everything we're not interested in
|
||||||
if typus not in ('playing', 'timeline', 'activity'):
|
if typus not in ('playing', 'timeline', 'activity'):
|
||||||
return
|
return
|
||||||
elif typus == 'activity' and state.DB_SCAN is True:
|
elif typus == 'activity' and state.DB_SCAN is True:
|
||||||
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
||||||
# possibly causing these reprocessing messages en mass)
|
# possibly causing these reprocessing messages en mass)
|
||||||
log.debug('%s: Dropping message as PKC is currently synching'
|
LOG.debug('%s: Dropping message as PKC is currently synching',
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
else:
|
else:
|
||||||
# Put PMS message on queue and let libsync take care of it
|
# Put PMS message on queue and let libsync take care of it
|
||||||
self.queue.put(message)
|
state.WEBSOCKET_QUEUE.put(message)
|
||||||
|
|
||||||
def IOError_response(self):
|
def IOError_response(self):
|
||||||
log.warn("Repeatedly could not connect to PMS, "
|
LOG.warn("Repeatedly could not connect to PMS, "
|
||||||
"declaring the connection dead")
|
"declaring the connection dead")
|
||||||
window('plex_online', value='false')
|
window('plex_online', value='false')
|
||||||
|
|
||||||
|
@ -233,72 +214,70 @@ class Alexa_Websocket(WebSocket):
|
||||||
|
|
||||||
Can't use thread_methods!
|
Can't use thread_methods!
|
||||||
"""
|
"""
|
||||||
__thread_stopped = False
|
thread_stopped = False
|
||||||
__thread_suspended = False
|
thread_suspended = False
|
||||||
|
|
||||||
def getUri(self):
|
def getUri(self):
|
||||||
self.plex_client_Id = window('plex_client_Id')
|
|
||||||
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
||||||
% (state.PLEX_USER_ID,
|
% (state.PLEX_USER_ID,
|
||||||
self.plex_client_Id, state.PLEX_TOKEN))
|
v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
state.PLEX_TOKEN))
|
||||||
sslopt = {}
|
sslopt = {}
|
||||||
log.debug("%s: Uri: %s, sslopt: %s"
|
LOG.debug("%s: Uri: %s, sslopt: %s",
|
||||||
% (self.__class__.__name__, uri, sslopt))
|
self.__class__.__name__, uri, sslopt)
|
||||||
return uri, sslopt
|
return uri, sslopt
|
||||||
|
|
||||||
def process(self, opcode, message):
|
def process(self, opcode, message):
|
||||||
if opcode not in self.opcode_data:
|
if opcode not in self.opcode_data:
|
||||||
return
|
return
|
||||||
log.debug('%s: Received the following message from Alexa:'
|
LOG.debug('%s: Received the following message from Alexa:',
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
log.debug('%s: %s' % (self.__class__.__name__, message))
|
LOG.debug('%s: %s', self.__class__.__name__, message)
|
||||||
try:
|
try:
|
||||||
message = etree.fromstring(message)
|
message = etree.fromstring(message)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error('%s: Error decoding message from Alexa: %s'
|
LOG.error('%s: Error decoding message from Alexa: %s',
|
||||||
% (self.__class__.__name__, ex))
|
self.__class__.__name__, ex)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if message.attrib['command'] == 'processRemoteControlCommand':
|
if message.attrib['command'] == 'processRemoteControlCommand':
|
||||||
message = message[0]
|
message = message[0]
|
||||||
else:
|
else:
|
||||||
log.error('%s: Unknown Alexa message received'
|
LOG.error('%s: Unknown Alexa message received',
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
log.error('%s: Could not parse Alexa message'
|
LOG.error('%s: Could not parse Alexa message',
|
||||||
% self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
return
|
return
|
||||||
process_command(message.attrib['path'][1:],
|
process_command(message.attrib['path'][1:], message.attrib)
|
||||||
message.attrib,
|
|
||||||
queue=self.mgr.plexCompanion.queue)
|
|
||||||
|
|
||||||
def IOError_response(self):
|
def IOError_response(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Path in thread_methods
|
# Path in thread_methods
|
||||||
def stop_thread(self):
|
def stop(self):
|
||||||
self.__thread_stopped = True
|
self.thread_stopped = True
|
||||||
|
|
||||||
def suspend_thread(self):
|
def suspend(self):
|
||||||
self.__thread_suspended = True
|
self.thread_suspended = True
|
||||||
|
|
||||||
def resume_thread(self):
|
def resume(self):
|
||||||
self.__thread_suspended = False
|
self.thread_suspended = False
|
||||||
|
|
||||||
def thread_stopped(self):
|
def stopped(self):
|
||||||
if self.__thread_stopped is True:
|
if self.thread_stopped is True:
|
||||||
return True
|
return True
|
||||||
if state.STOP_PKC:
|
if state.STOP_PKC:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# The culprit
|
# The culprit
|
||||||
def thread_suspended(self):
|
def suspended(self):
|
||||||
"""
|
"""
|
||||||
Overwrite method since we need to check for plex token
|
Overwrite method since we need to check for plex token
|
||||||
"""
|
"""
|
||||||
if self.__thread_suspended is True:
|
if self.thread_suspended is True:
|
||||||
return True
|
return True
|
||||||
if not state.PLEX_TOKEN:
|
if not state.PLEX_TOKEN:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -38,11 +38,21 @@
|
||||||
<setting type="lsep" label="39700" />
|
<setting type="lsep" label="39700" />
|
||||||
<setting id="enable_alexa" label="39701" type="bool" default="true"/>
|
<setting id="enable_alexa" label="39701" type="bool" default="true"/>
|
||||||
<setting type="lsep" label="" />
|
<setting type="lsep" label="" />
|
||||||
<setting id="enableContext" type="bool" label="30413" default="true" />
|
<!-- Different settings that are not visible - to avoid warnings in the log -->
|
||||||
<setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-1,true)" subsetting="true" />
|
<setting id="skipContextMenu" type="bool" label="30520" default="false"/>
|
||||||
<setting id="plex_restricteduser" type="bool" default="false" visible="false"/>
|
<setting id="plex_restricteduser" type="bool" default="false" visible="false"/>
|
||||||
<setting id="plex_allows_mediaDeletion" type="bool" default="true" visible="false"/>
|
<setting id="plex_allows_mediaDeletion" type="bool" default="true" visible="false"/>
|
||||||
<setting id="companion_show_gdm_port_warning" type="bool" default="true" visible="false"/>
|
<setting id="companion_show_gdm_port_warning" type="bool" default="true" visible="false"/>
|
||||||
|
<setting id="InstallQuestionsAnswered" type="bool" default="false" visible="false"/>
|
||||||
|
<setting id="SyncInstallRunDone" type="bool" default="false" visible="false"/>
|
||||||
|
<setting id="last_migrated_PKC_version" type="text" default="" visible="false"/>
|
||||||
|
<setting id="plexAvatar" type="text" default="" visible="false"/>
|
||||||
|
<setting id="plex_client_Id" type="text" default="" visible="false"/>
|
||||||
|
<setting id="plex_machineIdentifier" type="text" default="" visible="false"/>
|
||||||
|
<setting id="dbCreatedWithVersion" type="text" default="" visible="false"/>
|
||||||
|
<setting id="plexid" type="text" default="" visible="false"/>
|
||||||
|
<setting id="userid" type="text" default="" visible="false"/>
|
||||||
|
<setting id="username" type="text" default="" visible="false"/>
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
<category label="30506"><!-- Sync Options -->
|
<category label="30506"><!-- Sync Options -->
|
||||||
|
@ -112,6 +122,7 @@
|
||||||
<setting id="bestTrailer" type="bool" label="30542" default="true" />
|
<setting id="bestTrailer" type="bool" label="30542" default="true" />
|
||||||
<setting id="force_transcode_pix" type="bool" label="30545" default="false" />
|
<setting id="force_transcode_pix" type="bool" label="30545" default="false" />
|
||||||
<setting id="kodi_video_cache" type="number" visible="false" default="20971520" />
|
<setting id="kodi_video_cache" type="number" visible="false" default="20971520" />
|
||||||
|
<setting id="warned_setting_videoplayer.autoplaynextitem" type="bool" visible="false" default="false" />
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
<category label="30544"><!-- artwork -->
|
<category label="30544"><!-- artwork -->
|
||||||
|
@ -134,6 +145,7 @@
|
||||||
|
|
||||||
<category label="39073"><!-- Appearance Tweaks -->
|
<category label="39073"><!-- Appearance Tweaks -->
|
||||||
<setting id="fetch_pms_item_number" label="39077" type="number" default="25" option="int" />
|
<setting id="fetch_pms_item_number" label="39077" type="number" default="25" option="int" />
|
||||||
|
<setting id="forceReloadSkinOnPlaybackStop" type="bool" label="39065" default="false" /><!-- Force-refresh Kodi skin on stopping playback -->
|
||||||
<setting type="lsep" label="39074" /><!-- TV Shows -->
|
<setting type="lsep" label="39074" /><!-- TV Shows -->
|
||||||
<setting id="OnDeckTVextended" type="bool" label="39058" default="true" /><!-- Extend Plex TV Series "On Deck" view to all shows -->
|
<setting id="OnDeckTVextended" type="bool" label="39058" default="true" /><!-- Extend Plex TV Series "On Deck" view to all shows -->
|
||||||
<setting id="OnDeckTvAppendShow" type="bool" label="39047" default="false" /><!--On Deck view: Append show title to episode-->
|
<setting id="OnDeckTvAppendShow" type="bool" label="39047" default="false" /><!--On Deck view: Append show title to episode-->
|
||||||
|
|
161
service.py
161
service.py
|
@ -1,8 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
from logging import getLogger
|
||||||
import logging
|
|
||||||
from os import path as os_path
|
from os import path as os_path
|
||||||
from sys import path as sys_path, argv
|
from sys import path as sys_path, argv
|
||||||
|
|
||||||
|
@ -11,50 +9,46 @@ from xbmcaddon import Addon
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
_addon = Addon(id='plugin.video.plexkodiconnect')
|
_ADDON = Addon(id='plugin.video.plexkodiconnect')
|
||||||
try:
|
try:
|
||||||
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
|
_ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
_addon_path = _addon.getAddonInfo('path').decode()
|
_ADDON_PATH = _ADDON.getAddonInfo('path').decode()
|
||||||
try:
|
try:
|
||||||
_base_resource = translatePath(os_path.join(
|
_BASE_RESOURCE = translatePath(os_path.join(
|
||||||
_addon_path,
|
_ADDON_PATH,
|
||||||
'resources',
|
'resources',
|
||||||
'lib')).decode('utf-8')
|
'lib')).decode('utf-8')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
_base_resource = translatePath(os_path.join(
|
_BASE_RESOURCE = translatePath(os_path.join(
|
||||||
_addon_path,
|
_ADDON_PATH,
|
||||||
'resources',
|
'resources',
|
||||||
'lib')).decode()
|
'lib')).decode()
|
||||||
sys_path.append(_base_resource)
|
sys_path.append(_BASE_RESOURCE)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from utils import settings, window, language as lang, dialog, tryDecode
|
from utils import settings, window, language as lang, dialog
|
||||||
from userclient import UserClient
|
from userclient import UserClient
|
||||||
import initialsetup
|
import initialsetup
|
||||||
from kodimonitor import KodiMonitor
|
from kodimonitor import KodiMonitor, SpecialMonitor
|
||||||
from librarysync import LibrarySync
|
from librarysync import LibrarySync
|
||||||
import videonodes
|
|
||||||
from websocket_client import PMS_Websocket, Alexa_Websocket
|
from websocket_client import PMS_Websocket, Alexa_Websocket
|
||||||
import downloadutils
|
|
||||||
from playqueue import Playqueue
|
|
||||||
|
|
||||||
import PlexAPI
|
from PlexFunctions import check_connection
|
||||||
from PlexCompanion import PlexCompanion
|
from PlexCompanion import PlexCompanion
|
||||||
from command_pipeline import Monitor_Window
|
from command_pipeline import Monitor_Window
|
||||||
from playback_starter import Playback_Starter
|
from playback_starter import Playback_Starter
|
||||||
|
from playqueue import PlayqueueMonitor
|
||||||
from artwork import Image_Cache_Thread
|
from artwork import Image_Cache_Thread
|
||||||
import variables as v
|
import variables as v
|
||||||
import state
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import loghandler
|
import loghandler
|
||||||
|
|
||||||
loghandler.config()
|
loghandler.config()
|
||||||
log = logging.getLogger("PLEX.service")
|
LOG = getLogger("PLEX.service")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,61 +61,28 @@ class Service():
|
||||||
ws = None
|
ws = None
|
||||||
library = None
|
library = None
|
||||||
plexCompanion = None
|
plexCompanion = None
|
||||||
playqueue = None
|
|
||||||
|
|
||||||
user_running = False
|
user_running = False
|
||||||
ws_running = False
|
ws_running = False
|
||||||
alexa_running = False
|
alexa_running = False
|
||||||
library_running = False
|
library_running = False
|
||||||
plexCompanion_running = False
|
plexCompanion_running = False
|
||||||
playqueue_running = False
|
|
||||||
kodimonitor_running = False
|
kodimonitor_running = False
|
||||||
playback_starter_running = False
|
playback_starter_running = False
|
||||||
image_cache_thread_running = False
|
image_cache_thread_running = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self.monitor = Monitor()
|
|
||||||
|
|
||||||
window('plex_kodiProfile',
|
|
||||||
value=tryDecode(translatePath("special://profile")))
|
|
||||||
window('plex_context',
|
|
||||||
value='true' if settings('enableContext') == "true" else "")
|
|
||||||
window('fetch_pms_item_number',
|
|
||||||
value=settings('fetch_pms_item_number'))
|
|
||||||
|
|
||||||
# Initial logging
|
# Initial logging
|
||||||
log.info("======== START %s ========" % v.ADDON_NAME)
|
LOG.info("======== START %s ========", v.ADDON_NAME)
|
||||||
log.info("Platform: %s" % v.PLATFORM)
|
LOG.info("Platform: %s", v.PLATFORM)
|
||||||
log.info("KODI Version: %s" % v.KODILONGVERSION)
|
LOG.info("KODI Version: %s", v.KODILONGVERSION)
|
||||||
log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
|
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
|
||||||
log.info("Using plugin paths: %s"
|
LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == "true")
|
||||||
% (settings('useDirectPaths') != "true"))
|
LOG.info("Number of sync threads: %s", settings('syncThreadNumber'))
|
||||||
log.info("Number of sync threads: %s"
|
LOG.info("Full sys.argv received: %s", argv)
|
||||||
% settings('syncThreadNumber'))
|
self.monitor = Monitor()
|
||||||
log.info("Full sys.argv received: %s" % argv)
|
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||||
|
initialsetup.reload_pkc()
|
||||||
# Reset window props for profile switch
|
|
||||||
properties = [
|
|
||||||
"plex_online", "plex_serverStatus", "plex_onWake",
|
|
||||||
"plex_kodiScan",
|
|
||||||
"plex_shouldStop", "plex_dbScan",
|
|
||||||
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps",
|
|
||||||
"pms_token", "plex_token",
|
|
||||||
"pms_server", "plex_machineIdentifier", "plex_servername",
|
|
||||||
"plex_authenticated", "PlexUserImage", "useDirectPaths",
|
|
||||||
"countError", "countUnauthorized",
|
|
||||||
"plex_restricteduser", "plex_allows_mediaDeletion",
|
|
||||||
"plex_command", "plex_result", "plex_force_transcode_pix"
|
|
||||||
]
|
|
||||||
for prop in properties:
|
|
||||||
window(prop, clear=True)
|
|
||||||
|
|
||||||
# Clear video nodes properties
|
|
||||||
videonodes.VideoNodes().clearProperties()
|
|
||||||
|
|
||||||
# Set the minimum database version
|
|
||||||
window('plex_minDBVersion', value="1.5.10")
|
|
||||||
|
|
||||||
def __stop_PKC(self):
|
def __stop_PKC(self):
|
||||||
"""
|
"""
|
||||||
|
@ -136,36 +97,34 @@ class Service():
|
||||||
monitor = self.monitor
|
monitor = self.monitor
|
||||||
kodiProfile = v.KODI_PROFILE
|
kodiProfile = v.KODI_PROFILE
|
||||||
|
|
||||||
# Detect playback start early on
|
|
||||||
self.command_pipeline = Monitor_Window(self)
|
|
||||||
self.command_pipeline.start()
|
|
||||||
|
|
||||||
# Server auto-detect
|
# Server auto-detect
|
||||||
initialsetup.InitialSetup().setup()
|
initialsetup.InitialSetup().setup()
|
||||||
|
|
||||||
|
# Detect playback start early on
|
||||||
|
self.command_pipeline = Monitor_Window()
|
||||||
|
self.command_pipeline.start()
|
||||||
|
|
||||||
# Initialize important threads, handing over self for callback purposes
|
# Initialize important threads, handing over self for callback purposes
|
||||||
self.user = UserClient(self)
|
self.user = UserClient()
|
||||||
self.ws = PMS_Websocket(self)
|
self.ws = PMS_Websocket()
|
||||||
self.alexa = Alexa_Websocket(self)
|
self.alexa = Alexa_Websocket()
|
||||||
self.library = LibrarySync(self)
|
self.library = LibrarySync()
|
||||||
self.plexCompanion = PlexCompanion(self)
|
self.plexCompanion = PlexCompanion()
|
||||||
self.playqueue = Playqueue(self)
|
self.specialMonitor = SpecialMonitor()
|
||||||
self.playback_starter = Playback_Starter(self)
|
self.playback_starter = Playback_Starter()
|
||||||
|
self.playqueue = PlayqueueMonitor()
|
||||||
if settings('enableTextureCache') == "true":
|
if settings('enableTextureCache') == "true":
|
||||||
self.image_cache_thread = Image_Cache_Thread()
|
self.image_cache_thread = Image_Cache_Thread()
|
||||||
|
|
||||||
plx = PlexAPI.PlexAPI()
|
|
||||||
|
|
||||||
welcome_msg = True
|
welcome_msg = True
|
||||||
counter = 0
|
counter = 0
|
||||||
while not __stop_PKC():
|
while not __stop_PKC():
|
||||||
|
|
||||||
if window('plex_kodiProfile') != kodiProfile:
|
if window('plex_kodiProfile') != kodiProfile:
|
||||||
# Profile change happened, terminate this thread and others
|
# Profile change happened, terminate this thread and others
|
||||||
log.info("Kodi profile was: %s and changed to: %s. "
|
LOG.info("Kodi profile was: %s and changed to: %s. "
|
||||||
"Terminating old PlexKodiConnect thread."
|
"Terminating old PlexKodiConnect thread.",
|
||||||
% (kodiProfile,
|
kodiProfile, window('plex_kodiProfile'))
|
||||||
window('plex_kodiProfile')))
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Before proceeding, need to make sure:
|
# Before proceeding, need to make sure:
|
||||||
|
@ -191,11 +150,8 @@ class Service():
|
||||||
time=2000,
|
time=2000,
|
||||||
sound=False)
|
sound=False)
|
||||||
# Start monitoring kodi events
|
# Start monitoring kodi events
|
||||||
self.kodimonitor_running = KodiMonitor(self)
|
self.kodimonitor_running = KodiMonitor()
|
||||||
# Start playqueue client
|
self.specialMonitor.start()
|
||||||
if not self.playqueue_running:
|
|
||||||
self.playqueue_running = True
|
|
||||||
self.playqueue.start()
|
|
||||||
# Start the Websocket Client
|
# Start the Websocket Client
|
||||||
if not self.ws_running:
|
if not self.ws_running:
|
||||||
self.ws_running = True
|
self.ws_running = True
|
||||||
|
@ -216,6 +172,7 @@ class Service():
|
||||||
if not self.playback_starter_running:
|
if not self.playback_starter_running:
|
||||||
self.playback_starter_running = True
|
self.playback_starter_running = True
|
||||||
self.playback_starter.start()
|
self.playback_starter.start()
|
||||||
|
self.playqueue.start()
|
||||||
if (not self.image_cache_thread_running and
|
if (not self.image_cache_thread_running and
|
||||||
settings('enableTextureCache') == "true"):
|
settings('enableTextureCache') == "true"):
|
||||||
self.image_cache_thread_running = True
|
self.image_cache_thread_running = True
|
||||||
|
@ -225,7 +182,7 @@ class Service():
|
||||||
# Alert user is not authenticated and suppress future
|
# Alert user is not authenticated and suppress future
|
||||||
# warning
|
# warning
|
||||||
self.warn_auth = False
|
self.warn_auth = False
|
||||||
log.warn("Not authenticated yet.")
|
LOG.warn("Not authenticated yet.")
|
||||||
|
|
||||||
# User access is restricted.
|
# User access is restricted.
|
||||||
# Keep verifying until access is granted
|
# Keep verifying until access is granted
|
||||||
|
@ -249,7 +206,7 @@ class Service():
|
||||||
if server is False:
|
if server is False:
|
||||||
# No server info set in add-on settings
|
# No server info set in add-on settings
|
||||||
pass
|
pass
|
||||||
elif plx.CheckConnection(server, verifySSL=True) is False:
|
elif check_connection(server, verifySSL=True) is False:
|
||||||
# Server is offline or cannot be reached
|
# Server is offline or cannot be reached
|
||||||
# Alert the user and suppress future warning
|
# Alert the user and suppress future warning
|
||||||
if self.server_online:
|
if self.server_online:
|
||||||
|
@ -257,7 +214,7 @@ class Service():
|
||||||
window('plex_online', value="false")
|
window('plex_online', value="false")
|
||||||
# Suspend threads
|
# Suspend threads
|
||||||
state.SUSPEND_LIBRARY_THREAD = True
|
state.SUSPEND_LIBRARY_THREAD = True
|
||||||
log.error("Plex Media Server went offline")
|
LOG.error("Plex Media Server went offline")
|
||||||
if settings('show_pms_offline') == 'true':
|
if settings('show_pms_offline') == 'true':
|
||||||
dialog('notification',
|
dialog('notification',
|
||||||
lang(33001),
|
lang(33001),
|
||||||
|
@ -269,9 +226,9 @@ class Service():
|
||||||
if counter > 20:
|
if counter > 20:
|
||||||
counter = 0
|
counter = 0
|
||||||
setup = initialsetup.InitialSetup()
|
setup = initialsetup.InitialSetup()
|
||||||
tmp = setup.PickPMS()
|
tmp = setup.pick_pms()
|
||||||
if tmp is not None:
|
if tmp is not None:
|
||||||
setup.WritePMStoSettings(tmp)
|
setup.write_pms_to_settings(tmp)
|
||||||
else:
|
else:
|
||||||
# Server is online
|
# Server is online
|
||||||
counter = 0
|
counter = 0
|
||||||
|
@ -291,7 +248,7 @@ class Service():
|
||||||
icon='{plex}',
|
icon='{plex}',
|
||||||
time=5000,
|
time=5000,
|
||||||
sound=False)
|
sound=False)
|
||||||
log.info("Server %s is online and ready." % server)
|
LOG.info("Server %s is online and ready.", server)
|
||||||
window('plex_online', value="true")
|
window('plex_online', value="true")
|
||||||
if state.AUTHENTICATED:
|
if state.AUTHENTICATED:
|
||||||
# Server got offline when we were authenticated.
|
# Server got offline when we were authenticated.
|
||||||
|
@ -316,29 +273,25 @@ class Service():
|
||||||
|
|
||||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||||
state.STOP_PKC = True
|
state.STOP_PKC = True
|
||||||
try:
|
|
||||||
downloadutils.DownloadUtils().stopSession()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
window('plex_service_started', clear=True)
|
window('plex_service_started', clear=True)
|
||||||
log.info("======== STOP %s ========" % v.ADDON_NAME)
|
LOG.info("======== STOP %s ========", v.ADDON_NAME)
|
||||||
|
|
||||||
|
|
||||||
# Safety net - Kody starts PKC twice upon first installation!
|
# Safety net - Kody starts PKC twice upon first installation!
|
||||||
if window('plex_service_started') == 'true':
|
if window('plex_service_started') == 'true':
|
||||||
exit = True
|
EXIT = True
|
||||||
else:
|
else:
|
||||||
window('plex_service_started', value='true')
|
window('plex_service_started', value='true')
|
||||||
exit = False
|
EXIT = False
|
||||||
|
|
||||||
# Delay option
|
# Delay option
|
||||||
delay = int(settings('startupDelay'))
|
DELAY = int(settings('startupDelay'))
|
||||||
|
|
||||||
log.info("Delaying Plex startup by: %s sec..." % delay)
|
LOG.info("Delaying Plex startup by: %s sec...", DELAY)
|
||||||
if exit:
|
if EXIT:
|
||||||
log.error('PKC service.py already started - exiting this instance')
|
LOG.error('PKC service.py already started - exiting this instance')
|
||||||
elif delay and Monitor().waitForAbort(delay):
|
elif DELAY and Monitor().waitForAbort(DELAY):
|
||||||
# Start the service
|
# Start the service
|
||||||
log.info("Abort requested while waiting. PKC not started.")
|
LOG.info("Abort requested while waiting. PKC not started.")
|
||||||
else:
|
else:
|
||||||
Service().ServiceEntryPoint()
|
Service().ServiceEntryPoint()
|
||||||
|
|
Loading…
Reference in a new issue