Merge branch 'hotfixes' into translations

This commit is contained in:
croneter 2018-04-03 17:20:28 +02:00
commit fe952afa3e
55 changed files with 8105 additions and 8495 deletions

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

Binary file not shown.

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)))

View file

@ -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

View file

@ -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 ##===----")

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
View 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

View file

@ -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 ----====#")

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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])

View file

@ -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):

View file

@ -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
View 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))

View file

@ -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 ##===----")

View file

@ -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)

View file

@ -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()

View file

@ -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')

View file

@ -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 ##===----")

View file

@ -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
View 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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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):
""" """

View file

@ -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'

View file

@ -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

View file

@ -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'
}

View file

@ -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

View file

@ -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]:

View file

@ -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

View file

@ -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-->

View file

@ -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()