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)
[![beta version](https://img.shields.io/badge/beta_version-1.8.14-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
[![stable version](https://img.shields.io/badge/stable_version-1.8.18-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
[![beta version](https://img.shields.io/badge/beta_version-2.0.16-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
@ -15,6 +15,11 @@ PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly cu
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
### UPDATE YOUR PKC REPO TO RECEIVE UPDATES!
Unfortunately, the PKC Kodi repository had to move because it stopped working (thanks https://bintray.com). If you installed PKC before December 15, 2017, you need to [**MANUALLY** update the repo once](https://github.com/croneter/PlexKodiConnect/wiki/Update-PKC-Repository).
### Please Help Translating
Please help translate PlexKodiConnect into your language: [Transifex.com](https://www.transifex.com/croneter/pkc)
@ -69,15 +74,16 @@ PKC synchronizes your media from your Plex server to the native Kodi database. H
+ Chinese Simplified, thanks @everdream
+ Norwegian, thanks @mjorud
+ Portuguese, thanks @goncalo532
+ Russian, thanks @UncleStark
+ [Please help translating](https://www.transifex.com/croneter/pkc)
### Download and Installation
Install PKC via the PlexKodiConnect Kodi repository below (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically.
Install PKC via the PlexKodiConnect Kodi repository download button just below (do NOT use the standard GitHub download!). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically.
| Stable version | Beta version |
|----------------|--------------|
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) |
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) |
### Additional Artwork
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
@ -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)
### 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.
**Full disclaimer:** I will see your name and address on my PayPal account. Rest assured that I will not share this with anyone.
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 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)
![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
[![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"?>
<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>
<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>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides>
@ -13,7 +15,7 @@
<item>
<label>30401</label>
<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>
</extension>
<extension point="xbmc.addon.metadata">
@ -59,7 +61,148 @@
<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>
<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
- Fix IndexError e.g. for channels if stream info missing
- 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):
- Greatly speed up displaying context menu
- Fix IndexError e.g. for channels if stream info missing

View file

@ -1,41 +1,48 @@
# -*- coding: utf-8 -*-
###############################################################################
from os import path as os_path
from sys import path as sys_path
from sys import listitem
from urllib import urlencode
from xbmcaddon import Addon
from xbmc import translatePath, sleep, log, LOGERROR
from xbmc import getCondVisibility, sleep
from xbmcgui import Window
_addon = Addon(id='plugin.video.plexkodiconnect')
try:
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
_addon_path = _addon.getAddonInfo('path').decode()
try:
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
_base_resource = translatePath(os_path.join(
_addon_path,
'resources',
'lib')).decode()
sys_path.append(_base_resource)
from pickler import unpickle_me, pickl_window
###############################################################################
if __name__ == "__main__":
win = Window(10000)
while win.getProperty('plex_command'):
def _get_kodi_type():
kodi_type = listitem.getVideoInfoTag().getMediaType().decode('utf-8')
if not kodi_type:
if getCondVisibility('Container.Content(albums)'):
kodi_type = "album"
elif getCondVisibility('Container.Content(artists)'):
kodi_type = "artist"
elif getCondVisibility('Container.Content(songs)'):
kodi_type = "song"
elif getCondVisibility('Container.Content(pictures)'):
kodi_type = "picture"
return kodi_type
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)
win.setProperty('plex_command', 'CONTEXT_menu')
while not pickl_window('plex_result'):
sleep(50)
result = unpickle_me()
if result is None:
log('PLEX.%s: Error encountered, aborting' % __name__, level=LOGERROR)
window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args))
if __name__ == "__main__":
main()

View file

@ -32,7 +32,7 @@ sys_path.append(_base_resource)
###############################################################################
import entrypoint
from utils import window, reset, passwordsXML, language as lang, dialog, \
from utils import window, reset, passwords_xml, language as lang, dialog, \
plex_command
from pickler import unpickle_me, pickl_window
from PKC_listitem import convert_PKC_to_listitem
@ -115,7 +115,7 @@ class Main():
entrypoint.resetAuth()
elif mode == 'passwords':
passwordsXML()
passwords_xml()
elif mode == 'switchuser':
entrypoint.switchPlexUser()

BIN
empty_video.mp4 Normal file

Binary file not shown.

View file

@ -23,10 +23,19 @@ msgctxt "#30000"
msgid "Server Address (IP)"
msgstr ""
msgctxt "#30001"
msgid "Searching for PMS"
msgstr ""
msgctxt "#30002"
msgid "Preferred playback method"
msgstr ""
# Warning displayed if Kodi setting is enabled. Be sure to escape the quotes again! The exact wording can be found in the Kodi settings, player settings, videos
msgctxt "#30003"
msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?"
msgstr ""
msgctxt "#30004"
msgid "Log level"
msgstr ""
@ -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."
msgstr ""
# Error message displayed when verifying Direct Path sync paths passed by Plex
msgctxt "#39031"
msgid "Kodi can't locate file: "
msgstr ""
msgctxt "#39032"
msgid "Please verify the path. You may need to verify your network credentials in the add-on settings or use different Plex paths. Stop syncing?"
msgid "Kodi cannot locate the file %s. Please verify your PKC settings. Stop syncing?"
msgstr ""
msgctxt "#39033"
@ -1556,6 +1562,11 @@ msgctxt "#39064"
msgid "Recently Added: Also show already watched episodes"
msgstr ""
# PKC settings, Appearance Tweaks
msgctxt "#39065"
msgid "Force-refresh Kodi skin on stopping playback"
msgstr ""
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)"
msgstr ""
@ -1666,10 +1677,6 @@ msgctxt "#39213"
msgid "is offline"
msgstr ""
msgctxt "#39214"
msgid "Even though we signed in to plex.tv, we could not authorize for PMS"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr ""
@ -1851,10 +1858,6 @@ msgctxt "#39601"
msgid "Could not stop the database from running. Please try again later."
msgstr ""
msgctxt "#39602"
msgid "Remove all cached artwork? (recommended!)"
msgstr ""
msgctxt "#39603"
msgid "Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended and unnecessary!)"
msgstr ""

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
import Queue
from Queue import Empty
from socket import SHUT_RDWR
from urllib import urlencode
from xbmc import sleep, executebuiltin
from utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from utils import settings, thread_methods, language as lang, dialog
from plexbmchelper import listener, plexgdm, subscribers, httppersist
from plexbmchelper.subscribers import LOCKER
from PlexFunctions import ParseContainerKey, GetPlexMetadata, DownloadChunks
from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml
from playlist_func import get_pms_playqueue, get_plextype_from_xml, \
get_playlist_details_from_xml
from playback import playback_triage, play_xml
import json_rpc as js
import player
import variables as v
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'])
class PlexCompanion(Thread):
"""
Plex Companion monitoring class. Invoke only once
"""
def __init__(self, callback=None):
log.info("----===## Starting PlexCompanion ##===----")
if callback is not None:
self.mgr = callback
self.settings = plexsettings.getSettings()
def __init__(self):
LOG.info("----===## Starting PlexCompanion ##===----")
# Init Plex Companion queue
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings)
log.debug("Registration string is:\n%s"
% self.client.getClientDetails())
self.client.clientDetails()
LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
# kodi player instance
self.player = player.Player()
self.player = player.PKC_Player()
self.httpd = False
self.subscription_manager = None
Thread.__init__(self)
def _getStartItem(self, string):
"""
Grabs the Plex id from e.g. '/library/metadata/12987'
and returns the tuple (typus, id) where typus is either 'queueId' or
'plexId' and id is the corresponding id as a string
"""
typus = 'plexId'
if string.startswith('/library/metadata'):
try:
string = string.split('/')[3]
except IndexError:
string = ''
@LOCKER.lockthis
def _process_alexa(self, data):
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata for: %s', data)
return
api = API(xml[0])
if api.plex_type() == v.PLEX_TYPE_ALBUM:
LOG.debug('Plex music album detected')
PQ.init_playqueue_from_plex_children(
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:
log.error('Unknown string! %s' % string)
return typus, string
state.PLEX_TRANSIENT_TOKEN = data.get('token')
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.
{'action': 'playlist',
@ -75,105 +185,26 @@ class PlexCompanion(Thread):
'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
'type': 'video'}}
"""
log.debug('Processing: %s' % task)
LOG.debug('Processing: %s', task)
data = task['data']
# Get the token of the user flinging media (might be different one)
token = data.get('token')
if task['action'] == 'alexa':
# e.g. Alexa
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
log.error('Could not download Plex metadata')
return
api = API(xml[0])
if api.getType() == v.PLEX_TYPE_ALBUM:
log.debug('Plex music album detected')
queue = self.mgr.playqueue.init_playqueue_from_plex_children(
api.getRatingKey())
queue.plex_transient_token = token
else:
state.PLEX_TRANSIENT_TOKEN = token
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true',
'node': 'false'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
# E.g. watch later initiated by Companion
state.PLEX_TRANSIENT_TOKEN = token
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
self._process_node(data)
elif task['action'] == 'playlist':
# Get the playqueue ID
try:
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
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
# example data: {'playQueueID': '8475', 'commandID': '11'}
xml = get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
log.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear()
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
data['playQueueID'])
self._process_refresh(data)
elif task['action'] == 'setStreams':
self._process_streams(data)
def run(self):
# Ensure that sockets will be closed no matter what
"""
Ensure that sockets will be closed no matter what
"""
try:
self.__run()
self._run()
finally:
try:
self.httpd.socket.shutdown(SHUT_RDWR)
@ -184,24 +215,20 @@ class PlexCompanion(Thread):
self.httpd.socket.close()
except AttributeError:
pass
log.info("----===## Plex Companion stopped ##===----")
LOG.info("----===## Plex Companion stopped ##===----")
def __run(self):
self.httpd = False
def _run(self):
httpd = self.httpd
# Cache for quicker while loops
client = self.client
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
stopped = self.stopped
suspended = self.suspended
# Start up instances
requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100)
self.queue = queue
request_mgr = httppersist.RequestMgr()
subscription_manager = subscribers.SubscriptionMgr(request_mgr,
self.player)
self.subscription_manager = subscription_manager
if settings('plexCompanion') == 'true':
# Start up httpd
@ -210,82 +237,74 @@ class PlexCompanion(Thread):
try:
httpd = listener.ThreadedHTTPServer(
client,
subscriptionManager,
jsonClass,
self.settings,
queue,
('', self.settings['myport']),
subscription_manager,
('', v.COMPANION_PORT),
listener.MyHandler)
httpd.timeout = 0.95
break
except:
log.error("Unable to start PlexCompanion. Traceback:")
LOG.error("Unable to start PlexCompanion. Traceback:")
import traceback
log.error(traceback.print_exc())
LOG.error(traceback.print_exc())
sleep(3000)
if start_count == 3:
log.error("Error: Unable to start web helper.")
LOG.error("Error: Unable to start web helper.")
httpd = False
break
start_count += 1
else:
log.info('User deactivated Plex Companion')
LOG.info('User deactivated Plex Companion')
client.start_all()
message_count = 0
if httpd:
t = Thread(target=httpd.handle_request)
thread = Thread(target=httpd.handle_request)
while not thread_stopped():
while not stopped():
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
while thread_suspended():
if thread_stopped():
while suspended():
if stopped():
break
sleep(1000)
try:
message_count += 1
if httpd:
if not t.isAlive():
if not thread.isAlive():
# Use threads cause the method will stall
t = Thread(target=httpd.handle_request)
t.start()
thread = Thread(target=httpd.handle_request)
thread.start()
if message_count == 3000:
message_count = 0
if client.check_client_registration():
log.debug("Client is still registered")
LOG.debug('Client is still registered')
else:
log.debug("Client is no longer registered. "
"Plex Companion still running on port %s"
% self.settings['myport'])
LOG.debug('Client is no longer registered. Plex '
'Companion still running on port %s',
v.COMPANION_PORT)
client.register_as_client()
# Get and set servers
if message_count % 30 == 0:
subscriptionManager.serverlist = client.getServerList()
subscriptionManager.notify()
subscription_manager.serverlist = client.getServerList()
subscription_manager.notify()
if not httpd:
message_count = 0
except:
log.warn("Error in loop, continuing anyway. Traceback:")
LOG.warn("Error in loop, continuing anyway. Traceback:")
import traceback
log.warn(traceback.format_exc())
LOG.warn(traceback.format_exc())
# See if there's anything we need to process
try:
task = queue.get(block=False)
except Queue.Empty:
task = state.COMPANION_QUEUE.get(block=False)
except Empty:
pass
else:
# Got instructions, process them
self.processTasks(task)
queue.task_done()
self._process_tasks(task)
state.COMPANION_QUEUE.task_done()
# Don't sleep
continue
sleep(50)
subscription_manager.signal_stop()
client.stop_all()

View file

@ -1,21 +1,31 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from urllib import urlencode
from urllib import urlencode, quote_plus
from ast import literal_eval
from urlparse import urlparse, parse_qsl
import re
from re import compile as re_compile
from copy import deepcopy
from time import time
from threading import Thread
import downloadutils
from utils import settings
from xbmc import sleep
from downloadutils import DownloadUtils as DU
from utils import settings, try_encode, try_decode
from variables import PLEX_TO_KODI_TIMEFACTOR
import plex_tv
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
CONTAINERSIZE = int(settings('limitindex'))
REGEX_PLEX_KEY = re.compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
# For discovery of PMS in the local LAN
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
PLEX_GDM_PORT = 32414
PLEX_GDM_MSG = 'M-SEARCH * HTTP/1.0'
###############################################################################
@ -76,30 +86,372 @@ def GetMethodFromPlexType(plexType):
return methods[plexType]
def XbmcItemtypes():
return ['photo', 'video', 'audio']
def PlexItemtypes():
return ['photo', 'video', 'audio']
def PlexLibraryItemtypes():
return ['movie', 'show']
# later add: 'artist', 'photo'
def EmbyItemtypes():
return ['Movie', 'Series', 'Season', 'Episode']
def SelectStreams(url, args):
def GetPlexLoginFromSettings():
"""
Does a PUT request to tell the PMS what audio and subtitle streams we have
chosen.
Returns a dict:
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
Returns strings or unicode
Returns empty strings '' for a setting if not found.
myplexlogin is 'true' if user opted to log into plex.tv (the default)
plexhome is 'true' if plex home is used (the default)
"""
downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type='PUT')
return {
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
}
def check_connection(url, token=None, verifySSL=None):
"""
Checks connection to a Plex server, available at url. Can also be used
to check for connection with plex.tv.
Override SSL to skip the check by setting verifySSL=False
if 'None', SSL will be checked (standard requests setting)
if 'True', SSL settings from file settings are used (False/True)
Input:
url URL to Plex server (e.g. https://192.168.1.1:32400)
token appropriate token to access server. If None is passed,
the current token is used
Output:
False if server could not be reached or timeout occured
200 if connection was successfull
int or other HTML status codes as received from the server
"""
# Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work.
header_options = None
if token is not None:
header_options = {'X-Plex-Token': token}
if verifySSL is True:
verifySSL = None if settings('sslverify') == 'true' else False
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
else:
url = url + '/library/onDeck'
LOG.debug("Checking connection to server %s with verifySSL=%s",
url, verifySSL)
answer = DU().downloadUrl(url,
authenticate=False,
headerOptions=header_options,
verifySSL=verifySSL,
timeout=10)
if answer is None:
LOG.debug("Could not connect to %s", url)
return False
try:
# xml received?
answer.attrib
except AttributeError:
if answer is True:
# Maybe no xml but connection was successful nevertheless
answer = 200
else:
# Success - we downloaded an xml!
answer = 200
# We could connect but maybe were not authenticated. No worries
LOG.debug("Checking connection successfull. Answer: %s", answer)
return answer
def discover_pms(token=None):
"""
Optional parameter:
token token for plex.tv
Returns a list of available PMS to connect to, one entry is the dict:
{
'machineIdentifier' [str] unique identifier of the PMS
'name' [str] name of the PMS
'token' [str] token needed to access that PMS
'ownername' [str] name of the owner of this PMS or None if
the owner itself supplied tries to connect
'product' e.g. 'Plex Media Server' or None
'version' e.g. '1.11.2.4772-3e...' or None
'device': e.g. 'PC' or 'Windows' or None
'platform': e.g. 'Windows', 'Android' or None
'local' [bool] True if plex.tv supplied
'publicAddressMatches'='1'
or if found using Plex GDM in the local LAN
'owned' [bool] True if it's the owner's PMS
'relay' [bool] True if plex.tv supplied 'relay'='1'
'presence' [bool] True if plex.tv supplied 'presence'='1'
'httpsRequired' [bool] True if plex.tv supplied
'httpsRequired'='1'
'scheme' [str] either 'http' or 'https'
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
'port': [str] Port of the PMS, e.g. '32400'
'baseURL': [str] <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):
@ -128,7 +480,7 @@ def GetPlexMetadata(key):
# 'includeConcerts': 1
}
url = url + '?' + urlencode(arguments)
xml = downloadutils.DownloadUtils().downloadUrl(url)
xml = DU().downloadUrl(url)
if xml == 401:
# Either unauthorized (taken care of by doUtils) or PMS under strain
return 401
@ -137,7 +489,7 @@ def GetPlexMetadata(key):
xml.attrib
# Nope we did not receive a valid XML
except AttributeError:
log.error("Error retrieving metadata for %s" % url)
LOG.error("Error retrieving metadata for %s", url)
xml = None
return xml
@ -179,22 +531,21 @@ def DownloadChunks(url):
"""
xml = None
pos = 0
errorCounter = 0
while errorCounter < 10:
error_counter = 0
while error_counter < 10:
args = {
'X-Plex-Container-Size': CONTAINERSIZE,
'X-Plex-Container-Start': pos
}
xmlpart = downloadutils.DownloadUtils().downloadUrl(
url + urlencode(args))
xmlpart = DU().downloadUrl(url + urlencode(args))
# If something went wrong - skip in the hope that it works next time
try:
xmlpart.attrib
except AttributeError:
log.error('Error while downloading chunks: %s'
% (url + urlencode(args)))
LOG.error('Error while downloading chunks: %s',
url + urlencode(args))
pos += CONTAINERSIZE
errorCounter += 1
error_counter += 1
continue
# Very first run: starting xml (to retain data in xml's root!)
@ -212,8 +563,8 @@ def DownloadChunks(url):
if len(xmlpart) < CONTAINERSIZE:
break
pos += CONTAINERSIZE
if errorCounter == 10:
log.error('Fatal error while downloading chunks for %s' % url)
if error_counter == 10:
LOG.error('Fatal error while downloading chunks for %s', url)
return None
return xml
@ -261,8 +612,7 @@ def get_plex_sections():
"""
Returns all Plex sections (libraries) of the PMS as an etree xml
"""
return downloadutils.DownloadUtils().downloadUrl(
'{server}/library/sections')
return DU().downloadUrl('{server}/library/sections')
def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
@ -281,26 +631,16 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie',
}
if trailers is True:
args['extrasPrefixCount'] = settings('trailerNumber')
xml = downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), action_type="POST")
xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST")
try:
xml[0].tag
except (IndexError, TypeError, AttributeError):
log.error("Error retrieving metadata for %s" % url)
LOG.error("Error retrieving metadata for %s", url)
return None
return xml
def getPlexRepeat(kodiRepeat):
plexRepeat = {
'off': '0',
'one': '1',
'all': '2' # does this work?!?
}
return plexRepeat.get(kodiRepeat)
def PMSHttpsEnabled(url):
def _pms_https_enabled(url):
"""
Returns True if the PMS can talk https, False otherwise.
None if error occured, e.g. the connection timed out
@ -312,21 +652,20 @@ def PMSHttpsEnabled(url):
Prefers HTTPS over HTTP
"""
doUtils = downloadutils.DownloadUtils().downloadUrl
res = doUtils('https://%s/identity' % url,
authenticate=False,
verifySSL=False)
res = DU().downloadUrl('https://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res.attrib
except AttributeError:
# Might have SSL deactivated. Try with http
res = doUtils('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
res = DU().downloadUrl('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res.attrib
except AttributeError:
log.error("Could not contact PMS %s" % url)
LOG.error("Could not contact PMS %s", url)
return None
else:
# Received a valid XML. Server wants to talk HTTP
@ -342,17 +681,17 @@ def GetMachineIdentifier(url):
Returns None if something went wrong
"""
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url,
authenticate=False,
verifySSL=False,
timeout=10)
xml = DU().downloadUrl('%s/identity' % url,
authenticate=False,
verifySSL=False,
timeout=10)
try:
machineIdentifier = xml.attrib['machineIdentifier']
except (AttributeError, KeyError):
log.error('Could not get the PMS machineIdentifier for %s' % url)
LOG.error('Could not get the PMS machineIdentifier for %s', url)
return None
log.debug('Found machineIdentifier %s for the PMS %s'
% (machineIdentifier, url))
LOG.debug('Found machineIdentifier %s for the PMS %s',
machineIdentifier, url)
return machineIdentifier
@ -372,9 +711,8 @@ def GetPMSStatus(token):
or an empty dict.
"""
answer = {}
xml = downloadutils.DownloadUtils().downloadUrl(
'{server}/status/sessions',
headerOptions={'X-Plex-Token': token})
xml = DU().downloadUrl('{server}/status/sessions',
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except AttributeError:
@ -412,8 +750,8 @@ def scrobble(ratingKey, state):
url = "{server}/:/unscrobble?" + urlencode(args)
else:
return
downloadutils.DownloadUtils().downloadUrl(url)
log.info("Toggled watched state for Plex item %s" % ratingKey)
DU().downloadUrl(url)
LOG.info("Toggled watched state for Plex item %s", ratingKey)
def delete_item_from_pms(plexid):
@ -423,24 +761,76 @@ def delete_item_from_pms(plexid):
Returns True if successful, False otherwise
"""
if downloadutils.DownloadUtils().downloadUrl(
'{server}/library/metadata/%s' % plexid,
action_type="DELETE") is True:
log.info('Successfully deleted Plex id %s from the PMS' % plexid)
if DU().downloadUrl('{server}/library/metadata/%s' % plexid,
action_type="DELETE") is True:
LOG.info('Successfully deleted Plex id %s from the PMS', plexid)
return True
else:
log.error('Could not delete Plex id %s from the PMS' % plexid)
return False
LOG.error('Could not delete Plex id %s from the PMS', plexid)
return False
def get_PMS_settings(url, token):
"""
Retrieve the PMS' settings via <url>/:/
Retrieve the PMS' settings via <url>/:/prefs
Call with url: scheme://ip:port
"""
return downloadutils.DownloadUtils().downloadUrl(
return DU().downloadUrl(
'%s/:/prefs' % url,
authenticate=False,
verifySSL=False,
headerOptions={'X-Plex-Token': token} if token else None)
def GetUserArtworkURL(username):
"""
Returns the URL for the user's Avatar. Or False if something went
wrong.
"""
users = plex_tv.list_home_users(settings('plexToken'))
url = ''
# If an error is encountered, set to False
if not users:
LOG.info("Couldnt get user from plex.tv. No URL for user avatar")
return False
for user in users:
if username in user['title']:
url = user['thumb']
LOG.debug("Avatar url for user %s is: %s", username, url)
return url
def transcode_image_path(key, AuthToken, path, width, height):
"""
Transcode Image support
parameters:
key
AuthToken
path - source path of current XML: path[srcXML]
width
height
result:
final path to image file
"""
# external address - can we get a transcoding request for external images?
if key.startswith('http://') or key.startswith('https://'):
path = key
elif key.startswith('/'): # internal full path.
path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key
path = 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 -*-
###############################################################################
import logging
from json import dumps, loads
import requests
from logging import getLogger
from Queue import Queue, Empty
from shutil import rmtree
from urllib import quote_plus, unquote
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 utils import window, settings, language as lang, kodiSQL, tryEncode, \
thread_methods, dialog, exists_dir, tryDecode
from utils import window, settings, language as lang, kodi_sql, try_encode, \
thread_methods, dialog, exists_dir, try_decode
import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
# Disable annoying requests warnings
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
###############################################################################
log = logging.getLogger("PLEX."+__name__)
###############################################################################
ARTWORK_QUEUE = Queue()
def setKodiWebServerDetails():
"""
Get the Kodi webserver details - used to set the texture cache
"""
xbmc_port = None
xbmc_username = None
xbmc_password = None
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):
@ -130,8 +37,6 @@ def double_urldecode(text):
'DB_SCAN',
'STOP_SYNC'])
class Image_Cache_Thread(Thread):
xbmc_host = 'localhost'
xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails()
sleep_between = 50
# Potentially issues with limited number of threads
# Hence let Kodi wait till download is successful
@ -142,17 +47,17 @@ class Image_Cache_Thread(Thread):
Thread.__init__(self)
def run(self):
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
stopped = self.stopped
suspended = self.suspended
queue = self.queue
sleep_between = self.sleep_between
while not thread_stopped():
while not stopped():
# In the event the server goes offline
while thread_suspended():
while suspended():
# Set in service.py
if thread_stopped():
if stopped():
# Abort was requested while waiting. We should exit
log.info("---===### Stopped Image_Cache_Thread ###===---")
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
return
sleep(1000)
try:
@ -165,43 +70,45 @@ class Image_Cache_Thread(Thread):
try:
requests.head(
url="http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, url),
auth=(self.xbmc_username, self.xbmc_password),
% (state.WEBSERVER_HOST,
state.WEBSERVER_PORT,
url),
auth=(state.WEBSERVER_USERNAME,
state.WEBSERVER_PASSWORD),
timeout=self.timeout)
except requests.Timeout:
# We don't need the result, only trigger Kodi to start the
# download. All is well
break
except requests.ConnectionError:
if thread_stopped():
if stopped():
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
# Wait before trying again
if sleeptime > 5:
log.error('Repeatedly got ConnectionError for url %s'
% double_urldecode(url))
LOG.error('Repeatedly got ConnectionError for url %s',
double_urldecode(url))
break
log.debug('Were trying too hard to download art, server '
LOG.debug('Were trying too hard to download art, server '
'over-loaded. Sleep %s seconds before trying '
'again to download %s'
% (2**sleeptime, double_urldecode(url)))
'again to download %s',
2**sleeptime, double_urldecode(url))
sleep((2**sleeptime)*1000)
sleeptime += 1
continue
except Exception as e:
log.error('Unknown exception for url %s: %s'
% (double_urldecode(url), e))
LOG.error('Unknown exception for url %s: %s'.
double_urldecode(url), e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
LOG.error("Traceback:\n%s", traceback.format_exc())
break
# We did not even get a timeout
break
queue.task_done()
log.debug('Cached art: %s' % double_urldecode(url))
# Sleep for a bit to reduce CPU strain
sleep(sleep_between)
log.info("---===### Stopped Image_Cache_Thread ###===---")
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
class Artwork():
@ -217,18 +124,19 @@ class Artwork():
if not dialog('yesno', "Image Texture Cache", lang(39250)):
return
log.info("Doing Image Cache Sync")
LOG.info("Doing Image Cache Sync")
# ask to rest all existing or not
if dialog('yesno', "Image Texture Cache", lang(39251)):
log.info("Resetting all cache data first")
LOG.info("Resetting all cache data first")
# Remove all existing textures first
path = tryDecode(translatePath("special://thumbnails/"))
path = try_decode(translatePath("special://thumbnails/"))
if exists_dir(path):
rmtree(path, ignore_errors=True)
self.restore_cache_directories()
# remove all existing data from texture DB
connection = kodiSQL('texture')
connection = kodi_sql('texture')
cursor = connection.cursor()
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
cursor.execute(query, ('table', ))
@ -241,191 +149,120 @@ class Artwork():
connection.close()
# Cache all entries in video DB
connection = kodiSQL('video')
connection = kodi_sql('video')
cursor = connection.cursor()
# dont include actors
query = "SELECT url FROM art WHERE media_type != ?"
cursor.execute(query, ('actor', ))
result = cursor.fetchall()
total = len(result)
log.info("Image cache sync about to process %s video images" % total)
LOG.info("Image cache sync about to process %s video images", total)
connection.close()
for url in result:
self.cacheTexture(url[0])
self.cache_texture(url[0])
# Cache all entries in music DB
connection = kodiSQL('music')
connection = kodi_sql('music')
cursor = connection.cursor()
cursor.execute("SELECT url FROM art")
result = cursor.fetchall()
total = len(result)
log.info("Image cache sync about to process %s music images" % total)
LOG.info("Image cache sync about to process %s music images", total)
connection.close()
for url in result:
self.cacheTexture(url[0])
self.cache_texture(url[0])
def cacheTexture(self, url):
# Cache a single image url to the texture cache
def cache_texture(self, url):
'''
Cache a single image url to the texture cache
'''
if url and self.enableTextureCache:
self.queue.put(double_urlencode(tryEncode(url)))
self.queue.put(double_urlencode(try_encode(url)))
def addArtwork(self, artwork, kodiId, mediaType, cursor):
# Kodi conversion table
kodiart = {
'Primary': ["thumb", "poster"],
'Banner': "banner",
'Logo': "clearlogo",
'Art': "clearart",
'Thumb': "landscape",
'Disc': "discart",
'Backdrop': "fanart",
'BoxRear': "poster"
}
def modify_artwork(self, artworks, kodi_id, kodi_type, cursor):
"""
Pass in an artworks dict (see PlexAPI) to set an items artwork.
"""
for kodi_art, url in artworks.iteritems():
self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor)
# Artwork is a dictionary
for art in artwork:
if art == "Backdrop":
# Backdrop entry is a list
# Process extra fanart for artwork downloader (fanart, fanart1,
# fanart2...)
backdrops = artwork[art]
backdropsNumber = len(backdrops)
query = ' '.join((
"SELECT url",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type LIKE ?"
))
cursor.execute(query, (kodiId, mediaType, "fanart%",))
rows = cursor.fetchall()
if len(rows) > backdropsNumber:
# More backdrops in database. Delete extra fanart.
query = ' '.join((
"DELETE FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type LIKE ?"
))
cursor.execute(query, (kodiId, mediaType, "fanart_",))
# Process backdrops and extra fanart
index = ""
for backdrop in backdrops:
self.addOrUpdateArt(
imageUrl=backdrop,
kodiId=kodiId,
mediaType=mediaType,
imageType="%s%s" % ("fanart", index),
cursor=cursor)
if backdropsNumber > 1:
try: # Will only fail on the first try, str to int.
index += 1
except TypeError:
index = 1
elif art == "Primary":
# Primary art is processed as thumb and poster for Kodi.
for artType in kodiart[art]:
self.addOrUpdateArt(
imageUrl=artwork[art],
kodiId=kodiId,
mediaType=mediaType,
imageType=artType,
cursor=cursor)
elif kodiart.get(art):
# Process the rest artwork type that Kodi can use
self.addOrUpdateArt(
imageUrl=artwork[art],
kodiId=kodiId,
mediaType=mediaType,
imageType=kodiart[art],
cursor=cursor)
def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor):
if not imageUrl:
# Possible that the imageurl is an empty string
return
query = ' '.join((
"SELECT url",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type = ?"
))
cursor.execute(query, (kodiId, mediaType, imageType,))
def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor):
"""
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
Kodi art table for item kodi_id/kodi_type. Will also cache everything
except actor portraits.
"""
query = '''
SELECT url FROM art
WHERE media_id = ? AND media_type = ? AND type = ?
LIMIT 1
'''
cursor.execute(query, (kodi_id, kodi_type, kodi_art,))
try:
# Update the artwork
url = cursor.fetchone()[0]
old_url = cursor.fetchone()[0]
except TypeError:
# Add the artwork
log.debug("Adding Art Link for kodiId: %s (%s)"
% (kodiId, imageUrl))
query = (
'''
LOG.debug('Adding Art Link for %s kodi_id %s, kodi_type %s: %s',
kodi_art, kodi_id, kodi_type, url)
query = '''
INSERT INTO art(media_id, media_type, type, url)
VALUES (?, ?, ?, ?)
'''
)
cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
'''
cursor.execute(query, (kodi_id, kodi_type, kodi_art, url))
else:
if url == imageUrl:
if url == old_url:
# Only cache artwork if it changed
return
# Only for the main backdrop, poster
if (window('plex_initialScan') != "true" and
imageType in ("fanart", "poster")):
# Delete current entry before updating with the new one
self.deleteCachedArtwork(url)
log.debug("Updating Art url for %s kodiId %s %s -> (%s)"
% (imageType, kodiId, url, imageUrl))
query = ' '.join((
"UPDATE art",
"SET url = ?",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type = ?"
))
cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
self.delete_cached_artwork(old_url)
LOG.debug("Updating Art url for %s kodi_id %s, kodi_type %s to %s",
kodi_art, kodi_id, kodi_type, url)
query = '''
UPDATE art SET url = ?
WHERE media_id = ? AND media_type = ? AND type = ?
'''
cursor.execute(query, (url, kodi_id, kodi_type, kodi_art))
# Cache fanart and poster in Kodi texture cache
if mediaType != 'actor':
self.cacheTexture(imageUrl)
if kodi_type != 'actor':
self.cache_texture(url)
def deleteArtwork(self, kodiId, mediaType, cursor):
query = ' '.join((
"SELECT url",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?"
))
def delete_artwork(self, kodiId, mediaType, cursor):
query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?'
cursor.execute(query, (kodiId, mediaType,))
rows = cursor.fetchall()
for row in rows:
self.deleteCachedArtwork(row[0])
for row in cursor.fetchall():
self.delete_cached_artwork(row[0])
def deleteCachedArtwork(self, url):
# Only necessary to remove and apply a new backdrop or poster
connection = kodiSQL('texture')
@staticmethod
def delete_cached_artwork(url):
"""
Deleted the cached artwork with path url (if it exists)
"""
connection = kodi_sql('texture')
cursor = connection.cursor()
try:
cursor.execute("SELECT cachedurl FROM texture WHERE url = ?",
cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1",
(url,))
cachedurl = cursor.fetchone()[0]
except TypeError:
log.info("Could not find cached url.")
# Could not find cached url
pass
else:
# Delete thumbnail as well as the entry
path = translatePath("special://thumbnails/%s" % cachedurl)
log.debug("Deleting cached thumbnail: %s" % path)
LOG.debug("Deleting cached thumbnail: %s", path)
if exists(path):
rmtree(tryDecode(path), ignore_errors=True)
rmtree(try_decode(path), ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
finally:
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 -*-
###############################################################################
import logging
from logging import getLogger
from utils import window, settings
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
requests. An authentication option is NOT yet added.
@ -21,6 +21,8 @@ def getXArgsDeviceInfo(options=None):
Inputs:
options: dictionary of options that will override the
standard header options otherwise set.
include_token: set to False if you don't want to include the Plex token
(e.g. for Companion communication)
Output:
header dictionary
"""
@ -41,7 +43,7 @@ def getXArgsDeviceInfo(options=None):
'X-Plex-Client-Identifier': getDeviceId(),
'X-Plex-Provides': 'client,controller,player,pubsub-player',
}
if window('pms_token'):
if include_token and window('pms_token'):
xargs['X-Plex-Token'] = window('pms_token')
if options is not None:
xargs.update(options)
@ -57,24 +59,27 @@ def getDeviceId(reset=False):
If id does not exist, create one and save in Kodi settings file.
"""
if reset is True:
v.PKC_MACHINE_IDENTIFIER = None
window('plex_client_Id', clear=True)
settings('plex_client_Id', value="")
clientId = window('plex_client_Id')
if clientId:
return clientId
client_id = v.PKC_MACHINE_IDENTIFIER
if client_id:
return client_id
clientId = settings('plex_client_Id')
client_id = settings('plex_client_Id')
# Because Kodi appears to cache file settings!!
if clientId != "" and reset is False:
window('plex_client_Id', value=clientId)
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
return clientId
if client_id != "" and reset is False:
v.PKC_MACHINE_IDENTIFIER = client_id
window('plex_client_Id', value=client_id)
log.info("Unique device Id plex_client_Id loaded: %s", client_id)
return client_id
log.info("Generating a new deviceid.")
from uuid import uuid4
clientId = str(uuid4())
settings('plex_client_Id', value=clientId)
window('plex_client_Id', value=clientId)
log.info("Unique device Id plex_client_Id loaded: %s" % clientId)
return clientId
client_id = str(uuid4())
settings('plex_client_Id', value=client_id)
v.PKC_MACHINE_IDENTIFIER = client_id
window('plex_client_Id', value=client_id)
log.info("Unique device Id plex_client_Id generated: %s", client_id)
return client_id

View file

@ -2,7 +2,6 @@
###############################################################################
import logging
from threading import Thread
from Queue import Queue
from xbmc import sleep
@ -10,8 +9,7 @@ from utils import window, thread_methods
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = logging.getLogger("PLEX." + __name__)
###############################################################################
@ -23,17 +21,11 @@ class Monitor_Window(Thread):
Adjusts state.py accordingly
"""
# Borg - multiple instances, shared state
def __init__(self, callback=None):
self.mgr = callback
self.playback_queue = Queue()
Thread.__init__(self)
def run(self):
thread_stopped = self.thread_stopped
queue = self.playback_queue
log.info("----===## Starting Kodi_Play_Client ##===----")
while not thread_stopped():
stopped = self.stopped
queue = state.COMMAND_PIPELINE_QUEUE
LOG.info("----===## Starting Kodi_Play_Client ##===----")
while not stopped():
if window('plex_command'):
value = window('plex_command')
window('plex_command', clear=True)
@ -62,12 +54,15 @@ class Monitor_Window(Thread):
value.replace('PLEX_USERNAME-', '') or None
elif value.startswith('RUN_LIB_SCAN-'):
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
elif value == 'CONTEXT_menu':
queue.put('dummy?mode=context_menu')
elif value.startswith('CONTEXT_menu?'):
queue.put('dummy?mode=context_menu&%s'
% value.replace('CONTEXT_menu?', ''))
elif value.startswith('NAVIGATE'):
queue.put(value.replace('NAVIGATE-', ''))
else:
raise NotImplementedError('%s not implemented' % value)
else:
sleep(50)
# Put one last item into the queue to let playback_starter end
queue.put(None)
log.info("----===## Kodi_Play_Client stopped ##===----")
LOG.info("----===## Kodi_Play_Client stopped ##===----")

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 utils import JSONRPC
from variables import ALEXA_TO_COMPANION
from playqueue import Playqueue
import playqueue as PQ
from PlexFunctions import GetPlexKeyNumber
import json_rpc as js
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
def getPlayers():
info = JSONRPC("Player.GetActivePlayers").execute()['result'] or []
ret = {}
for player in info:
player['playerid'] = int(player['playerid'])
ret[player['type']] = player
return ret
def getPlayerIds():
ret = []
for player in getPlayers().values():
ret.append(player['playerid'])
return ret
def getPlaylistId(typus):
def skip_to(params):
"""
typus: one of the Kodi types, e.g. audio or video
Skip to a specific playlist position.
Returns None if nothing was found
Does not seem to be implemented yet by Plex!
"""
for playlist in getPlaylists():
if playlist.get('type') == typus:
return playlist.get('playlistid')
def getPlaylists():
"""
Returns a list, e.g.
[
{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}
]
"""
return JSONRPC('Playlist.GetPlaylists').execute()
def millisToTime(t):
millis = int(t)
seconds = millis / 1000
minutes = seconds / 60
hours = minutes / 60
seconds = seconds % 60
minutes = minutes % 60
millis = millis % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': millis}
def skipTo(params):
# Does not seem to be implemented yet
playQueueItemID = params.get('playQueueItemID', 'not available')
library, plex_id = GetPlexKeyNumber(params.get('key'))
log.debug('Skipping to playQueueItemID %s, plex_id %s'
% (playQueueItemID, plex_id))
playqueue_item_id = params.get('playQueueItemID')
_, plex_id = GetPlexKeyNumber(params.get('key'))
LOG.debug('Skipping to playQueueItemID %s, plex_id %s',
playqueue_item_id, plex_id)
found = True
playqueues = Playqueue()
for (player, ID) in getPlayers().iteritems():
playqueue = playqueues.get_playqueue_from_type(player)
for player in js.get_players().values():
playqueue = PQ.PLAYQUEUES[player['playerid']]
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
else:
log.debug('Item not found to skip to')
found = False
if found:
for i, item in enumerate(playqueue.items):
if item.plex_id == plex_id:
found = True
break
if found is True:
Player().play(playqueue.kodi_pl, None, False, i)
else:
LOG.error('Item not found to skip to')
def convert_alexa_to_companion(dictionary):
"""
The params passed by Alexa must first be converted to Companion talk
"""
for key in dictionary:
if key in ALEXA_TO_COMPANION:
dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key]
del dictionary[key]
def process_command(request_path, params, queue=None):
def process_command(request_path, params):
"""
queue: Queue() of PlexCompanion.py
"""
if params.get('deviceName') == 'Alexa':
convert_alexa_to_companion(params)
log.debug('Received request_path: %s, params: %s' % (request_path, params))
if "/playMedia" in request_path:
LOG.debug('Received request_path: %s, params: %s', request_path, params)
if request_path == 'player/playback/playMedia':
# We need to tell service.py
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
queue.put({
state.COMPANION_QUEUE.put({
'action': action,
'data': params
})
elif request_path == 'player/playback/refreshPlayQueue':
queue.put({
state.COMPANION_QUEUE.put({
'action': 'refreshPlayQueue',
'data': params
})
elif request_path == "player/playback/setParameters":
if 'volume' in params:
volume = int(params['volume'])
log.debug("Adjusting the volume to %s" % volume)
JSONRPC('Application.SetVolume').execute({"volume": volume})
js.set_volume(int(params['volume']))
else:
log.error('Unknown parameters: %s' % params)
LOG.error('Unknown parameters: %s', params)
elif request_path == "player/playback/play":
for playerid in getPlayerIds():
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
"play": True})
js.play()
elif request_path == "player/playback/pause":
for playerid in getPlayerIds():
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
"play": False})
js.pause()
elif request_path == "player/playback/stop":
for playerid in getPlayerIds():
JSONRPC("Player.Stop").execute({"playerid": playerid})
js.stop()
elif request_path == "player/playback/seekTo":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute(
{"playerid": playerid,
"value": millisToTime(params.get('offset', 0))})
js.seek_to(int(params.get('offset', 0)))
elif request_path == "player/playback/stepForward":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute({"playerid": playerid,
"value": "smallforward"})
js.smallforward()
elif request_path == "player/playback/stepBack":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute({"playerid": playerid,
"value": "smallbackward"})
js.smallbackward()
elif request_path == "player/playback/skipNext":
for playerid in getPlayerIds():
JSONRPC("Player.GoTo").execute({"playerid": playerid,
"to": "next"})
js.skipnext()
elif request_path == "player/playback/skipPrevious":
for playerid in getPlayerIds():
JSONRPC("Player.GoTo").execute({"playerid": playerid,
"to": "previous"})
js.skipprevious()
elif request_path == "player/playback/skipTo":
skipTo(params)
skip_to(params)
elif request_path == "player/navigation/moveUp":
JSONRPC("Input.Up").execute()
js.input_up()
elif request_path == "player/navigation/moveDown":
JSONRPC("Input.Down").execute()
js.input_down()
elif request_path == "player/navigation/moveLeft":
JSONRPC("Input.Left").execute()
js.input_left()
elif request_path == "player/navigation/moveRight":
JSONRPC("Input.Right").execute()
js.input_right()
elif request_path == "player/navigation/select":
JSONRPC("Input.Select").execute()
js.input_select()
elif request_path == "player/navigation/home":
JSONRPC("Input.Home").execute()
js.input_home()
elif request_path == "player/navigation/back":
JSONRPC("Input.Back").execute()
js.input_back()
elif request_path == "player/playback/setStreams":
state.COMPANION_QUEUE.put({
'action': 'setStreams',
'data': params
})
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 -*-
###############################################################################
from logging import getLogger
import logging
import xbmc
import xbmcaddon
from xbmc import getInfoLabel, sleep, executebuiltin
from xbmcaddon import Addon
import plexdb_functions as plexdb
from utils import window, settings, dialog, language as lang, kodiSQL
from utils import window, settings, dialog, language as lang
from dialogs import context
from PlexFunctions import delete_item_from_pms
import playqueue as PQ
import variables as v
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
OPTIONS = {
'Refresh': lang(30410),
@ -32,81 +32,67 @@ OPTIONS = {
class ContextMenu(object):
"""
Class initiated if user opens "Plex options" on a PLEX item using the Kodi
context menu
"""
_selected_option = None
def __init__(self):
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8')
self.item_type = self._get_item_type()
self.item_id = self._get_item_id(self.kodi_id, self.item_type)
log.info("Found item_id: %s item_type: %s"
% (self.item_id, self.item_type))
if not self.item_id:
def __init__(self, kodi_id=None, kodi_type=None):
"""
Simply instantiate with ContextMenu() - no need to call any methods
"""
self.kodi_id = kodi_id
self.kodi_type = kodi_type
self.plex_id = self._get_plex_id(self.kodi_id, self.kodi_type)
if self.kodi_type:
self.plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[self.kodi_type]
else:
self.plex_type = None
LOG.debug("Found plex_id: %s plex_type: %s",
self.plex_id, self.plex_type)
if not self.plex_id:
return
if self._select_menu():
self._action_menu()
if self._selected_option in (OPTIONS['Delete'],
OPTIONS['Refresh']):
log.info("refreshing container")
xbmc.sleep(500)
xbmc.executebuiltin('Container.Refresh')
LOG.info("refreshing container")
sleep(500)
executebuiltin('Container.Refresh')
@classmethod
def _get_item_type(cls):
item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8')
if not item_type:
if xbmc.getCondVisibility('Container.Content(albums)'):
item_type = "album"
elif xbmc.getCondVisibility('Container.Content(artists)'):
item_type = "artist"
elif xbmc.getCondVisibility('Container.Content(songs)'):
item_type = "song"
elif xbmc.getCondVisibility('Container.Content(pictures)'):
item_type = "picture"
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:
@staticmethod
def _get_plex_id(kodi_id, kodi_type):
plex_id = getInfoLabel('ListItem.Property(plexid)') or None
if not plex_id and kodi_id and kodi_type:
with plexdb.Get_Plex_DB() as plexcursor:
item = plexcursor.getItem_byKodiId(kodi_id, item_type)
item = plexcursor.getItem_byKodiId(kodi_id, kodi_type)
try:
item_id = item[0]
plex_id = item[0]
except TypeError:
log.error('Could not get the Plex id for context menu')
return item_id
LOG.info('Could not get the Plex id for context menu')
return plex_id
def _select_menu(self):
# Display select dialog
"""
Display select dialog
"""
options = []
# if user uses direct paths, give option to initiate playback via PMS
if (window('useDirectPaths') == 'true' and
self.item_type in v.KODI_VIDEOTYPES):
if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES:
options.append(OPTIONS['PMS_Play'])
if self.item_type in v.KODI_VIDEOTYPES:
if self.kodi_type in v.KODI_VIDEOTYPES:
options.append(OPTIONS['Transcode'])
# userdata = self.api.getUserData()
# userdata = self.api.userdata()
# if userdata['Favorite']:
# # Remove from emby favourites
# options.append(OPTIONS['RemoveFav'])
# else:
# # Add to emby favourites
# options.append(OPTIONS['AddFav'])
# if self.item_type == "song":
# if self.kodi_type == "song":
# # Set custom song rating
# options.append(OPTIONS['RateSong'])
# Refresh item
# options.append(OPTIONS['Refresh'])
# Delete item, only if the Plex Home main user is logged in
@ -115,103 +101,64 @@ class ContextMenu(object):
options.append(OPTIONS['Delete'])
# Addon settings
options.append(OPTIONS['Addon'])
context_menu = context.ContextMenu(
"script-emby-context.xml",
xbmcaddon.Addon(
'plugin.video.plexkodiconnect').getAddonInfo('path'),
"default", "1080i")
Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
"default",
"1080i")
context_menu.set_options(options)
context_menu.doModal()
if context_menu.is_selected():
self._selected_option = context_menu.get_selected()
return self._selected_option
def _action_menu(self):
"""
Do whatever the user selected to do
"""
selected = self._selected_option
if selected == OPTIONS['Transcode']:
window('plex_forcetranscode', value='true')
state.FORCE_TRANSCODE = True
self._PMS_play()
elif selected == OPTIONS['PMS_Play']:
self._PMS_play()
# elif selected == OPTIONS['Refresh']:
# self.emby.refreshItem(self.item_id)
# elif selected == OPTIONS['AddFav']:
# self.emby.updateUserRating(self.item_id, favourite=True)
# elif selected == OPTIONS['RemoveFav']:
# self.emby.updateUserRating(self.item_id, favourite=False)
# elif selected == OPTIONS['RateSong']:
# self._rate_song()
elif selected == OPTIONS['Addon']:
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
elif selected == OPTIONS['Delete']:
self._delete_item()
def _rate_song(self):
conn = kodiSQL('music')
cursor = conn.cursor()
query = "SELECT rating FROM song WHERE idSong = ?"
cursor.execute(query, (self.kodi_id,))
try:
value = cursor.fetchone()[0]
current_value = int(round(float(value), 0))
except TypeError:
pass
else:
new_value = dialog("numeric", 0, lang(30411), str(current_value))
if new_value > -1:
new_value = int(new_value)
if new_value > 5:
new_value = 5
if settings('enableUpdateSongRating') == "true":
musicutils.updateRatingToFile(new_value, self.api.get_file_path())
query = "UPDATE song SET rating = ? WHERE idSong = ?"
cursor.execute(query, (new_value, self.kodi_id,))
conn.commit()
finally:
cursor.close()
def _delete_item(self):
"""
Delete item on PMS
"""
delete = True
if settings('skipContextMenu') != "true":
if not dialog("yesno", heading=lang(29999), line1=lang(33041)):
log.info("User skipped deletion for: %s", self.item_id)
if not dialog("yesno", heading="{plex}", line1=lang(33041)):
LOG.info("User skipped deletion for: %s", self.plex_id)
delete = False
if delete:
log.info("Deleting Plex item with id %s", self.item_id)
if delete_item_from_pms(self.item_id) is False:
LOG.info("Deleting Plex item with id %s", self.plex_id)
if delete_item_from_pms(self.plex_id) is False:
dialog("ok", heading="{plex}", line1=lang(30414))
def _PMS_play(self):
"""
For using direct paths: Initiates playback using the PMS
"""
window('plex_contextplay', value='true')
params = {
'filename': '/library/metadata/%s' % self.item_id,
'id': self.item_id,
'dbid': self.kodi_id,
'mode': "play"
}
from urllib import urlencode
handle = ("plugin://plugin.video.plexkodiconnect/movies?%s"
% urlencode(params))
xbmc.executebuiltin('RunPlugin(%s)' % handle)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
playqueue.clear()
state.CONTEXT_MENU_PLAY = True
handle = ('plugin://%s/?plex_id=%s&plex_type=%s&mode=play'
% (v.ADDON_TYPE[self.plex_type],
self.plex_id,
self.plex_type))
executebuiltin('RunPlugin(%s)' % handle)

View file

@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import os
from logging import getLogger
from os.path import join
import xbmcgui
import xbmcaddon
from xbmcaddon import Addon
from utils import window
###############################################################################
log = logging.getLogger("PLEX."+__name__)
addon = xbmcaddon.Addon('plugin.video.plexkodiconnect')
LOG = getLogger("PLEX." + __name__)
ADDON = Addon('plugin.video.plexkodiconnect')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
@ -27,16 +25,16 @@ USER_IMAGE = 150
class ContextMenu(xbmcgui.WindowXMLDialog):
_options = []
selected_option = None
def __init__(self, *args, **kwargs):
self._options = []
self.selected_option = None
self.list_ = None
self.background = None
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_options(self, options=[]):
def set_options(self, options=None):
if not options:
options = []
self._options = options
def is_selected(self):
@ -46,17 +44,13 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
return self.selected_option
def onInit(self):
if window('PlexUserImage'):
self.getControl(USER_IMAGE).setImage(window('PlexUserImage'))
height = 479 + (len(self._options) * 55)
log.info("options: %s", self._options)
LOG.debug("options: %s", self._options)
self.list_ = self.getControl(LIST)
for option in self._options:
self.list_.addItem(self._add_listitem(option))
self.background = self._add_editcontrol(730, height, 30, 450)
self.setFocus(self.list_)
@ -64,27 +58,23 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
if self.getFocusId() == LIST:
option = self.list_.getSelectedItem()
self.selected_option = option.getLabel()
log.info('option selected: %s', self.selected_option)
LOG.info('option selected: %s', self.selected_option)
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
def _add_editcontrol(self, x, y, height, width, password=None):
media = join(ADDON.getAddonInfo('path'),
'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlImage(0, 0, 0, 0,
filename=os.path.join(media, "white.png"),
filename=join(media, "white.png"),
aspectRatio=0,
colorDiffuse="ff111111")
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
import requests
from logging import getLogger
import xml.etree.ElementTree as etree
import requests
from utils import settings, window, language as lang, dialog
from utils import window, language as lang, dialog
import clientinfo as client
import state
@ -17,7 +16,7 @@ import state
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@ -47,17 +46,7 @@ class DownloadUtils():
Reserved for userclient only
"""
self.server = server
log.debug("Set server: %s" % server)
def setToken(self, token):
"""
Reserved for userclient only
"""
self.token = token
if token == '':
log.debug('Set token: empty token!')
else:
log.debug("Set token: xxxxxxx")
LOG.debug("Set server: %s", server)
def setSSL(self, verifySSL=None, certificate=None):
"""
@ -68,15 +57,15 @@ class DownloadUtils():
certificate must be path to certificate or 'None'
"""
if verifySSL is None:
verifySSL = settings('sslverify')
verifySSL = state.VERIFY_SSL_CERT
if certificate is None:
certificate = settings('sslcert')
log.debug("Verify SSL certificates set to: %s" % verifySSL)
log.debug("SSL client side certificate set to: %s" % certificate)
if verifySSL != 'true':
self.s.verify = False
if certificate != 'None':
certificate = state.SSL_CERT_PATH
# Set the session's parameters
self.s.verify = verifySSL
if certificate:
self.s.cert = certificate
LOG.debug("Verify SSL certificates set to: %s", verifySSL)
LOG.debug("SSL client side certificate set to: %s", certificate)
def startSession(self, reset=False):
"""
@ -95,7 +84,6 @@ class DownloadUtils():
# Set other stuff
self.setServer(window('pms_server'))
self.setToken(window('pms_token'))
# Counters to declare PMS dead or unauthorized
# Use window variables because start of movies will be called with a
@ -108,18 +96,18 @@ class DownloadUtils():
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
log.info("Requests session started on: %s" % self.server)
LOG.info("Requests session started on: %s", self.server)
def stopSession(self):
try:
self.s.close()
except:
log.info("Requests session already closed")
LOG.info("Requests session already closed")
try:
del self.s
except:
pass
log.info('Request session stopped')
LOG.info('Request session stopped')
def getHeader(self, options=None):
header = client.getXArgsDeviceInfo()
@ -142,7 +130,8 @@ class DownloadUtils():
def downloadUrl(self, url, action_type="GET", postBody=None,
parameters=None, authenticate=True, headerOptions=None,
verifySSL=True, timeout=None, return_response=False):
verifySSL=True, timeout=None, return_response=False,
headerOverride=None):
"""
Override SSL check with verifySSL=False
@ -164,7 +153,7 @@ class DownloadUtils():
try:
s = self.s
except AttributeError:
log.info("Request session does not exist: start one")
LOG.info("Request session does not exist: start one")
self.startSession()
s = self.s
# Replace for the real values
@ -173,9 +162,13 @@ class DownloadUtils():
# User is not (yet) authenticated. Used to communicate with
# plex.tv and to check for PMS servers
s = requests
headerOptions = self.getHeader(options=headerOptions)
if settings('sslcert') != 'None':
kwargs['cert'] = settings('sslcert')
if not headerOverride:
headerOptions = self.getHeader(options=headerOptions)
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
# otherwise - faster)
@ -196,39 +189,39 @@ class DownloadUtils():
r = self._doDownload(s, action_type, **kwargs)
# THE EXCEPTIONS
except requests.exceptions.SSLError as e:
LOG.warn("Invalid SSL certificate for: %s", url)
LOG.warn(e)
except requests.exceptions.ConnectionError as e:
# Connection error
log.warn("Server unreachable at: %s" % url)
log.warn(e)
LOG.warn("Server unreachable at: %s", url)
LOG.warn(e)
except requests.exceptions.Timeout as e:
log.warn("Server timeout at: %s" % url)
log.warn(e)
LOG.warn("Server timeout at: %s", url)
LOG.warn(e)
except requests.exceptions.HTTPError as e:
log.warn('HTTP Error at %s' % url)
log.warn(e)
except requests.exceptions.SSLError as e:
log.warn("Invalid SSL certificate for: %s" % url)
log.warn(e)
LOG.warn('HTTP Error at %s', url)
LOG.warn(e)
except requests.exceptions.TooManyRedirects as e:
log.warn("Too many redirects connecting to: %s" % url)
log.warn(e)
LOG.warn("Too many redirects connecting to: %s", url)
LOG.warn(e)
except requests.exceptions.RequestException as e:
log.warn("Unknown error connecting to: %s" % url)
log.warn(e)
LOG.warn("Unknown error connecting to: %s", url)
LOG.warn(e)
except SystemExit:
log.info('SystemExit detected, aborting download')
LOG.info('SystemExit detected, aborting download')
self.stopSession()
except:
log.warn('Unknown error while downloading. Traceback:')
LOG.warn('Unknown error while downloading. Traceback:')
import traceback
log.warn(traceback.format_exc())
LOG.warn(traceback.format_exc())
# THE RESPONSE #####
else:
@ -250,19 +243,19 @@ class DownloadUtils():
# Called when checking a connect - no need for rash action
return 401
r.encoding = 'utf-8'
log.warn('HTTP error 401 from PMS %s' % url)
log.info(r.text)
LOG.warn('HTTP error 401 from PMS %s', url)
LOG.info(r.text)
if '401 Unauthorized' in r.text:
# Truly unauthorized
window('countUnauthorized',
value=str(int(window('countUnauthorized')) + 1))
if (int(window('countUnauthorized')) >=
self.unauthorizedAttempts):
log.warn('We seem to be truly unauthorized for PMS'
' %s ' % url)
LOG.warn('We seem to be truly unauthorized for PMS'
' %s ', url)
if state.PMS_STATUS not in ('401', 'Auth'):
# Tell userclient token has been revoked.
log.debug('Setting PMS server status to '
LOG.debug('Setting PMS server status to '
'unauthorized')
state.PMS_STATUS = '401'
window('plex_serverStatus', value="401")
@ -272,7 +265,7 @@ class DownloadUtils():
icon='{error}')
else:
# there might be other 401 where e.g. PMS under strain
log.info('PMS might only be under strain')
LOG.info('PMS might only be under strain')
return 401
elif r.status_code in (200, 201):
@ -300,21 +293,19 @@ class DownloadUtils():
# update
pass
else:
log.warn("Unable to convert the response for: "
"%s" % url)
log.warn("Received headers were: %s" % r.headers)
log.warn('Received text:')
log.warn(r.text)
LOG.warn("Unable to convert the response for: "
"%s", url)
LOG.warn("Received headers were: %s", r.headers)
LOG.warn('Received text: %s', r.text)
return True
elif r.status_code == 403:
# E.g. deleting a PMS item
log.warn('PMS sent 403: Forbidden error for url %s' % url)
LOG.warn('PMS sent 403: Forbidden error for url %s', url)
return None
else:
log.warn('Unknown answer from PMS %s with status code %s. '
'Message:' % (url, r.status_code))
r.encoding = 'utf-8'
log.warn(r.text)
LOG.warn('Unknown answer from PMS %s with status code %s. ',
url, r.status_code)
return True
# And now deal with the consequences of the exceptions
@ -324,8 +315,8 @@ class DownloadUtils():
window('countError',
value=str(int(window('countError')) + 1))
if int(window('countError')) >= self.connectionAttempts:
log.warn('Failed to connect to %s too many times. '
'Declare PMS dead' % url)
LOG.warn('Failed to connect to %s too many times. '
'Declare PMS dead', url)
window('plex_online', value="false")
except:
# 'countError' not yet set

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from logging import getLogger
from shutil import copyfile
from os import walk, makedirs
from os.path import basename, join
@ -11,17 +11,18 @@ import xbmcplugin
from xbmc import sleep, executebuiltin, translatePath
from xbmcgui import ListItem
from utils import window, settings, language as lang, dialog, tryEncode, \
CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode
from utils import window, settings, language as lang, dialog, try_encode, \
catch_exceptions, exists_dir, plex_command, try_decode
import downloadutils
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
GetMachineIdentifier
from PlexAPI import API
import json_rpc as js
import variables as v
###############################################################################
log = logging.getLogger("PLEX."+__name__)
log = getLogger("PLEX."+__name__)
try:
HANDLE = int(argv[1])
@ -39,7 +40,7 @@ def chooseServer():
import initialsetup
setup = initialsetup.InitialSetup()
server = setup.PickPMS(showDialog=True)
server = setup.pick_pms(showDialog=True)
if server is None:
log.error('We did not connect to a new PMS, aborting')
plex_command('SUSPEND_USER_CLIENT', 'False')
@ -47,16 +48,14 @@ def chooseServer():
return
log.info("User chose server %s" % server['name'])
setup.WritePMStoSettings(server)
setup.write_pms_to_settings(server)
if not __LogOut():
return
from utils import deletePlaylists, deleteNodes
# First remove playlists
deletePlaylists()
# Remove video nodes
deleteNodes()
from utils import wipe_database
# Wipe Kodi and Plex database as well as playlists and video nodes
wipe_database()
# Log in again
__LogIn()
@ -86,7 +85,7 @@ def togglePlexTV():
else:
log.info('Login to plex.tv')
import initialsetup
initialsetup.InitialSetup().PlexTVSignIn()
initialsetup.InitialSetup().plex_tv_sign_in()
dialog('notification',
lang(29999),
lang(39221),
@ -174,10 +173,10 @@ def switchPlexUser():
return
# First remove playlists of old user
from utils import deletePlaylists, deleteNodes
deletePlaylists()
from utils import delete_playlists, delete_nodes
delete_playlists()
# Remove video nodes
deleteNodes()
delete_nodes()
__LogIn()
@ -193,48 +192,41 @@ def GetSubFolders(nodeindex):
##### 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']
li = ListItem(title)
li.setProperty('IsPlayable', "true")
li.setProperty('IsPlayable', 'true')
metadata = {
'duration': str(item['runtime']/60),
'Plot': item['plot'],
'Playcount': item['playcount']
}
if "episode" in item:
if 'episode' in item:
episode = item['episode']
metadata['Episode'] = episode
if "season" in item:
if 'season' in item:
season = item['season']
metadata['Season'] = season
if season and episode:
li.setProperty('episodeno', "s%.2de%.2d" % (season, episode))
if appendSxxExx is True:
title = "S%.2dE%.2d - %s" % (season, episode, title)
if "firstaired" in item:
li.setProperty('episodeno', 's%.2de%.2d' % (season, episode))
if append_sxxexx is True:
title = 'S%.2dE%.2d - %s' % (season, episode, title)
if 'firstaired' in item:
metadata['Premiered'] = item['firstaired']
if "showtitle" in item:
if 'showtitle' in item:
metadata['TVshowTitle'] = item['showtitle']
if appendShowTitle is True:
if append_show_title is True:
title = item['showtitle'] + ' - ' + title
if "rating" in item:
metadata['Rating'] = str(round(float(item['rating']),1))
if "director" in item:
metadata['Director'] = " / ".join(item['director'])
if "writer" in item:
metadata['Writer'] = " / ".join(item['writer'])
if "cast" in item:
if 'rating' in item:
metadata['Rating'] = str(round(float(item['rating']), 1))
if 'director' in item:
metadata['Director'] = item['director']
if 'writer' in item:
metadata['Writer'] = item['writer']
if 'cast' in item:
cast = []
castandrole = []
for person in item['cast']:
@ -245,16 +237,17 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
metadata['CastAndRole'] = castandrole
metadata['Title'] = title
metadata['mediatype'] = 'episode'
metadata['dbid'] = str(item['episodeid'])
li.setLabel(title)
li.setInfo(type='Video', infoLabels=metadata)
li.setInfo(type="Video", infoLabels=metadata)
li.setProperty('resumetime', str(item['resume']['position']))
li.setProperty('totaltime', str(item['resume']['total']))
li.setArt(item['art'])
li.setThumbnailImage(item['art'].get('thumb',''))
li.setThumbnailImage(item['art'].get('thumb', ''))
li.setArt({'icon': 'DefaultTVShows.png'})
li.setProperty('dbid', str(item['episodeid']))
li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
li.setProperty('fanart_image', item['art'].get('tvshow.fanart', ''))
try:
li.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
except TypeError:
@ -263,12 +256,10 @@ def createListItem(item, appendShowTitle=False, appendSxxExx=False):
for key, value in item['streamdetails'].iteritems():
for stream in value:
li.addStreamInfo(key, stream)
return li
##### GET NEXTUP EPISODES FOR TAGNAME #####
def getNextUpEpisodes(tagname, limit):
count = 0
# if the addon is called with nextup parameter,
# we return the nextepisodes list of the given tagname
@ -283,68 +274,50 @@ def getNextUpEpisodes(tagname, limit):
]},
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
}
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
# If we found any, find the oldest unwatched show for each one.
try:
items = result['result']['tvshows']
except (KeyError, TypeError):
pass
else:
for item in items:
if settings('ignoreSpecialsNextEpisodes') == "true":
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'and': [
{'operator': "lessthan",
'field': "playcount",
'value': "1"},
{'operator': "greaterthan",
'field': "season",
'value': "0"}]},
'properties': [
"title", "playcount", "season", "episode", "showtitle",
"plot", "file", "rating", "resume", "tvshowid", "art",
"streamdetails", "firstaired", "runtime", "writer",
"dateadded", "lastplayed"
],
'limits': {"end": 1}
}
else:
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'operator': "lessthan",
'field': "playcount",
'value': "1"},
'properties': [
"title", "playcount", "season", "episode", "showtitle",
"plot", "file", "rating", "resume", "tvshowid", "art",
"streamdetails", "firstaired", "runtime", "writer",
"dateadded", "lastplayed"
],
'limits': {"end": 1}
}
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
try:
episodes = result['result']['episodes']
except (KeyError, TypeError):
pass
else:
for episode in episodes:
li = createListItem(episode)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=episode['file'],
listitem=li)
count += 1
if count == limit:
break
for item in js.get_tv_shows(params):
if settings('ignoreSpecialsNextEpisodes') == "true":
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'and': [
{'operator': "lessthan",
'field': "playcount",
'value': "1"},
{'operator': "greaterthan",
'field': "season",
'value': "0"}]},
'properties': [
"title", "playcount", "season", "episode", "showtitle",
"plot", "file", "rating", "resume", "tvshowid", "art",
"streamdetails", "firstaired", "runtime", "writer",
"dateadded", "lastplayed"
],
'limits': {"end": 1}
}
else:
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'operator': "lessthan",
'field': "playcount",
'value': "1"},
'properties': [
"title", "playcount", "season", "episode", "showtitle",
"plot", "file", "rating", "resume", "tvshowid", "art",
"streamdetails", "firstaired", "runtime", "writer",
"dateadded", "lastplayed"
],
'limits': {"end": 1}
}
for episode in js.get_episodes(params):
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=episode['file'],
listitem=createListItem(episode))
count += 1
if count == limit:
break
xbmcplugin.endOfDirectory(handle=HANDLE)
@ -364,42 +337,26 @@ def getInProgressEpisodes(tagname, limit):
]},
'properties': ['title', 'studio', 'mpaa', 'file', 'art']
}
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
# If we found any, find the oldest unwatched show for each one.
try:
items = result['result']['tvshows']
except (KeyError, TypeError):
pass
else:
for item in items:
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'operator': "true",
'field': "inprogress",
'value': ""},
'properties': ["title", "playcount", "season", "episode",
"showtitle", "plot", "file", "rating", "resume",
"tvshowid", "art", "cast", "streamdetails", "firstaired",
"runtime", "writer", "dateadded", "lastplayed"]
}
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
try:
episodes = result['result']['episodes']
except (KeyError, TypeError):
pass
else:
for episode in episodes:
li = createListItem(episode)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=episode['file'],
listitem=li)
count += 1
if count == limit:
break
for item in js.get_tv_shows(params):
params = {
'tvshowid': item['tvshowid'],
'sort': {'method': "episode"},
'filter': {
'operator': "true",
'field': "inprogress",
'value': ""},
'properties': ["title", "playcount", "season", "episode",
"showtitle", "plot", "file", "rating", "resume",
"tvshowid", "art", "cast", "streamdetails", "firstaired",
"runtime", "writer", "dateadded", "lastplayed"]
}
for episode in js.get_episodes(params):
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=episode['file'],
listitem=createListItem(episode))
count += 1
if count == limit:
break
xbmcplugin.endOfDirectory(handle=HANDLE)
##### GET RECENT EPISODES FOR TAGNAME #####
@ -409,25 +366,16 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit):
# if the addon is called with recentepisodes parameter,
# we return the recentepisodes list of the given tagname
xbmcplugin.setContent(HANDLE, 'episodes')
appendShowTitle = settings('RecentTvAppendShow') == 'true'
appendSxxExx = settings('RecentTvAppendSeason') == 'true'
append_show_title = settings('RecentTvAppendShow') == 'true'
append_sxxexx = settings('RecentTvAppendSeason') == 'true'
# First we get a list of all the TV shows - filtered by tag
allshowsIds = set()
params = {
'sort': {'order': "descending", 'method': "dateadded"},
'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
}
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
# If we found any, find the oldest unwatched show for each one.
try:
items = result['result'][mediatype]
except (KeyError, TypeError):
# No items, empty folder
xbmcplugin.endOfDirectory(handle=HANDLE)
return
allshowsIds = set()
for item in items:
allshowsIds.add(item['tvshowid'])
for tv_show in js.get_tv_shows(params):
allshowsIds.add(tv_show['tvshowid'])
params = {
'sort': {'order': "descending", 'method': "dateadded"},
'properties': ["title", "playcount", "season", "episode", "showtitle",
@ -442,26 +390,18 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit):
'field': "playcount",
'value': "1"
}
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
try:
episodes = result['result']['episodes']
except (KeyError, TypeError):
pass
else:
for episode in episodes:
if episode['tvshowid'] in allshowsIds:
li = createListItem(episode,
appendShowTitle=appendShowTitle,
appendSxxExx=appendSxxExx)
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=episode['file'],
listitem=li)
count += 1
if count == limit:
break
for episode in js.get_episodes(params):
if episode['tvshowid'] in allshowsIds:
listitem = createListItem(episode,
append_show_title=append_show_title,
append_sxxexx=append_sxxexx)
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=episode['file'],
listitem=listitem)
count += 1
if count == limit:
break
xbmcplugin.endOfDirectory(handle=HANDLE)
@ -506,14 +446,14 @@ def getVideoFiles(plexId, params):
if exists_dir(path):
for root, dirs, files in walk(path):
for directory in dirs:
item_path = tryEncode(join(root, directory))
item_path = try_encode(join(root, directory))
li = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=item_path,
listitem=li,
isFolder=True)
for file in files:
item_path = tryEncode(join(root, file))
item_path = try_encode(join(root, file))
li = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=file,
@ -524,7 +464,7 @@ def getVideoFiles(plexId, params):
xbmcplugin.endOfDirectory(HANDLE)
@CatchExceptions(warnuser=False)
@catch_exceptions(warnuser=False)
def getExtraFanArt(plexid, plexPath):
"""
Get extrafanart for listitem
@ -541,7 +481,7 @@ def getExtraFanArt(plexid, plexPath):
# We need to store the images locally for this to work
# because of the caching system in xbmc
fanartDir = tryDecode(translatePath(
fanartDir = try_decode(translatePath(
"special://thumbnails/plex/%s/" % plexid))
if not exists_dir(fanartDir):
# Download the images to the cache directory
@ -552,22 +492,22 @@ def getExtraFanArt(plexid, plexPath):
return xbmcplugin.endOfDirectory(HANDLE)
api = API(xml[0])
backdrops = api.getAllArtwork()['Backdrop']
backdrops = api.artwork()['Backdrop']
for count, backdrop in enumerate(backdrops):
# Same ordering as in artwork
fanartFile = tryEncode(join(fanartDir, "fanart%.3d.jpg" % count))
fanartFile = try_encode(join(fanartDir, "fanart%.3d.jpg" % count))
li = ListItem("%.3d" % count, path=fanartFile)
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=fanartFile,
listitem=li)
copyfile(backdrop, tryDecode(fanartFile))
copyfile(backdrop, try_decode(fanartFile))
else:
log.info("Found cached backdrop.")
# Use existing cached images
for root, dirs, files in walk(fanartDir):
for file in files:
fanartFile = tryEncode(join(root, file))
fanartFile = try_encode(join(root, file))
li = ListItem(file, path=fanartFile)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=fanartFile,
@ -587,8 +527,8 @@ def getOnDeck(viewid, mediatype, tagname, limit):
limit: Max. number of items to retrieve, e.g. 50
"""
xbmcplugin.setContent(HANDLE, 'episodes')
appendShowTitle = settings('OnDeckTvAppendShow') == 'true'
appendSxxExx = settings('OnDeckTvAppendSeason') == 'true'
append_show_title = settings('OnDeckTvAppendShow') == 'true'
append_sxxexx = settings('OnDeckTvAppendSeason') == 'true'
directpaths = settings('useDirectPaths') == 'true'
if settings('OnDeckTVextended') == 'false':
# Chances are that this view is used on Kodi startup
@ -609,19 +549,20 @@ def getOnDeck(viewid, mediatype, tagname, limit):
limitcounter = 0
for item in xml:
api = API(item)
listitem = api.CreateListItemFromPlexItem(
appendShowTitle=appendShowTitle,
appendSxxExx=appendSxxExx)
listitem = api.create_listitem(
append_show_title=append_show_title,
append_sxxexx=append_sxxexx)
if directpaths:
url = api.getFilePath()
url = api.file_path()
else:
params = {
'mode': "play",
'id': api.getRatingKey(),
'dbid': listitem.getProperty('dbid')
}
url = "plugin://plugin.video.plexkodiconnect/tvshows/?%s" \
% urlencode(params)
url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play&filename=%s'
% (v.ADDON_ID,
api.plex_id(),
api.plex_type(),
api.file_name(force_first_media=True)))
if api.resume_point():
listitem.setProperty('resumetime',
str(api.resume_point()))
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=url,
@ -644,11 +585,8 @@ def getOnDeck(viewid, mediatype, tagname, limit):
{'operator': "is", 'field': "tag", 'value': "%s" % tagname}
]}
}
result = JSONRPC('VideoLibrary.GetTVShows').execute(params)
# If we found any, find the oldest unwatched show for each one.
try:
items = result['result'][mediatype]
except (KeyError, TypeError):
items = js.get_tv_shows(params)
if not items:
# Now items retrieved - empty directory
xbmcplugin.endOfDirectory(handle=HANDLE)
return
@ -689,33 +627,26 @@ def getOnDeck(viewid, mediatype, tagname, limit):
count = 0
for item in items:
inprog_params['tvshowid'] = item['tvshowid']
result = JSONRPC('VideoLibrary.GetEpisodes').execute(inprog_params)
try:
episodes = result['result']['episodes']
except (KeyError, TypeError):
episodes = js.get_episodes(inprog_params)
if not episodes:
# No, there are no episodes not yet finished. Get "next up"
params['tvshowid'] = item['tvshowid']
result = JSONRPC('VideoLibrary.GetEpisodes').execute(params)
try:
episodes = result['result']['episodes']
except (KeyError, TypeError):
episodes = js.get_episodes(params)
if not episodes:
# Also no episodes currently coming up
continue
for episode in episodes:
# There will always be only 1 episode ('limit=1')
li = createListItem(episode,
appendShowTitle=appendShowTitle,
appendSxxExx=appendSxxExx)
xbmcplugin.addDirectoryItem(
handle=HANDLE,
url=episode['file'],
listitem=li,
isFolder=False)
listitem = createListItem(episode,
append_show_title=append_show_title,
append_sxxexx=append_sxxexx)
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=episode['file'],
listitem=listitem,
isFolder=False)
count += 1
if count >= limit:
break
xbmcplugin.endOfDirectory(handle=HANDLE)
@ -752,10 +683,6 @@ def channels():
"""
Listing for Plex Channels
"""
if window('plex_restricteduser') == 'true':
log.error('No Plex Channels - restricted user')
return xbmcplugin.endOfDirectory(HANDLE, False)
xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all')
try:
xml[0].attrib
@ -888,25 +815,28 @@ def __build_folder(xml_element, plex_section_id=None):
def __build_item(xml_element):
api = API(xml_element)
listitem = api.CreateListItemFromPlexItem()
if (api.getKey().startswith('/system/services') or
api.getKey().startswith('http')):
listitem = api.create_listitem()
resume = api.resume_point()
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 = {
'mode': 'plex_node',
'key': xml_element.attrib.get('key'),
'view_offset': xml_element.attrib.get('viewOffset', '0'),
'offset': xml_element.attrib.get('viewOffset', '0'),
}
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
elif api.getType() == v.PLEX_TYPE_PHOTO:
elif api.plex_type() == v.PLEX_TYPE_PHOTO:
url = api.get_picture_path()
else:
params = {
'mode': 'play',
'filename': api.getKey(),
'id': api.getRatingKey(),
'dbid': listitem.getProperty('dbid')
}
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
url = 'plugin://%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s' \
% (v.ADDON_TYPE[api.plex_type()],
api.plex_id(),
api.plex_type(),
api.file_name(force_first_media=True))
if api.resume_point():
listitem.setProperty('resumetime', str(api.resume_point()))
xbmcplugin.addDirectoryItem(handle=HANDLE,
url=url,
listitem=listitem)

View file

@ -1,107 +1,217 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from Queue import Queue
import xml.etree.ElementTree as etree
import logging
import xbmc
import xbmcgui
from xbmc import executebuiltin, translatePath
from utils import settings, window, language as lang, tryEncode, \
advancedsettings_xml
import downloadutils
from userclient import UserClient
from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import state
from utils import settings, window, language as lang, try_encode, try_decode, \
XmlKodiSetting, reboot_kodi, dialog
from migration import check_migration
from downloadutils import DownloadUtils as DU
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):
log.debug('Entering initialsetup class')
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.plx = PlexAPI()
self.dialog = xbmcgui.Dialog()
LOG.debug('Entering initialsetup class')
self.server = UserClient().getServer()
self.serverid = settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings()
plexdict = PF.GetPlexLoginFromSettings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexToken = plexdict['plexToken']
self.plex_login = plexdict['plexLogin']
self.plex_token = plexdict['plexToken']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.pms_token = settings('accessToken')
if self.plexToken:
log.debug('Found a plex.tv token in the settings')
if self.plex_token:
LOG.debug('Found a plex.tv token in the settings')
def PlexTVSignIn(self):
def plex_tv_sign_in(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
Returns True if successful, or False if not
"""
result = self.plx.PlexTvSignInWithPin()
result = plex_tv.sign_in_with_pin()
if result:
self.plexLogin = result['username']
self.plexToken = result['token']
self.plex_login = result['username']
self.plex_token = result['token']
self.plexid = result['plexid']
return True
return False
def CheckPlexTVSignIn(self):
def check_plex_tv_sign_in(self):
"""
Checks existing connection to plex.tv. If not, triggers sign in
Returns True if signed in, False otherwise
"""
answer = True
chk = self.plx.CheckConnection('plex.tv', token=self.plexToken)
chk = PF.check_connection('plex.tv', token=self.plex_token)
if chk in (401, 403):
# HTTP Error: unauthorized. Token is no longer valid
log.info('plex.tv connection returned HTTP %s' % str(chk))
LOG.info('plex.tv connection returned HTTP %s', str(chk))
# Delete token in the settings
settings('plexToken', value='')
settings('plexLogin', value='')
# Could not login, please try again
self.dialog.ok(lang(29999), lang(39009))
answer = self.PlexTVSignIn()
dialog('ok', lang(29999), lang(39009))
answer = self.plex_tv_sign_in()
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
log.info('Problems connecting to plex.tv; connection returned '
'HTTP %s' % str(chk))
self.dialog.ok(lang(29999), lang(39010))
LOG.info('Problems connecting to plex.tv; connection returned '
'HTTP %s', str(chk))
dialog('ok', lang(29999), lang(39010))
answer = False
else:
log.info('plex.tv connection with token successful')
LOG.info('plex.tv connection with token successful')
settings('plex_status', value=lang(39227))
# Refresh the info from Plex.tv
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plexToken})
xml = DU().downloadUrl('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plex_token})
try:
self.plexLogin = xml.attrib['title']
self.plex_login = xml.attrib['title']
except (AttributeError, KeyError):
log.error('Failed to update Plex info from plex.tv')
LOG.error('Failed to update Plex info from plex.tv')
else:
settings('plexLogin', value=self.plexLogin)
settings('plexLogin', value=self.plex_login)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
settings('plexhome', value=home)
settings('plexAvatar', value=xml.attrib.get('thumb'))
settings('plexHomeSize', value=xml.attrib.get('homeSize', '1'))
log.info('Updated Plex info from plex.tv')
LOG.info('Updated Plex info from plex.tv')
return answer
def CheckPMS(self):
def check_existing_pms(self):
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:
@ -112,80 +222,80 @@ class InitialSetup():
not set before
"""
answer = True
chk = self.plx.CheckConnection(self.server, verifySSL=False)
chk = PF.check_connection(self.server, verifySSL=False)
if chk is False:
log.warn('Could not reach PMS %s' % self.server)
LOG.warn('Could not reach PMS %s', self.server)
answer = False
if answer is True and not self.serverid:
log.info('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID' % self.server)
self.serverid = GetMachineIdentifier(self.server)
LOG.info('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID', self.server)
self.serverid = PF.GetMachineIdentifier(self.server)
if self.serverid is None:
log.warn('Could not retrieve machineIdentifier')
LOG.warn('Could not retrieve machineIdentifier')
answer = False
else:
settings('plex_machineIdentifier', value=self.serverid)
elif answer is True:
tempServerid = GetMachineIdentifier(self.server)
if tempServerid != self.serverid:
log.warn('The current PMS %s was expected to have a '
temp_server_id = PF.GetMachineIdentifier(self.server)
if temp_server_id != self.serverid:
LOG.warn('The current PMS %s was expected to have a '
'unique machineIdentifier of %s. But we got '
'%s. Pick a new server to be sure'
% (self.server, self.serverid, tempServerid))
'%s. Pick a new server to be sure',
self.server, self.serverid, temp_server_id)
answer = False
return answer
def _getServerList(self):
@staticmethod
def _check_pms_connectivity(server):
"""
Returns a list of servers from GDM and possibly plex.tv
"""
self.plx.discoverPMS(xbmc.getIPAddress(),
plexToken=self.plexToken)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
log.debug('PMS serverlist: %s' % serverlist)
return serverlist
def _checkServerCon(self, server):
"""
Checks for server's connectivity. Returns CheckConnection result
Checks for server's connectivity. Returns check_connection result
"""
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local'] == '1':
url = '%s://%s:%s' \
% (server['scheme'], server['ip'], server['port'])
if server['local']:
url = ('%s://%s:%s'
% (server['scheme'], server['ip'], server['port']))
# Deactive SSL verification if the server is local!
verifySSL = False
else:
url = server['baseURL']
verifySSL = True
chk = self.plx.CheckConnection(url,
token=server['accesstoken'],
verifySSL=verifySSL)
chk = PF.check_connection(url,
token=server['token'],
verifySSL=verifySSL)
return chk
def PickPMS(self, showDialog=False):
def pick_pms(self, showDialog=False):
"""
Searches for PMS in local Lan and optionally (if self.plexToken set)
Searches for PMS in local Lan and optionally (if self.plex_token set)
also on plex.tv
showDialog=True: let the user pick one
showDialog=False: automatically pick PMS based on machineIdentifier
Returns the picked PMS' detail as a dict:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
'machineIdentifier' [str] unique identifier of the PMS
'name' [str] name of the PMS
'token' [str] token needed to access that PMS
'ownername' [str] name of the owner of this PMS or None if
the owner itself supplied tries to connect
'product' e.g. 'Plex Media Server' or None
'version' e.g. '1.11.2.4772-3e...' or None
'device': e.g. 'PC' or 'Windows' or None
'platform': e.g. 'Windows', 'Android' or None
'local' [bool] True if plex.tv supplied
'publicAddressMatches'='1'
or if found using Plex GDM in the local LAN
'owned' [bool] True if it's the owner's PMS
'relay' [bool] True if plex.tv supplied 'relay'='1'
'presence' [bool] True if plex.tv supplied 'presence'='1'
'httpsRequired' [bool] True if plex.tv supplied
'httpsRequired'='1'
'scheme' [str] either 'http' or 'https'
'ip': [str] IP of the PMS, e.g. '192.168.1.1'
'port': [str] Port of the PMS, e.g. '32400'
'baseURL': [str] <scheme>://<ip>:<port> of the PMS
}
or None if unsuccessful
"""
server = None
@ -193,105 +303,77 @@ class InitialSetup():
if not self.server or not self.serverid:
showDialog = True
if showDialog is True:
server = self._UserPickPMS()
server = self._user_pick_pms()
else:
server = self._AutoPickPMS()
server = self._auto_pick_pms()
if server is not None:
self._write_PMS_settings(server['baseURL'], server['accesstoken'])
_write_pms_settings(server['baseURL'], server['token'])
return server
def _write_PMS_settings(self, url, token):
"""
Sets certain settings for server by asking for the PMS' settings
Call with url: scheme://ip:port
"""
xml = get_PMS_settings(url, token)
try:
xml.attrib
except AttributeError:
log.error('Could not get PMS settings for %s' % url)
return
for entry in xml:
if entry.attrib.get('id', '') == 'allowMediaDeletion':
settings('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
window('plex_allows_mediaDeletion',
value=entry.attrib.get('value', 'true'))
def _AutoPickPMS(self):
def _auto_pick_pms(self):
"""
Will try to pick PMS based on machineIdentifier saved in file settings
but only once
Returns server or None if unsuccessful
"""
httpsUpdated = False
checkedPlexTV = False
https_updated = False
server = None
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
server = item
if server is None:
name = settings('plex_servername')
log.warn('The PMS you have used before with a unique '
LOG.warn('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline' % (self.serverid, name))
'offline', self.serverid, name)
return
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
server['scheme'] = 'https'
httpsUpdated = True
continue
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
if self.CheckPlexTVSignIn() is True:
if checkedPlexTV is False:
# 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
chk = self._check_pms_connectivity(server)
if chk == 504 and https_updated is False:
# switch HTTPS to HTTP or vice-versa
if server['scheme'] == 'https':
server['scheme'] = 'http'
else:
return
server['scheme'] = 'https'
https_updated = True
continue
# Problems connecting
elif chk >= 400 or chk is False:
log.warn('Problems connecting to server %s. chk is %s'
% (server['name'], chk))
LOG.warn('Problems connecting to server %s. chk is %s',
server['name'], chk)
return
log.info('We found a server to automatically connect to: %s'
% server['name'])
LOG.info('We found a server to automatically connect to: %s',
server['name'])
return server
def _UserPickPMS(self):
def _user_pick_pms(self):
"""
Lets user pick his/her PMS from a list
Returns server or None if unsuccessful
"""
httpsUpdated = False
https_updated = False
# Searching for PMS
dialog('notification',
heading='{plex}',
message=lang(30001),
icon='{plex}',
time=5000)
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
# Exit if no servers found
if len(serverlist) == 0:
log.warn('No plex media servers found!')
self.dialog.ok(lang(29999), lang(39011))
if not serverlist:
LOG.warn('No plex media servers found!')
dialog('ok', lang(29999), lang(39011))
return
# Get a nicer list
dialoglist = []
for server in serverlist:
if server['local'] == '1':
if server['local']:
# server is in the same network as client.
# Add"local"
msg = lang(39022)
@ -308,34 +390,34 @@ class InitialSetup():
dialoglist.append('%s (%s)'
% (server['name'], msg))
# Let user pick server from a list
resp = self.dialog.select(lang(39012), dialoglist)
resp = dialog('select', lang(39012), dialoglist)
if resp == -1:
# User cancelled
return
server = serverlist[resp]
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
chk = self._check_pms_connectivity(server)
if chk == 504 and https_updated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
httpsUpdated = True
https_updated = True
continue
httpsUpdated = False
https_updated = False
if chk == 401:
log.warn('Not yet authorized for Plex server %s'
% server['name'])
LOG.warn('Not yet authorized for Plex server %s',
server['name'])
# Please sign in to plex.tv
self.dialog.ok(lang(29999),
lang(39013) + server['name'],
lang(39014))
if self.PlexTVSignIn() is False:
dialog('ok',
lang(29999),
lang(39013) + server['name'],
lang(39014))
if self.plex_tv_sign_in() is False:
# Exit while loop if user cancels
return
# Problems connecting
elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server?
answ = self.dialog.yesno(lang(29999),
lang(39015))
answ = dialog('yesno', lang(29999), lang(39015))
# Exit while loop if user chooses No
if not answ:
return
@ -343,34 +425,20 @@ class InitialSetup():
else:
return server
def WritePMStoSettings(self, server):
@staticmethod
def write_pms_to_settings(server):
"""
Saves server to file settings. server is a dict of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
Saves server to file settings
"""
settings('plex_machineIdentifier', server['machineIdentifier'])
settings('plex_servername', server['name'])
settings('plex_serverowned',
'true' if server['owned'] == '1'
else 'false')
settings('plex_serverowned', 'true' if server['owned'] else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1':
if server['local']:
scheme = server['scheme']
settings('ipaddress', server['ip'])
settings('port', server['port'])
log.debug("Setting SSL verify to false, because server is "
LOG.debug("Setting SSL verify to false, because server is "
"local")
settings('sslverify', 'false')
else:
@ -378,7 +446,7 @@ class InitialSetup():
scheme = baseURL[0]
settings('ipaddress', baseURL[1].replace('//', ''))
settings('port', baseURL[2])
log.debug("Setting SSL verify to true, because server is not "
LOG.debug("Setting SSL verify to true, because server is not "
"local")
settings('sslverify', 'true')
@ -387,10 +455,10 @@ class InitialSetup():
else:
settings('https', 'false')
# And finally do some logging
log.debug("Writing to Kodi user settings file")
log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (server['machineIdentifier'], server['ip'],
server['port'], server['scheme']))
LOG.debug("Writing to Kodi user settings file")
LOG.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s ",
server['machineIdentifier'], server['ip'], server['port'],
server['scheme'])
def setup(self):
"""
@ -399,99 +467,162 @@ class InitialSetup():
Check server, user, direct paths, music, direct stream if not direct
path.
"""
log.info("Initial setup called.")
dialog = self.dialog
# Get current Kodi video cache setting
cache, _ = advancedsettings_xml(['cache', 'memorysize'])
if cache is None:
# Kodi default cache
cache = '20971520'
else:
cache = str(cache.text)
log.info('Current Kodi video memory cache in bytes: %s' % cache)
LOG.info("Initial setup called.")
try:
with XmlKodiSetting('advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml:
# Get current Kodi video cache setting
cache = xml.get_setting(['cache', 'memorysize'])
# Disable foreground "Loading media information from files"
# (still used by Kodi, even though the Wiki says otherwise)
xml.set_setting(['musiclibrary', 'backgroundupdate'],
value='true')
# Disable cleaning of library - not compatible with PKC
xml.set_setting(['videolibrary', 'cleanonupdate'],
value='false')
# 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)
# Hack to make PKC Kodi master lock compatible
try:
with XmlKodiSetting('sources.xml',
force_create=True,
top_element='sources') as xml:
root = xml.set_setting(['video'])
count = 2
for source in root.findall('.//path'):
if source.text == "smb://":
count -= 1
if count == 0:
# sources already set
break
else:
# Missing smb:// occurences, re-add.
for _ in range(0, count):
source = etree.SubElement(root, 'source')
etree.SubElement(
source,
'name').text = "PlexKodiConnect Masterlock Hack"
etree.SubElement(
source,
'path',
attrib={'pathversion': "1"}).text = "smb://"
etree.SubElement(source, 'allowsharing').text = "true"
if reboot is False:
reboot = xml.write_xml
except etree.ParseError:
pass
# Do we need to migrate stuff?
check_migration()
# Optionally sign into plex.tv. Will not be called on very first run
# as plexToken will be ''
settings('plex_status', value=lang(39226))
if self.plexToken and self.myplexlogin:
self.CheckPlexTVSignIn()
# Display a warning if Kodi puts ALL movies into the queue, basically
# breaking playback reporting for PKC
if js.settings_getsettingvalue('videoplayer.autoplaynextitem'):
LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!')
if settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
# Only warn once
settings('warned_setting_videoplayer.autoplaynextitem',
value='true')
# Warning: Kodi setting "Play next video automatically" is
# enabled. This could break PKC. Deactivate?
if dialog('yesno', lang(29999), lang(30003)):
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
False)
# Set any video library updates to happen in the background in order to
# hide "Compressing database"
js.settings_setsettingvalue('videolibrary.backgroundupdate', True)
# If a Plex server IP has already been set
# return only if the right machine identifier is found
if self.server:
log.info("PMS is already set: %s. Checking now..." % self.server)
if self.CheckPMS():
log.info("Using PMS %s with machineIdentifier %s"
% (self.server, self.serverid))
self._write_PMS_settings(self.server, self.pms_token)
LOG.info("PMS is already set: %s. Checking now...", self.server)
if self.check_existing_pms():
LOG.info("Using PMS %s with machineIdentifier %s",
self.server, self.serverid)
_write_pms_settings(self.server, self.pms_token)
if reboot is True:
reboot_kodi()
return
# If not already retrieved myplex info, optionally let user sign in
# to plex.tv. This DOES get called on very first install run
if not self.plexToken and self.myplexlogin:
self.PlexTVSignIn()
if not self.plex_token and self.myplexlogin:
self.plex_tv_sign_in()
server = self.PickPMS()
server = self.pick_pms()
if server is not None:
# Write our chosen server to Kodi settings file
self.WritePMStoSettings(server)
self.write_pms_to_settings(server)
# User already answered the installation questions
if settings('InstallQuestionsAnswered') == 'true':
if reboot is True:
reboot_kodi()
return
# Additional settings where the user needs to choose
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
goToSettings = False
if dialog.yesno(lang(29999),
lang(39027),
lang(39028),
nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"):
log.debug("User opted to use direct paths.")
goto_settings = False
if dialog('yesno',
lang(29999),
lang(39027),
lang(39028),
nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"):
LOG.debug("User opted to use direct paths.")
settings('useDirectPaths', value="1")
state.DIRECT_PATHS = True
# Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if dialog.yesno(heading=lang(29999), line1=lang(39033)):
log.debug("User chose to replace paths with smb")
if dialog('yesno', heading=lang(29999), line1=lang(39033)):
LOG.debug("User chose to replace paths with smb")
else:
settings('replaceSMB', value="false")
# complete replace all original Plex library paths with custom SMB
if dialog.yesno(heading=lang(29999), line1=lang(39043)):
log.debug("User chose custom smb paths")
if dialog('yesno', heading=lang(29999), line1=lang(39043)):
LOG.debug("User chose custom smb paths")
settings('remapSMB', value="true")
# Please enter your custom smb paths in the settings under
# "Sync Options" and then restart Kodi
dialog.ok(heading=lang(29999), line1=lang(39044))
goToSettings = True
dialog('ok', heading=lang(29999), line1=lang(39044))
goto_settings = True
# Go to network credentials?
if dialog.yesno(heading=lang(29999),
line1=lang(39029),
line2=lang(39030)):
log.debug("Presenting network credentials dialog.")
from utils import passwordsXML
passwordsXML()
if dialog('yesno',
heading=lang(29999),
line1=lang(39029),
line2=lang(39030)):
LOG.debug("Presenting network credentials dialog.")
from utils import passwords_xml
passwords_xml()
# Disable Plex music?
if dialog.yesno(heading=lang(29999), line1=lang(39016)):
log.debug("User opted to disable Plex music library.")
if dialog('yesno', heading=lang(29999), line1=lang(39016)):
LOG.debug("User opted to disable Plex music library.")
settings('enableMusic', value="false")
# Download additional art from FanArtTV
if dialog.yesno(heading=lang(29999), line1=lang(39061)):
log.debug("User opted to use FanArtTV")
if dialog('yesno', heading=lang(29999), line1=lang(39061)):
LOG.debug("User opted to use FanArtTV")
settings('FanartTV', value="true")
# Do you want to replace your custom user ratings with an indicator of
# how many versions of a media item you posses?
if dialog.yesno(heading=lang(29999), line1=lang(39718)):
log.debug("User opted to replace user ratings with version number")
if dialog('yesno', heading=lang(29999), line1=lang(39718)):
LOG.debug("User opted to replace user ratings with version number")
settings('indicate_media_versions', value="true")
# 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
settings('InstallQuestionsAnswered', value='true')
if goToSettings is False:
if goto_settings is False:
# Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017))
if goToSettings:
goto_settings = dialog('yesno',
heading=lang(29999),
line1=lang(39017))
if goto_settings:
state.PMS_STATUS = 'Stop'
xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
elif reboot is True:
reboot_kodi()

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 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
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
plex_command
from utils import window, settings, plex_command, thread_methods, try_encode
from PlexFunctions import scrobble
from kodidb_functions import get_kodiid_from_filename
from PlexAPI import API
from kodidb_functions import kodiid_from_filename
from plexbmchelper.subscribers import LOCKER
from playback import playback_triage
from initialsetup import set_replace_paths
import playqueue as PQ
import json_rpc as js
import playlist_func as PL
import state
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
# settings: window-variable
WINDOW_SETTINGS = {
'enableContext': 'plex_context',
'plex_restricteduser': 'plex_restricteduser',
'force_transcode_pix': 'plex_force_transcode_pix',
'fetch_pms_item_number': 'fetch_pms_item_number'
'force_transcode_pix': 'plex_force_transcode_pix'
}
# settings: state-variable (state.py)
@ -42,28 +46,39 @@ STATE_SETTINGS = {
'remapSMBphotoOrg': 'remapSMBphotoOrg',
'remapSMBphotoNew': 'remapSMBphotoNew',
'enableMusic': 'ENABLE_MUSIC',
'enableBackgroundSync': 'BACKGROUND_SYNC'
'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN',
'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER'
}
###############################################################################
class KodiMonitor(Monitor):
def __init__(self, callback):
self.mgr = callback
self.doUtils = DownloadUtils().downloadUrl
"""
PKC implementation of the Kodi Monitor class. Invoke only once.
"""
def __init__(self):
self.xbmcplayer = Player()
self.playqueue = self.mgr.playqueue
self._already_slept = False
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):
log.debug("Kodi library scan %s running." % library)
"""
Will be called when Kodi starts scanning the library
"""
LOG.debug("Kodi library scan %s running.", library)
if library == "video":
window('plex_kodiScan', value="true")
def onScanFinished(self, library):
log.debug("Kodi library scan %s finished." % library)
"""
Will be called when Kodi finished scanning the library
"""
LOG.debug("Kodi library scan %s finished.", library)
if library == "video":
window('plex_kodiScan', clear=True)
@ -71,18 +86,15 @@ class KodiMonitor(Monitor):
"""
Monitor the PKC settings for changes made by the user
"""
log.debug('PKC settings change detected')
LOG.debug('PKC settings change detected')
changed = False
# Reset the window variables from the settings variables
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
if window(window_value) != settings(settings_value):
changed = True
log.debug('PKC window settings changed: %s is now %s'
% (settings_value, settings(settings_value)))
LOG.debug('PKC window settings changed: %s is now %s',
settings_value, settings(settings_value))
window(window_value, value=settings(settings_value))
if settings_value == 'fetch_pms_item_number':
log.info('Requesting playlist/nodes refresh')
plex_command('RUN_LIB_SCAN', 'views')
# Reset the state variables in state.py
for settings_value, state_name in STATE_SETTINGS.iteritems():
new = settings(settings_value)
@ -92,14 +104,22 @@ class KodiMonitor(Monitor):
new = False
if getattr(state, state_name) != new:
changed = True
log.debug('PKC state settings %s changed from %s to %s'
% (settings_value, getattr(state, state_name), new))
LOG.debug('PKC state settings %s changed from %s to %s',
settings_value, getattr(state, state_name), new)
setattr(state, state_name, new)
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
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(
settings('backgroundsync_saftyMargin'))
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
state.SSL_CERT_PATH = settings('sslcert') \
if settings('sslcert') != 'None' else None
# Never set through the user
# state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
if changed is True:
@ -108,183 +128,312 @@ class KodiMonitor(Monitor):
state.STOP_SYNC = False
state.PATH_VERIFIED = False
@CatchExceptions(warnuser=False)
def onNotification(self, sender, method, data):
"""
Called when a bunch of different stuff happens on the Kodi side
"""
if data:
data = loads(data, 'utf-8')
log.debug("Method: %s Data: %s" % (method, data))
LOG.debug("Method: %s Data: %s", method, data)
if method == "Player.OnPlay":
self.PlayBackStart(data)
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()')
pass
elif method == 'Playlist.OnAdd':
self._playlist_onadd(data)
elif method == 'Playlist.OnRemove':
self._playlist_onremove(data)
elif method == 'Playlist.OnClear':
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
playcount = data.get('playcount')
item = data.get('item')
if playcount is None or item is None:
return
try:
kodiid = item['id']
item_type = item['type']
except (KeyError, TypeError):
log.info("Item is invalid for playstate update.")
LOG.info("Item is invalid for playstate update.")
return
# Send notification to the server.
with plexdb.Get_Plex_DB() as plexcur:
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
try:
itemid = plex_dbitem[0]
except TypeError:
LOG.error("Could not find itemid in plex database for a "
"video library update")
else:
# Send notification to the server.
with plexdb.Get_Plex_DB() as plexcur:
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
try:
itemid = plex_dbitem[0]
except TypeError:
log.error("Could not find itemid in plex database for a "
"video library update")
# Stop from manually marking as watched unwatched, with
# actual playback.
if window('plex_skipWatched%s' % itemid) == "true":
# property is set in player.py
window('plex_skipWatched%s' % itemid, clear=True)
else:
# Stop from manually marking as watched unwatched, with
# actual playback.
if window('plex_skipWatched%s' % itemid) == "true":
# property is set in player.py
window('plex_skipWatched%s' % itemid, clear=True)
# notify the server
if playcount > 0:
scrobble(itemid, 'watched')
else:
# notify the server
if playcount > 0:
scrobble(itemid, 'watched')
else:
scrobble(itemid, 'unwatched')
scrobble(itemid, 'unwatched')
elif method == "VideoLibrary.OnRemove":
pass
elif method == "System.OnSleep":
# Connection is going to sleep
log.info("Marking the server as offline. SystemOnSleep activated.")
LOG.info("Marking the server as offline. SystemOnSleep activated.")
window('plex_online', value="sleep")
elif method == "System.OnWake":
# Allow network to wake up
sleep(10000)
window('plex_onWake', value="true")
window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
sleep(5000)
plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit":
log.info('Kodi OnQuit detected - shutting down')
LOG.info('Kodi OnQuit detected - shutting down')
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!
try:
currentFile = self.xbmcplayer.getPlayingFile()
except:
currentFile = None
count = 0
while currentFile is None:
sleep(100)
try:
currentFile = self.xbmcplayer.getPlayingFile()
except:
pass
if count == 50:
log.info("No current File, cancel OnPlayBackStart...")
return
else:
count += 1
# Just to be on the safe side
currentFile = tryDecode(currentFile)
log.debug("Currently playing file is: %s" % currentFile)
# 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.")
if 'id' not in data['item']:
return
old = state.OLD_PLAYER_STATES[data['playlistid']]
if (not state.DIRECT_PATHS and data['position'] == 0 and
not PQ.PLAYQUEUES[data['playlistid']].items and
data['item']['type'] == old['kodi_type'] and
data['item']['id'] == old['kodi_id']):
# Hack we need for RESUMABLE items because Kodi lost the path of the
# last played item that is now being replayed (see playback.py's
# Player().play()) Also see playqueue.py _compare_playqueues()
LOG.info('Detected re-start of playback of last item')
kwargs = {
'plex_id': old['plex_id'],
'plex_type': old['plex_type'],
'path': old['file'],
'resolve': False
}
thread = Thread(target=playback_triage, kwargs=kwargs)
thread.start()
return
log.debug("Playing itemtype is (or appears to be): %s" % typus)
# Try to get a Kodi ID
# If PKC was used - native paths, not direct paths
plex_id = window('plex_%s.itemid' % tryEncode(currentFile))
# Get rid of the '' if the window property was not set
plex_id = None if not plex_id else plex_id
kodiid = None
if plex_id is None:
log.debug('Did not get Plex id from window properties')
try:
kodiid = data['item']['id']
except (TypeError, KeyError):
log.debug('Did not get a Kodi id from Kodi, darn')
# For direct paths, if we're not streaming something
# When using Widgets, Kodi doesn't tell us shit so we need this hack
if (kodiid is None and plex_id is None and typus != 'song'
and not currentFile.startswith('http')):
(kodiid, typus) = get_kodiid_from_filename(currentFile)
if kodiid is None:
return
def _playlist_onremove(self, data):
"""
Called if an item is removed from a Kodi playlist. Example data dict:
{
u'playlistid': 1,
u'position': 0
}
"""
pass
if plex_id is None:
# Get Plex' item id
with plexdb.Get_Plex_DB() as plexcursor:
plex_dbitem = plexcursor.getItem_byKodiId(kodiid, typus)
@LOCKER.lockthis
def _playlist_onclear(self, data):
"""
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:
plex_id = plex_dbitem[0]
plex_type = plex_dbitem[2]
except TypeError:
log.info("No Plex id returned for kodiid %s. Aborting playback"
" report" % kodiid)
return
log.debug("Found Plex id %s for Kodi id %s for type %s"
% (plex_id, kodiid, typus))
# No plex id, hence item not in the library. E.g. clips
pass
return plex_id, plex_type
# Switch subtitle tracks if applicable
subtitle = window('plex_%s.subtitle' % tryEncode(currentFile))
if window(tryEncode('plex_%s.playmethod' % currentFile)) \
== 'Transcode' and subtitle:
if window('plex_%s.subtitle' % currentFile) == 'None':
self.xbmcplayer.showSubtitles(False)
else:
self.xbmcplayer.setSubtitleStream(int(subtitle))
# Set some stuff if Kodi initiated playback
if ((settings('useDirectPaths') == "1" and not typus == "song")
or
(typus == "song" and settings('enableMusic') == "true")):
if self.StartDirectPath(plex_id,
typus,
tryEncode(currentFile)) is False:
log.error('Could not initiate monitoring; aborting')
return
# Save currentFile for cleanup later and to be able to access refs
window('plex_lastPlayedFiled', value=currentFile)
window('plex_currently_playing_itemid', value=plex_id)
window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id)
log.info('Finish playback startup')
def StartDirectPath(self, plex_id, type, currentFile):
@staticmethod
def _add_remaining_items_to_playlist(playqueue):
"""
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:
xml[0].attrib
except:
log.error('Did not receive a valid XML for plex_id %s.' % plex_id)
return False
# Setup stuff, because playback was started by Kodi, not PKC
api = API(xml[0])
listitem = api.CreateListItemFromPlexItem()
api.set_playback_win_props(currentFile, listitem)
if type == "song" and settings('streamMusic') == "true":
window('plex_%s.playmethod' % currentFile, value="DirectStream")
for i, item in enumerate(items):
PL.add_item_to_PMS_playlist(playqueue, i + 1, kodi_item=item)
except PL.PlaylistError:
LOG.info('Could not build Plex playlist for: %s', items)
def _json_item(self, playerid):
"""
Uses JSON RPC to get the playing item's info and returns the tuple
kodi_id, kodi_type, path
or None each time if not found.
"""
if not self._already_slept:
# SLEEP before calling this for the first time just after playback
# start as Kodi updates this info very late!! Might get previous
# element otherwise
self._already_slept = True
sleep(1000)
json_item = js.get_item(playerid)
LOG.debug('Kodi playing item properties: %s', json_item)
return (json_item.get('id'),
json_item.get('type'),
json_item.get('file'))
@LOCKER.lockthis
def PlayBackStart(self, data):
"""
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:
window('plex_%s.playmethod' % currentFile, value="DirectPlay")
log.debug('Window properties set for direct paths!')
if not kodi_id:
kodi_id, kodi_type, path = self._json_item(playerid)
if kodi_id and item.kodi_id:
if item.kodi_id != kodi_id or item.kodi_type != kodi_type:
LOG.debug('Detected different Kodi id')
initialize = True
else:
initialize = False
else:
# E.g. clips set-up previously with no Kodi DB entry
if 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
"""
log.debug("---===### Starting FanartSync ###===---")
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
stopped = self.stopped
suspended = self.suspended
queue = self.queue
while not thread_stopped():
while not stopped():
# In the event the server goes offline
while thread_suspended():
while suspended():
# Set in service.py
if thread_stopped():
if stopped():
# Abort was requested while waiting. We should exit
log.info("---===### Stopped FanartSync ###===---")
return

View file

@ -47,7 +47,7 @@ class Threaded_Get_Metadata(Thread):
continue
else:
self.queue.task_done()
if self.thread_stopped():
if self.stopped():
# Shutdown from outside requested; purge out_queue as well
while not self.out_queue.empty():
# Still try because remaining item might have been taken
@ -78,8 +78,8 @@ class Threaded_Get_Metadata(Thread):
# cache local variables because it's faster
queue = self.queue
out_queue = self.out_queue
thread_stopped = self.thread_stopped
while thread_stopped() is False:
stopped = self.stopped
while stopped() is False:
# grabs Plex item from queue
try:
item = queue.get(block=False)

View file

@ -68,9 +68,9 @@ class Threaded_Process_Metadata(Thread):
item_fct = getattr(itemtypes, self.item_type)
# cache local variables because it's faster
queue = self.queue
thread_stopped = self.thread_stopped
stopped = self.stopped
with item_fct() as item_class:
while thread_stopped() is False:
while stopped() is False:
# grabs item from queue
try:
item = queue.get(block=False)

View file

@ -52,14 +52,13 @@ class Threaded_Show_Sync_Info(Thread):
# cache local variables because it's faster
total = self.total
dialog = DialogProgressBG('dialoglogProgressBG')
thread_stopped = self.thread_stopped
dialog.create("%s %s: %s %s"
% (lang(39714), self.item_type, str(total), lang(39715)))
player = Player()
total = 2 * total
totalProgress = 0
while thread_stopped() is False and not player.isPlaying():
while self.stopped() is False and not player.isPlaying():
with LOCK:
get_progress = GET_METADATA_COUNT
process_progress = PROCESS_METADATA_COUNT

View file

@ -8,16 +8,17 @@ from random import shuffle
import xbmc
from xbmcvfs import exists
from utils import window, settings, getUnixTimestamp, sourcesXML,\
thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\
setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\
tryDecode, deletePlaylists, deleteNodes, tryEncode, compare_version
from utils import window, settings, unix_timestamp, thread_methods, \
create_actor_db_index, dialog, log_time, playlist_xsp, language as lang, \
unix_date_to_kodi, reset, try_decode, delete_playlists, delete_nodes, \
try_encode, compare_version
import downloadutils
import itemtypes
import plexdb_functions as plexdb
import kodidb_functions as kodidb
import userclient
import videonodes
import json_rpc as js
import variables as v
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
@ -42,42 +43,19 @@ log = getLogger("PLEX."+__name__)
class LibrarySync(Thread):
"""
"""
def __init__(self, callback=None):
self.mgr = callback
def __init__(self):
self.itemsToProcess = []
self.sessionKeys = []
self.sessionKeys = {}
self.fanartqueue = Queue.Queue()
if settings('FanartTV') == 'true':
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
# How long should we wait at least to process new/changed PMS items?
self.user = userclient.UserClient()
self.vnodes = videonodes.VideoNodes()
self.xbmcplayer = xbmc.Player()
self.installSyncDone = settings('SyncInstallRunDone') == 'true'
state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60
state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber'))
state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true'
state.ENABLE_MUSIC = settings('enableMusic') == 'true'
state.BACKGROUND_SYNC = settings(
'enableBackgroundSync') == 'true'
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
settings('backgroundsync_saftyMargin'))
# Show sync dialog even if user deactivated?
self.force_dialog = True
# Init for replacing paths
state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true'
state.REMAP_PATH = settings('remapSMB') == 'true'
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
for arg in ('Org', 'New'):
key = 'remapSMB%s%s' % (typus, arg)
setattr(state, key, settings(key))
# Just in case a time sync goes wrong
state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset'))
Thread.__init__(self)
def showKodiNote(self, message, icon="plex"):
@ -177,7 +155,7 @@ class LibrarySync(Thread):
log.debug('No timestamp; using 0')
# Set the timer
koditime = getUnixTimestamp()
koditime = unix_timestamp()
# Toggle watched state
scrobble(plexId, 'watched')
# Let the PMS process this first!
@ -240,11 +218,13 @@ class LibrarySync(Thread):
# Create an index for actors to speed up sync
create_actor_db_index()
@LogTime
@log_time
def fullSync(self, repair=False):
"""
repair=True: force sync EVERY item
"""
# Reset our keys
self.sessionKeys = {}
# self.compare == False: we're syncing EVERY item
# True: we're syncing only the delta, e.g. different checksum
self.compare = not repair
@ -263,20 +243,14 @@ class LibrarySync(Thread):
return True
def _fullSync(self):
xbmc.executebuiltin('InhibitIdleShutdown(true)')
screensaver = getScreensaver()
setScreensaver(value="")
if self.new_items_only is True:
# Only do the following once for new items
# Add sources
sourcesXML()
# Set views. Abort if unsuccessful
if not self.maintainViews():
xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver)
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 = {
'movies': self.PlexMovies,
@ -287,11 +261,9 @@ class LibrarySync(Thread):
# Do the processing
for itemtype in process:
if (self.thread_stopped() or
self.thread_suspended() or
if (self.stopped() or
self.suspended() or
not process[itemtype]()):
xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver)
return False
# 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)')
window('plex_initialScan', clear=True)
xbmc.executebuiltin('InhibitIdleShutdown(false)')
setScreensaver(value=screensaver)
if window('plex_scancrashed') == 'true':
# Show warning if itemtypes.py crashed at some point
dialog('ok', heading='{plex}', line1=lang(39408))
@ -311,16 +281,6 @@ class LibrarySync(Thread):
if state.PMS_STATUS not in ('401', 'Auth'):
# Plex server had too much and returned ERROR
dialog('ok', heading='{plex}', line1=lang(39409))
# Path hack, so Kodis Information screen works
with kodidb.GetKodiDB('video') as kodi_db:
try:
kodi_db.pathHack()
log.info('Path hack successful')
except Exception as e:
# Empty movies, tv shows?
log.error('Path hack failed with error message: %s' % str(e))
setScreensaver(value=screensaver)
return True
def processView(self, folderItem, kodi_db, plex_db, totalnodes):
@ -354,7 +314,7 @@ class LibrarySync(Thread):
# Create playlist for the video library
if (foldername not in playlists and
mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
playlistXSP(mediatype, foldername, folderid, viewtype)
playlist_xsp(mediatype, foldername, folderid, viewtype)
playlists.append(foldername)
# Create the video node
if (foldername not in nodes and
@ -396,7 +356,7 @@ class LibrarySync(Thread):
# The tag could be a combined view. Ensure there's
# no other tags with the same name before deleting
# playlist.
playlistXSP(mediatype,
playlist_xsp(mediatype,
current_viewname,
folderid,
current_viewtype,
@ -413,7 +373,7 @@ class LibrarySync(Thread):
# Added new playlist
if (foldername not in playlists and mediatype in
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
playlistXSP(mediatype,
playlist_xsp(mediatype,
foldername,
folderid,
viewtype)
@ -439,7 +399,7 @@ class LibrarySync(Thread):
if mediatype != v.PLEX_TYPE_ARTIST:
if (foldername not in playlists and mediatype in
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
playlistXSP(mediatype,
playlist_xsp(mediatype,
foldername,
folderid,
viewtype)
@ -460,14 +420,8 @@ class LibrarySync(Thread):
Compare the views to Plex
"""
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
if music.set_excludefromscan_music_folders() is True:
log.info('Detected new Music library - restarting now')
# 'New Plex music library detected. Sorry, but we need to
# restart Kodi now due to the changes made.'
dialog('ok', heading='{plex}', line1=lang(39711))
from xbmc import executebuiltin
executebuiltin('RestartApp')
return False
# Will reboot Kodi is new library detected
music.excludefromscan_music_folders()
self.views = []
vnodes = self.vnodes
@ -599,7 +553,7 @@ class LibrarySync(Thread):
Output: self.updatelist, self.allPlexElementsId
self.updatelist APPENDED(!!) list itemids (Plex Keys as
as received from API.getRatingKey())
as received from API.plex_id())
One item in this list is of the form:
'itemId': xxx,
'itemType': 'Movies','TVShows', ...
@ -736,7 +690,7 @@ class LibrarySync(Thread):
for thread in threads:
# Threads might already have quit by themselves (e.g. Kodi exit)
try:
thread.stop_thread()
thread.stop()
except AttributeError:
pass
log.debug("Stop sent to all threads")
@ -758,7 +712,7 @@ class LibrarySync(Thread):
})
self.updatelist = []
@LogTime
@log_time
def PlexMovies(self):
# Initialize
self.allPlexElementsId = {}
@ -775,7 +729,7 @@ class LibrarySync(Thread):
# Pull the list of movies and boxsets in Kodi
try:
self.allKodiElementsId = dict(
plex_db.getChecksum(v.PLEX_TYPE_MOVIE))
plex_db.checksum(v.PLEX_TYPE_MOVIE))
except ValueError:
self.allKodiElementsId = {}
@ -784,7 +738,7 @@ class LibrarySync(Thread):
for view in views:
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
# Get items per view
viewId = view['id']
@ -804,7 +758,7 @@ class LibrarySync(Thread):
self.GetAndProcessXMLs(itemType)
# Update viewstate for EVERY item
for view in views:
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -850,7 +804,7 @@ class LibrarySync(Thread):
with itemMth() as method:
method.updateUserdata(xml)
@LogTime
@log_time
def PlexTVShows(self):
# Initialize
self.allPlexElementsId = {}
@ -867,7 +821,7 @@ class LibrarySync(Thread):
v.PLEX_TYPE_SEASON,
v.PLEX_TYPE_EPISODE):
try:
elements = dict(plex.getChecksum(kind))
elements = dict(plex.checksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/not yet synched
except ValueError:
@ -878,7 +832,7 @@ class LibrarySync(Thread):
for view in views:
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
# Get items per view
viewId = view['id']
@ -907,7 +861,7 @@ class LibrarySync(Thread):
# PROCESS TV Seasons #####
# Cycle through tv shows
for tvShowId in allPlexTvShowsId:
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
# Grab all seasons to tvshow from PMS
seasons = GetAllPlexChildren(tvShowId)
@ -932,7 +886,7 @@ class LibrarySync(Thread):
# PROCESS TV Episodes #####
# Cycle through tv shows
for view in views:
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
# Grab all episodes to tvshow from PMS
episodes = GetAllPlexLeaves(view['id'])
@ -967,7 +921,7 @@ class LibrarySync(Thread):
# Update viewstate:
for view in views:
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -980,7 +934,7 @@ class LibrarySync(Thread):
log.info("%s sync is finished." % itemType)
return True
@LogTime
@log_time
def PlexMusic(self):
itemType = 'Music'
@ -1004,7 +958,7 @@ class LibrarySync(Thread):
for kind in (v.PLEX_TYPE_ARTIST,
v.PLEX_TYPE_ALBUM,
v.PLEX_TYPE_SONG):
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
log.debug("Start processing music %s" % kind)
self.allKodiElementsId = {}
@ -1021,7 +975,7 @@ class LibrarySync(Thread):
# Update viewstate for EVERY item
for view in views:
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
self.PlexUpdateWatched(view['id'], itemType)
@ -1040,7 +994,7 @@ class LibrarySync(Thread):
with plexdb.Get_Plex_DB() as plex_db:
# Pull the list of items already in Kodi
try:
elements = dict(plex_db.getChecksum(kind))
elements = dict(plex_db.checksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/nothing yet synched
except ValueError:
@ -1048,7 +1002,7 @@ class LibrarySync(Thread):
for view in views:
if self.installSyncDone is not True:
state.PATH_VERIFIED = False
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
return False
# Get items per view
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
@ -1133,10 +1087,10 @@ class LibrarySync(Thread):
"""
self.videoLibUpdate = False
self.musicLibUpdate = False
now = getUnixTimestamp()
now = unix_timestamp()
deleteListe = []
for i, item in enumerate(self.itemsToProcess):
if self.thread_stopped() or self.thread_suspended():
if self.stopped() or self.suspended():
# Chances are that Kodi gets shut down
break
if item['state'] == 9:
@ -1215,7 +1169,8 @@ class LibrarySync(Thread):
elif item['type'] in (v.PLEX_TYPE_SHOW,
v.PLEX_TYPE_SEASON,
v.PLEX_TYPE_EPISODE):
log.debug("Removing episode/season/tv show %s" % item['ratingKey'])
log.debug("Removing episode/season/show with plex id %s",
item['ratingKey'])
self.videoLibUpdate = True
with itemtypes.TVShows() as show:
show.remove(item['ratingKey'])
@ -1251,7 +1206,7 @@ class LibrarySync(Thread):
'state': status,
'type': typus,
'ratingKey': str(item['itemID']),
'timestamp': getUnixTimestamp(),
'timestamp': unix_timestamp(),
'attempt': 0
})
elif typus in (v.PLEX_TYPE_MOVIE,
@ -1268,7 +1223,7 @@ class LibrarySync(Thread):
'state': status,
'type': typus,
'ratingKey': plex_id,
'timestamp': getUnixTimestamp(),
'timestamp': unix_timestamp(),
'attempt': 0
})
@ -1307,7 +1262,7 @@ class LibrarySync(Thread):
'state': None, # Don't need a state here
'type': kodi_info[5],
'ratingKey': plex_id,
'timestamp': getUnixTimestamp(),
'timestamp': unix_timestamp(),
'attempt': 0
})
@ -1316,102 +1271,111 @@ class LibrarySync(Thread):
Someone (not necessarily the user signed in) is playing something some-
where
"""
items = []
for item in data:
# Drop buffering messages immediately
status = item['state']
if status == 'buffering':
# Drop buffering messages immediately
continue
ratingKey = str(item['ratingKey'])
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(ratingKey)
if kodi_info is None:
# Item not (yet) in Kodi library
plex_id = item['ratingKey']
skip = False
for pid in (0, 1, 2):
if plex_id == state.PLAYER_STATES[pid]['plex_id']:
# Kodi is playing this item - no need to set the playstate
skip = True
if skip:
continue
sessionKey = item['sessionKey']
# Do we already have a sessionKey stored?
if sessionKey not in self.sessionKeys:
with plexdb.Get_Plex_DB() as plex_db:
kodi_info = plex_db.getItem_byId(plex_id)
if kodi_info is None:
# Item not (yet) in Kodi library
continue
if settings('plex_serverowned') == 'false':
# Not our PMS, we are not authorized to get the
# sessions
# Not our PMS, we are not authorized to get the sessions
# On the bright side, it must be us playing :-)
self.sessionKeys = {
sessionKey: {}
}
self.sessionKeys[sessionKey] = {}
else:
# PMS is ours - get all current sessions
self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN)
log.debug('Updated current sessions. They are: %s'
% self.sessionKeys)
self.sessionKeys.update(GetPMSStatus(state.PLEX_TOKEN))
log.debug('Updated current sessions. They are: %s',
self.sessionKeys)
if sessionKey not in self.sessionKeys:
log.warn('Session key %s still unknown! Skip '
'item' % sessionKey)
log.info('Session key %s still unknown! Skip '
'playstate update', sessionKey)
continue
currSess = self.sessionKeys[sessionKey]
# Attach Kodi info to the session
self.sessionKeys[sessionKey]['kodi_id'] = kodi_info[0]
self.sessionKeys[sessionKey]['file_id'] = kodi_info[1]
self.sessionKeys[sessionKey]['kodi_type'] = kodi_info[4]
session = self.sessionKeys[sessionKey]
if settings('plex_serverowned') != 'false':
# Identify the user - same one as signed on with PKC? Skip
# update if neither session's username nor userid match
# (Owner sometime's returns id '1', not always)
if (not state.PLEX_TOKEN and currSess['userId'] == '1'):
if not state.PLEX_TOKEN and session['userId'] == '1':
# PKC not signed in to plex.tv. Plus owner of PMS is
# playing (the '1').
# Hence must be us (since several users require plex.tv
# token for PKC)
pass
elif not (currSess['userId'] == state.PLEX_USER_ID
or
currSess['username'] == state.PLEX_USERNAME):
elif not (session['userId'] == state.PLEX_USER_ID or
session['username'] == state.PLEX_USERNAME):
log.debug('Our username %s, userid %s did not match '
'the session username %s with userid %s'
% (state.PLEX_USERNAME,
state.PLEX_USER_ID,
currSess['username'],
currSess['userId']))
'the session username %s with userid %s',
state.PLEX_USERNAME,
state.PLEX_USER_ID,
session['username'],
session['userId'])
continue
# Get an up-to-date XML from the PMS
# because PMS will NOT directly tell us:
# duration of item
# viewCount
if currSess.get('duration') is None:
xml = GetPlexMetadata(ratingKey)
# Get an up-to-date XML from the PMS because PMS will NOT directly
# tell us: duration of item viewCount
if session.get('duration') is None:
xml = GetPlexMetadata(plex_id)
if xml in (None, 401):
log.error('Could not get up-to-date xml for item %s'
% ratingKey)
log.error('Could not get up-to-date xml for item %s',
plex_id)
continue
API = PlexAPI.API(xml[0])
userdata = API.getUserData()
currSess['duration'] = userdata['Runtime']
currSess['viewCount'] = userdata['PlayCount']
api = PlexAPI.API(xml[0])
userdata = api.userdata()
session['duration'] = userdata['Runtime']
session['viewCount'] = userdata['PlayCount']
# Sometimes, Plex tells us resume points in milliseconds and
# not in seconds - thank you very much!
if item.get('viewOffset') > currSess['duration']:
resume = item.get('viewOffset') / 1000
if item['viewOffset'] > session['duration']:
resume = item['viewOffset'] / 1000
else:
resume = item.get('viewOffset')
# Append to list that we need to process
items.append({
'ratingKey': ratingKey,
'kodi_id': kodi_info[0],
'file_id': kodi_info[1],
'kodi_type': kodi_info[4],
'viewOffset': resume,
'state': status,
'duration': currSess['duration'],
'viewCount': currSess['viewCount'],
'lastViewedAt': DateToKodi(getUnixTimestamp())
})
log.debug('Update playstate for user %s with id %s: %s'
% (state.PLEX_USERNAME,
state.PLEX_USER_ID,
items[-1]))
# Now tell Kodi where we are
for item in items:
itemFkt = getattr(itemtypes,
v.ITEMTYPE_FROM_KODITYPE[item['kodi_type']])
with itemFkt() as Fkt:
Fkt.updatePlaystate(item)
resume = item['viewOffset']
if resume < v.IGNORE_SECONDS_AT_START:
continue
try:
completed = float(resume) / float(session['duration'])
except (ZeroDivisionError, TypeError):
log.error('Could not mark playstate for %s and session %s',
data, session)
continue
if completed >= v.MARK_PLAYED_AT:
# Only mark completely watched ONCE
if session.get('marked_played') is None:
session['marked_played'] = True
mark_played = True
else:
# Don't mark it as completely watched again
continue
else:
mark_played = False
log.debug('Update playstate for user %s with id %s for plex id %s',
state.PLEX_USERNAME, state.PLEX_USER_ID, plex_id)
item_fkt = getattr(itemtypes,
v.ITEMTYPE_FROM_KODITYPE[session['kodi_type']])
with item_fkt() as fkt:
fkt.updatePlaystate(mark_played,
session['viewCount'],
resume,
session['duration'],
session['file_id'],
unix_date_to_kodi(unix_timestamp()))
def fanartSync(self, refresh=False):
"""
@ -1455,9 +1419,9 @@ class LibrarySync(Thread):
window('plex_dbScan', value="true")
state.DB_SCAN = True
# First remove playlists
deletePlaylists()
delete_playlists()
# Remove video nodes
deleteNodes()
delete_nodes()
# Kick off refresh
if self.maintainViews() is True:
# Ran successfully
@ -1509,21 +1473,19 @@ class LibrarySync(Thread):
def run_internal(self):
# Re-assign handles to have faster calls
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
stopped = self.stopped
suspended = self.suspended
installSyncDone = self.installSyncDone
background_sync = state.BACKGROUND_SYNC
fullSync = self.fullSync
processMessage = self.processMessage
processItems = self.processItems
FULL_SYNC_INTERVALL = state.FULL_SYNC_INTERVALL
lastSync = 0
lastTimeSync = 0
lastProcessing = 0
oneDay = 60*60*24
# Link to Websocket queue
queue = self.mgr.ws.queue
queue = state.WEBSOCKET_QUEUE
startupComplete = False
self.views = []
@ -1536,12 +1498,12 @@ class LibrarySync(Thread):
if settings('FanartTV') == 'true':
self.fanartthread.start()
while not thread_stopped():
while not stopped():
# In the event the server goes offline
while thread_suspended():
while suspended():
# Set in service.py
if thread_stopped():
if stopped():
# Abort was requested while waiting. We should exit
log.info("###===--- LibrarySync Stopped ---===###")
return
@ -1552,11 +1514,9 @@ class LibrarySync(Thread):
self.force_dialog = False
# Verify the validity of the database
currentVersion = settings('dbCreatedWithVersion')
minVersion = window('plex_minDBVersion')
if not compare_version(currentVersion, minVersion):
if not compare_version(currentVersion, v.MIN_DB_VERSION):
log.warn("Db version out of date: %s minimum version "
"required: %s" % (currentVersion, minVersion))
"required: %s", currentVersion, v.MIN_DB_VERSION)
# DB out of date. Proceed to recreate?
resp = dialog('yesno',
heading=lang(29999),
@ -1576,11 +1536,11 @@ class LibrarySync(Thread):
# Also runs when first installed
# Verify the video database can be found
videoDb = v.DB_VIDEO_PATH
if not exists(tryEncode(videoDb)):
if not exists(try_encode(videoDb)):
# Database does not exists
log.error("The current Kodi version is incompatible "
"to know which Kodi versions are supported.")
log.error('Current Kodi version: %s' % tryDecode(
log.error('Current Kodi version: %s' % try_decode(
xbmc.getInfoLabel('System.BuildVersion')))
# "Current Kodi version is unsupported, cancel lib sync"
dialog('ok', heading='{plex}', line1=lang(39403))
@ -1589,10 +1549,10 @@ class LibrarySync(Thread):
state.DB_SCAN = True
window('plex_dbScan', value="true")
log.info("Db version: %s" % settings('dbCreatedWithVersion'))
lastTimeSync = getUnixTimestamp()
lastTimeSync = unix_timestamp()
# Initialize time offset Kodi - PMS
self.syncPMStime()
lastSync = getUnixTimestamp()
lastSync = unix_timestamp()
if settings('FanartTV') == 'true':
# Start getting additional missing artwork
with plexdb.Get_Plex_DB() as plex_db:
@ -1606,10 +1566,12 @@ class LibrarySync(Thread):
'refresh': True
})
log.info('Refreshing video nodes and playlists now')
deletePlaylists()
deleteNodes()
delete_playlists()
delete_nodes()
log.info("Initial start-up full sync starting")
xbmc.executebuiltin('InhibitIdleShutdown(true)')
librarySync = fullSync()
xbmc.executebuiltin('InhibitIdleShutdown(false)')
window('plex_dbScan', clear=True)
state.DB_SCAN = False
if librarySync:
@ -1631,16 +1593,16 @@ class LibrarySync(Thread):
self.triage_lib_scans()
self.force_dialog = False
continue
now = getUnixTimestamp()
now = unix_timestamp()
# Standard syncs - don't force-show dialogs
self.force_dialog = False
if (now - lastSync > FULL_SYNC_INTERVALL and
if (now - lastSync > state.FULL_SYNC_INTERVALL and
not self.xbmcplayer.isPlaying()):
lastSync = now
log.info('Doing scheduled full library scan')
state.DB_SCAN = True
window('plex_dbScan', value="true")
if fullSync() is False and not thread_stopped():
if fullSync() is False and not stopped():
log.error('Could not finish scheduled full sync')
self.force_dialog = True
self.showKodiNote(lang(39410),
@ -1658,7 +1620,7 @@ class LibrarySync(Thread):
self.syncPMStime()
window('plex_dbScan', clear=True)
state.DB_SCAN = False
elif background_sync:
elif not state.BACKGROUND_SYNC_DISABLED:
# Check back whether we should process something
# Only do this once every while (otherwise, potentially
# many screen refreshes lead to flickering)

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
fails with e.g. Android TV's Python, which does not accept arguments for
@ -43,5 +43,5 @@ class LogHandler(logging.StreamHandler):
try:
xbmc.log(self.format(record), level=LEVELS[record.levelno])
except UnicodeEncodeError:
xbmc.log(tryEncode(self.format(record)),
xbmc.log(try_encode(self.format(record)),
level=LEVELS[record.levelno])

View file

@ -1,57 +1,33 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from re import compile as re_compile
import xml.etree.ElementTree as etree
from xml.etree.ElementTree import ParseError
from utils import advancedsettings_xml, indent, tryEncode
from utils import XmlKodiSetting, reboot_kodi, language as lang
from PlexFunctions import get_plex_sections
from PlexAPI import API
import variables as v
###############################################################################
log = getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
###############################################################################
def get_current_music_folders():
"""
Returns a list of encoded strings as paths to the currently "blacklisted"
excludefromscan music folders in the advancedsettings.xml
"""
paths = []
root, _ = advancedsettings_xml(['audio', 'excludefromscan'])
if root is None:
return paths
for element in root:
try:
path = REGEX_MUSICPATH.findall(element.text)[0]
except IndexError:
log.error('Could not parse %s of xml element %s'
% (element.text, element.tag))
continue
else:
paths.append(path)
return paths
def set_excludefromscan_music_folders():
def excludefromscan_music_folders():
"""
Gets a complete list of paths for music libraries from the PMS. Sets them
to be excluded in the advancedsettings.xml from being scanned by Kodi.
Existing keys will be replaced
Returns False if no new Plex libraries needed to be exluded, True otherwise
Reboots Kodi if new library detected
"""
changed = False
write_xml = False
xml = get_plex_sections()
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not get Plex sections')
LOG.error('Could not get Plex sections')
return
# Build paths
paths = []
@ -62,43 +38,42 @@ def set_excludefromscan_music_folders():
continue
for location in library:
if location.tag == 'Location':
path = api.validatePlayurl(location.attrib['path'],
typus=v.PLEX_TYPE_ARTIST,
omitCheck=True)
path = api.validate_playurl(location.attrib['path'],
typus=v.PLEX_TYPE_ARTIST,
omit_check=True)
paths.append(__turn_to_regex(path))
# Get existing advancedsettings
root, tree = advancedsettings_xml(['audio', 'excludefromscan'],
force_create=True)
for path in paths:
for element in root:
if element.text == path:
# Path already excluded
break
else:
changed = True
write_xml = True
log.info('New Plex music library detected: %s' % path)
element = etree.Element(tag='regexp')
element.text = path
root.append(element)
# Delete obsolete entries (unlike above, we don't change 'changed' to not
# enforce a restart)
for element in root:
for path in paths:
if element.text == path:
break
else:
log.info('Deleting Plex music library from advancedsettings: %s'
% element.text)
root.remove(element)
write_xml = True
if write_xml is True:
indent(tree.getroot())
tree.write('%sadvancedsettings.xml' % v.KODI_PROFILE, encoding="UTF-8")
return changed
try:
with XmlKodiSetting('advancedsettings.xml',
force_create=True,
top_element='advancedsettings') as xml:
parent = xml.set_setting(['audio', 'excludefromscan'])
for path in paths:
for element in parent:
if element.text == path:
# Path already excluded
break
else:
LOG.info('New Plex music library detected: %s', path)
xml.set_setting(['audio', 'excludefromscan', 'regexp'],
value=path, append=True)
# We only need to reboot if we ADD new paths!
reboot = xml.write_xml
# Delete obsolete entries
for element in parent:
for path in paths:
if element.text == path:
break
else:
LOG.info('Deleting music library from advancedsettings: %s',
element.text)
parent.remove(element)
except (ParseError, IOError):
LOG.error('Could not adjust advancedsettings.xml')
reboot = False
if reboot is True:
# 'New Plex music library detected. Sorry, but we need to
# restart Kodi now due to the changes made.'
reboot_kodi(lang(39711))
def __turn_to_regex(path):

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
functions won't work. See the Pickle documentation
"""
log('%sStart pickling: %s' % (PREFIX, obj), level=LOGDEBUG)
log('%sStart pickling' % PREFIX, level=LOGDEBUG)
pickl_window(window_var, value=dumps(obj))
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
@ -46,7 +46,7 @@ def unpickle_me(window_var='plex_result'):
pickl_window(window_var, clear=True)
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
obj = loads(result)
log('%sSuccessfully unpickled: %s' % (PREFIX, obj), level=LOGDEBUG)
log('%sSuccessfully unpickled' % PREFIX, level=LOGDEBUG)
return obj

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 -*-
###############################################################################
import logging
from logging import getLogger
from threading import Thread
from urlparse import parse_qsl
from xbmc import Player
from PKC_listitem import PKC_ListItem
from pickler import pickle_me, Playback_Successful
from playbackutils import PlaybackUtils
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
import playback
from context_entry import ContextMenu
import state
import json_rpc as js
from pickler import pickle_me, Playback_Successful
import kodidb_functions as kodidb
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@ -30,135 +22,36 @@ class Playback_Starter(Thread):
"""
Processes new plays
"""
def __init__(self, callback=None):
self.mgr = callback
self.playqueue = self.mgr.playqueue
Thread.__init__(self)
def process_play(self, plex_id, kodi_id=None):
"""
Processes Kodi playback init for ONE item
"""
log.info("Process_play called with plex_id %s, kodi_id %s"
% (plex_id, kodi_id))
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):
_, params = item.split('?', 1)
try:
_, params = item.split('?', 1)
except ValueError:
# e.g. when plugin://...tvshows is called for entire season
with kodidb.GetKodiDB('video') as kodi_db:
show_id = kodi_db.show_id_from_path(item)
if show_id:
js.activate_window('videos',
'videodb://tvshows/titles/%s' % show_id)
else:
LOG.error('Could not find tv show id for %s', item)
pickle_me(Playback_Successful())
return
params = dict(parse_qsl(params))
mode = params.get('mode')
log.debug('Received mode: %s, params: %s' % (mode, params))
try:
if mode == 'play':
result = self.process_play(params.get('id'),
params.get('dbid'))
elif mode == 'companion':
result = self.process_companion()
elif mode == 'plex_node':
result = self.process_plex_node(
params.get('key'),
params.get('view_offset'),
directplay=True if params.get('play_directly') else False,
node=False if params.get('node') == 'false' else True)
elif mode == 'context_menu':
ContextMenu()
result = Playback_Successful()
except:
log.error('Error encountered for mode %s, params %s'
% (mode, params))
import traceback
log.error(traceback.format_exc())
# Let default.py know!
pickle_me(None)
else:
pickle_me(result)
LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play':
playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'),
path=params.get('path'))
elif mode == 'plex_node':
playback.process_indirect(params['key'], params['offset'])
elif mode == 'context_menu':
ContextMenu(kodi_id=params['kodi_id'],
kodi_type=params['kodi_type'])
def run(self):
queue = self.mgr.command_pipeline.playback_queue
log.info("----===## Starting Playback_Starter ##===----")
queue = state.COMMAND_PIPELINE_QUEUE
LOG.info("----===## Starting Playback_Starter ##===----")
while True:
item = queue.get()
if item is None:
@ -167,4 +60,4 @@ class Playback_Starter(Thread):
else:
self.triage(item)
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 -*-
###############################################################################
import logging
import json
from logging import getLogger
import copy
import xbmc
from utils import window, DateToKodi, getUnixTimestamp, tryDecode, tryEncode
import downloadutils
import plexdb_functions as plexdb
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 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 = {}
playStats = {}
currentFile = None
def _record_playstate(status, ended):
if not status['plex_id']:
LOG.debug('No Plex id found to record playstate for status %s', status)
return
with plexdb.Get_Plex_DB() as plex_db:
kodi_db_item = plex_db.getItem_byId(status['plex_id'])
if kodi_db_item is None:
# Item not (yet) in Kodi library
LOG.debug('No playstate update due to Plex id not found: %s', status)
return
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):
self.__dict__ = self._shared_state
self.doUtils = downloadutils.DownloadUtils().downloadUrl
xbmc.Player.__init__(self)
log.info("Started playback monitor.")
def GetPlayStats(self):
return self.playStats
LOG.info("Started playback monitor.")
def onPlayBackStarted(self):
"""
Will be called when xbmc starts playing a file.
Window values need to have been set in Kodimonitor.py
"""
self.stopAll()
# 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'''
pass
def onPlayBackPaused(self):
currentFile = self.currentFile
log.info("PLAYBACK_PAUSED: %s" % currentFile)
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = True
"""
Will be called when playback is paused
"""
pass
def onPlayBackResumed(self):
currentFile = self.currentFile
log.info("PLAYBACK_RESUMED: %s" % currentFile)
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = False
"""
Will be called when playback is resumed
"""
pass
def onPlayBackSeek(self, time, seekOffset):
# Make position when seeking a bit more accurate
currentFile = self.currentFile
log.info("PLAYBACK_SEEK: %s" % currentFile)
if self.played_info.get(currentFile):
try:
position = self.getTime()
except RuntimeError:
# When Kodi is not playing
return
self.played_info[currentFile]['currentPosition'] = position
"""
Will be called when user seeks to a certain time during playback
"""
pass
def onPlayBackStopped(self):
# Will be called when user stops xbmc playing a file
log.info("ONPLAYBACK_STOPPED")
self.stopAll()
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.")
"""
Will be called when playback is stopped by the user
"""
LOG.debug("ONPLAYBACK_STOPPED")
playback_cleanup()
def onPlayBackEnded(self):
# Will be called when xbmc stops playing a file, because the file ended
log.info("ONPLAYBACK_ENDED")
self.onPlayBackStopped()
def stopAll(self):
if not self.played_info:
return
log.info("Played_information: %s" % self.played_info)
# Process each items
for item in self.played_info:
data = self.played_info.get(item)
if not data:
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()
"""
Will be called when playback ends due to the media file being finished
"""
LOG.debug("ONPLAYBACK_ENDED")
if state.PKC_CAUSED_STOP is True:
state.PKC_CAUSED_STOP = False
LOG.debug('PKC caused this playback stop - ignoring')
else:
playback_cleanup(ended=True)

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 urlparse import parse_qsl, urlsplit
from re import compile as re_compile
import plexdb_functions as plexdb
from downloadutils import DownloadUtils as DU
from utils import JSONRPC, tryEncode, escape_html
from utils import try_decode, try_encode
from PlexAPI import API
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+)''')
###############################################################################
# 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):
playlistid = None # Kodi playlist ID, [int]
type = None # Kodi type: 'audio', 'video', 'picture'
kodi_pl = None # Kodi xbmc.PlayList object
items = [] # list of PLAYLIST_ITEMS
old_kodi_pl = [] # to store old Kodi JSON result with all pl items
ID = None # Plex id, e.g. playQueueID
version = None # Plex version, [int]
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()
class PlaylistObjectBaseclase(object):
"""
Base class
"""
def __init__(self):
self.playlistid = None
self.type = None
self.kodi_pl = None
self.items = []
self.old_kodi_pl = []
self.ID = None
self.id = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
log.debug('Playlist cleared: %s' % self)
# 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'
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'
class Playlist_Item(object):
ID = None # Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None # Plex unique item id, "ratingKey"
plex_type = None # Plex type, e.g. 'movie', 'clip'
plex_UUID = None # Plex librarySectionUUID
kodi_id = None # Kodi unique kodi id (unique only within type!)
kodi_type = None # Kodi type: 'movie'
file = None # Path to the item's file. STRING!!
uri = None # Weird Plex uri path involving plex_UUID. STRING!
guid = None # Weird Plex guid
"""
Object to fill our playqueues and playlists with.
id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None [str] Plex unique item id, "ratingKey"
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
plex_uuid = None [str] Plex librarySectionUUID
kodi_id = None Kodi unique kodi id (unique only within type!)
kodi_type = None [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):
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__:
if type(getattr(self, key)) in (str, unicode):
answ += '%s: %s, ' % (key, tryEncode(getattr(self, key)))
if key in ('id', 'plex_id', 'xml'):
continue
if isinstance(getattr(self, key), str):
answ += '\'%s\': \'%s\', ' % (key,
try_decode(getattr(self, key)))
else:
# e.g. int
answ += '%s: %s, ' % (key, str(getattr(self, key)))
return answ[:-2] + ">"
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
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):
@ -114,7 +260,7 @@ def playlist_item_from_kodi(kodi_item):
try:
item.plex_id = plex_dbitem[0]
item.plex_type = plex_dbitem[2]
item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-)
item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-)
except TypeError:
pass
item.file = kodi_item.get('file')
@ -122,16 +268,52 @@ def playlist_item_from_kodi(kodi_item):
query = dict(parse_qsl(urlsplit(item.file).query))
item.plex_id = query.get('plex_id')
item.plex_type = query.get('itemType')
if item.plex_id is None:
if item.plex_id is None and item.file is not None:
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, item.plex_id))
log.debug('Made playlist item from Kodi: %s' % item)
(item.plex_uuid, item.plex_id))
LOG.debug('Made playlist item from Kodi: %s', 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):
"""
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.kodi_id = plex_dbitem[0]
item.kodi_type = plex_dbitem[4]
except:
except (TypeError, IndexError):
raise KeyError('Could not find plex_id %s in database' % plex_id)
item.plex_UUID = plex_id
item.plex_uuid = plex_id
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, plex_id))
log.debug('Made playlist item from plex: %s' % item)
(item.plex_uuid, plex_id))
LOG.debug('Made playlist item from plex: %s', 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
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
"""
item = Playlist_Item()
api = API(xml_video_element)
item.plex_id = api.getRatingKey()
item.plex_type = api.getType()
item.ID = xml_video_element.attrib['%sItemID' % playlist.kind]
item.guid = xml_video_element.attrib.get('guid')
if item.guid is not None:
item.guid = escape_html(item.guid)
if item.plex_id:
item.plex_id = api.plex_id()
item.plex_type = api.plex_type()
# item.id will only be set if you passed in an xml_video_element from e.g.
# a playQueue
item.id = api.item_id()
if kodi_id is not None:
item.kodi_id = kodi_id
item.kodi_type = kodi_type
elif item.plex_id is not None:
with plexdb.Get_Plex_DB() as plex_db:
db_element = plex_db.getItem_byId(item.plex_id)
try:
item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4]
item.kodi_id, item.kodi_type = db_element[0], db_element[4]
except TypeError:
pass
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
def _get_playListVersion_from_xml(playlist, xml):
"""
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
playQueueVersion). Returns True if successful, False otherwise
playQueueVersion).
Raises PlaylistError if unsuccessful
"""
try:
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
except (TypeError, AttributeError, KeyError):
log.error('Could not get new playlist Version for playlist %s'
% playlist)
return False
return True
raise PlaylistError('Could not get new playlist Version for playlist '
'%s' % playlist)
def get_playlist_details_from_xml(playlist, xml):
"""
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
playlist.ID with the XML's playQueueID
playlist.id with the XML's playQueueID
Raises PlaylistError if something went wrong.
"""
try:
playlist.ID = xml.attrib['%sID' % playlist.kind]
playlist.id = xml.attrib['%sID' % playlist.kind]
playlist.version = xml.attrib['%sVersion' % playlist.kind]
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
playlist.selectedItemID = xml.attrib.get(
'%sSelectedItemID' % playlist.kind)
playlist.selectedItemOffset = xml.attrib.get(
'%sSelectedItemOffset' % playlist.kind)
except:
log.error('Could not parse xml answer from PMS for playlist %s'
% playlist)
import traceback
log.error(traceback.format_exc())
raise KeyError
log.debug('Updated playlist from xml: %s' % playlist)
LOG.debug('Updated playlist from xml: %s', playlist)
except (TypeError, KeyError, AttributeError) as msg:
raise PlaylistError('Could not get playlist details from xml: %s',
msg)
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
@ -226,11 +415,7 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
# Clear our existing playlist and the associated Kodi playlist
playlist.clear()
# Set new values
try:
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not update playlist from PMS')
return
get_playlist_details_from_xml(playlist, xml)
for plex_item in xml:
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
if playlist_item is not None:
@ -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):
"""
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:
if 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",
parameters=params)
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not init Plex playlist')
return
# Need to get the details for the playlist item
item = playlist_item_from_xml(xml[0])
except (KeyError, IndexError, TypeError):
raise PlaylistError('Could not init Plex playlist with plex_id %s and '
'kodi_item %s' % (plex_id, kodi_item))
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,
@ -274,10 +465,10 @@ def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
file: str!!
"""
log.debug('add_listitem_to_playlist at position %s. Playlist before add: '
'%s' % (pos, playlist))
LOG.debug('add_listitem_to_playlist at position %s. Playlist before add: '
'%s', pos, playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.ID is None:
if playlist.id is None:
init_Plex_playlist(playlist, plex_id, kodi_item)
else:
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
@ -300,51 +491,58 @@ def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
plex_id=None, file=None):
"""
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
file: str!
file: str!
Raises PlaylistError if something went wrong
"""
log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist)
LOG.debug('add_item_to_playlist. Playlist before adding: %s', playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.ID is None:
init_Plex_playlist(playlist, plex_id, kodi_item)
if playlist.id is None:
item = init_Plex_playlist(playlist, plex_id, kodi_item)
else:
add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
kodi_id = playlist.items[pos].kodi_id
kodi_type = playlist.items[pos].kodi_type
file = playlist.items[pos].file
add_item_to_kodi_playlist(playlist, pos, kodi_id, kodi_type, file)
item = add_item_to_PMS_playlist(playlist, pos, plex_id, kodi_item)
params = {
'playlistid': playlist.playlistid,
'position': pos
}
if item.kodi_id is not None:
params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)}
else:
params['item'] = {'file': item.file}
reply = 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):
"""
Adds a new item to the playlist at position pos [int] only on the Plex
side of things (e.g. because the user changed the Kodi side)
WILL ALSO UPDATE OUR PLAYLISTS
Returns the PKC PlayList item or raises PlaylistError
"""
verify_kodi_item(plex_id, kodi_item)
if plex_id:
try:
item = playlist_item_from_plex(plex_id)
except KeyError:
log.error('Could not add new item to the PMS playlist')
return
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri)
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri)
# Will always put the new item at the end of the Plex playlist
xml = DU().downloadUrl(url, action_type="PUT")
try:
item.ID = xml[-1].attrib['%sItemID' % playlist.kind]
except IndexError:
log.info('Could not get playlist children. Adding a dummy')
except (TypeError, AttributeError, KeyError):
log.error('Could not add item %s to playlist %s'
% (kodi_item, playlist))
return
# Get the guid for this item
for plex_item in xml:
if plex_item.attrib['%sItemID' % playlist.kind] == item.ID:
item.guid = escape_html(plex_item.attrib['guid'])
xml[-1].attrib
except (TypeError, AttributeError, KeyError, IndexError):
raise PlaylistError('Could not add item %s to playlist %s'
% (kodi_item, playlist))
api = API(xml[-1])
item.xml = xml[-1]
item.id = api.item_id()
item.guid = api.guid_html_escaped()
item.offset = api.resume_point()
item.playcount = api.viewcount()
playlist.items.append(item)
if pos == len(playlist.items) - 1:
# Item was added at the end
@ -354,21 +552,22 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
move_playlist_item(playlist,
len(playlist.items) - 1,
pos)
log.debug('Successfully added item on the Plex side: %s' % playlist)
LOG.debug('Successfully added item on the Plex side: %s', playlist)
return item
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
file=None):
file=None, xml_video_element=None):
"""
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
Returns False if unsuccessful
Returns the playlist item that was just added or raises PlaylistError
file: str!
"""
log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
'only at position %s for %s'
% (kodi_id, kodi_type, file, pos, playlist))
LOG.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
'only at position %s for %s',
kodi_id, kodi_type, file, pos, playlist)
params = {
'playlistid': playlist.playlistid,
'position': pos
@ -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)}
else:
params['item'] = {'file': file}
reply = JSONRPC('Playlist.Insert').execute(params)
reply = js.playlist_insert(params)
if reply.get('error') is not None:
log.error('Could not add item to playlist. Kodi reply. %s' % reply)
return False
else:
playlist.items.insert(pos, playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file}))
return True
raise PlaylistError('Could not add item to playlist. Kodi reply. %s',
reply)
if xml_video_element is not None:
item = playlist_item_from_xml(xml_video_element)
item.kodi_id = kodi_id
item.kodi_type = kodi_type
item.file = file
elif kodi_id is not None:
item = playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None:
xml = GetPlexMetadata(item.plex_id)
item.xml = xml[-1]
playlist.items.insert(pos, item)
return item
def move_playlist_item(playlist, before_pos, after_pos):
"""
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
WILL ALSO CHANGE OUR PLAYLISTS. Returns True if successful
WILL ALSO CHANGE OUR PLAYLISTS.
"""
log.debug('Moving item from %s to %s on the Plex side for %s'
% (before_pos, after_pos, playlist))
LOG.debug('Moving item from %s to %s on the Plex side for %s',
before_pos, after_pos, playlist)
if after_pos == 0:
url = "{server}/%ss/%s/items/%s/move?after=0" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID)
playlist.id,
playlist.items[before_pos].id)
else:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(playlist.kind,
playlist.ID,
playlist.items[before_pos].ID,
playlist.items[after_pos - 1].ID)
playlist.id,
playlist.items[before_pos].id,
playlist.items[after_pos - 1].id)
# We need to increment the playlistVersion
if _get_playListVersion_from_xml(
playlist, DU().downloadUrl(url, action_type="PUT")) is False:
return False
_get_playListVersion_from_xml(
playlist, DU().downloadUrl(url, action_type="PUT"))
# Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
log.debug('Done moving for %s' % playlist)
return True
LOG.debug('Done moving for %s', playlist)
def get_PMS_playlist(playlist, playlist_id=None):
@ -423,7 +629,7 @@ def get_PMS_playlist(playlist, playlist_id=None):
Returns None if something went wrong
"""
playlist_id = playlist_id if playlist_id else playlist.ID
playlist_id = playlist_id if playlist_id else playlist.id
xml = DU().downloadUrl(
"{server}/%ss/%s" % (playlist.kind, playlist_id),
headerOptions={'Accept': 'application/xml'})
@ -439,63 +645,24 @@ def refresh_playlist_from_PMS(playlist):
Only updates the selected item from the PMS side (e.g.
playQueueSelectedItemID). Will NOT check whether items still make sense.
"""
xml = get_PMS_playlist(playlist)
try:
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not refresh playlist from PMS')
get_playlist_details_from_xml(playlist, get_PMS_playlist(playlist))
def delete_playlist_item_from_PMS(playlist, pos):
"""
Delete the item at position pos [int] on the Plex side and our playlists
"""
log.debug('Deleting position %s for %s on the Plex side' % (pos, playlist))
LOG.debug('Deleting position %s for %s on the Plex side', pos, playlist)
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
(playlist.kind,
playlist.ID,
playlist.items[pos].ID,
playlist.id,
playlist.items[pos].id,
playlist.repeat),
action_type="DELETE")
_get_playListVersion_from_xml(playlist, xml)
del playlist.items[pos]
def get_kodi_playlist_items(playlist):
"""
Returns a list of the current Kodi playlist items using JSON
E.g.:
[{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file':
u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}]
"""
answ = JSONRPC('Playlist.GetItems').execute({
'playlistid': playlist.playlistid,
'properties': ["title", "file"]
})
try:
answ = answ['result']['items']
except KeyError:
answ = []
return answ
def get_kodi_playqueues():
"""
Example return: [{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}]
"""
queues = JSONRPC('Playlist.GetPlaylists').execute()
try:
queues = queues['result']
except KeyError:
log.error('Could not get Kodi playqueues. JSON Result was: %s'
% queues)
queues = []
return queues
# Functions operating on the Kodi playlist objects ##########
def add_to_Kodi_playlist(playlist, xml_video_element):
@ -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).
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)
params = {
'playlistid': playlist.playlistid
}
item = playlist_item_from_xml(xml_video_element)
if item.kodi_id:
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
json_item = {'%sid' % item.kodi_type: item.kodi_id}
else:
params['item'] = {'file': item.file}
reply = JSONRPC('Playlist.Add').execute(params)
json_item = {'file': item.file}
reply = js.playlist_add(playlist.playlistid, json_item)
if reply.get('error') is not None:
log.error('Could not add item %s to Kodi playlist. Error: %s'
% (xml_video_element, reply))
return None
else:
return item
raise PlaylistError('Could not add item %s to Kodi playlist. Error: '
'%s', xml_video_element, reply)
return item
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!
"""
log.debug('Insert listitem at position %s for Kodi only for %s'
% (pos, playlist))
LOG.debug('Insert listitem at position %s for Kodi only for %s',
pos, playlist)
# Add the item into Kodi playlist
playlist.kodi_pl.add(file, listitem, index=pos)
playlist.kodi_pl.add(url=file, listitem=listitem, index=pos)
# We need to add this to our internal queue as well
if xml_video_element is not None:
item = playlist_item_from_xml(playlist, xml_video_element)
item = playlist_item_from_xml(xml_video_element)
else:
item = playlist_item_from_kodi(kodi_item)
if file is not None:
item.file = file
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.
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
"""
log.debug('Removing position %s from Kodi only from %s' % (pos, playlist))
reply = JSONRPC('Playlist.Remove').execute({
'playlistid': playlist.playlistid,
'position': pos
})
LOG.debug('Removing position %s from Kodi only from %s', pos, playlist)
reply = js.playlist_remove(playlist.playlistid, pos)
if reply.get('error') is not None:
log.error('Could not delete the item from the playlist. Error: %s'
% reply)
LOG.error('Could not delete the item from the playlist. Error: %s',
reply)
return
else:
try:
del playlist.items[pos]
except IndexError:
log.error('Cannot delete position %s for %s' % (pos, playlist))
try:
del playlist.items[pos]
except IndexError:
LOG.error('Cannot delete position %s for %s', pos, playlist)
def get_pms_playqueue(playqueue_id):
@ -578,7 +737,7 @@ def get_pms_playqueue(playqueue_id):
try:
xml.attrib
except AttributeError:
log.error('Could not download Plex playqueue %s' % playqueue_id)
LOG.error('Could not download Plex playqueue %s', playqueue_id)
xml = None
return xml
@ -593,12 +752,12 @@ def get_plextype_from_xml(xml):
try:
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0]
except IndexError:
log.error('Could not get plex_id from xml: %s' % xml.attrib)
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return
new_xml = GetPlexMetadata(plex_id)
try:
new_xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not get plex metadata for plex id %s' % plex_id)
LOG.error('Could not get plex metadata for plex id %s', plex_id)
return
return new_xml[0].attrib.get('type')

View file

@ -1,229 +1,242 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from threading import RLock, Thread
"""
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
"""
from logging import getLogger
from threading import Thread
from re import compile as re_compile
from xbmc import 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
from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren
from PlexFunctions import GetAllPlexChildren
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 state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
# Lock used for playqueue manipulations
lock = RLock()
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'])
class Playqueue(Thread):
def init_playqueues():
"""
Monitors Kodi's playqueues for changes on the Kodi side
Call this once on startup to initialize the PKC playqueue objects in
the list PLAYQUEUES
"""
# Borg - multiple instances, shared state
__shared_state = {}
playqueues = None
def __init__(self, callback=None):
self.__dict__ = self.__shared_state
if self.playqueues is not None:
log.debug('Playqueue thread has already been initialized')
Thread.__init__(self)
return
self.mgr = callback
# Initialize Kodi playqueues
with lock:
self.playqueues = []
for queue in PL.get_kodi_playqueues():
if PLAYQUEUES:
LOG.debug('Playqueues have already been initialized')
return
# Initialize Kodi playqueues
with LOCK:
for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists():
if queue['playlistid'] != i:
continue
playqueue = PL.Playqueue_Object()
playqueue.playlistid = queue['playlistid']
playqueue.playlistid = i
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == 'audio':
if playqueue.type == v.KODI_TYPE_AUDIO:
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
elif playqueue.type == 'video':
elif playqueue.type == v.KODI_TYPE_VIDEO:
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
else:
# Currently, only video or audio playqueues available
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
# Overwrite 'picture' with 'photo'
playqueue.type = v.KODI_TYPE_PHOTO
self.playqueues.append(playqueue)
# sort the list by their playlistid, just in case
self.playqueues = sorted(
self.playqueues, key=lambda i: i.playlistid)
log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
Thread.__init__(self)
PLAYQUEUES.append(playqueue)
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
def get_playqueue_from_type(self, typus):
"""
Returns the playqueue according to the typus ('video', 'audio',
'picture') passed in
"""
with lock:
for playqueue in self.playqueues:
if playqueue.type == typus:
break
else:
raise ValueError('Wrong playlist type passed in: %s' % typus)
return playqueue
def init_playqueue_from_plex_children(self, plex_id):
"""
Init a new playqueue e.g. from an album. Alexa does this
Returns the Playlist_Object
"""
xml = GetAllPlexChildren(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not download the PMS xml for %s' % plex_id)
return
playqueue = self.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
playqueue.clear()
for i, child in enumerate(xml):
api = API(child)
PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey())
log.debug('Firing up Kodi player')
Player().play(playqueue.kodi_pl, None, False, 0)
def get_playqueue_from_type(kodi_playlist_type):
"""
Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in
"""
with LOCK:
for playqueue in PLAYQUEUES:
if playqueue.type == kodi_playlist_type:
break
else:
raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
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
offset = time offset in Plextime (milliseconds)
"""
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()
def init_playqueue_from_plex_children(plex_id, transient_token=None):
"""
Init a new playqueue e.g. from an album. Alexa does this
Returns the Playlist_Object
"""
xml = GetAllPlexChildren(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download the PMS xml for %s', plex_id)
return
playqueue = 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):
"""
Used to poll the Kodi playqueue and update the Plex playqueue if needed
"""
old = list(playqueue.items)
index = list(range(0, len(old)))
log.debug('Comparing new Kodi playqueue %s with our play queue %s'
% (new, old))
if self.thread_stopped():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
LOG.debug('Comparing new Kodi playqueue %s with our play queue %s',
new, old)
for i, new_item in enumerate(new):
if (new_item['file'].startswith('plugin://') and
not new_item['file'].startswith(PLUGIN)):
# Ignore new media added by other addons
continue
for j, old_item in enumerate(old):
if self.stopped():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
try:
if (old_item.file.startswith('plugin://') and
not old_item['file'].startswith(PLUGIN)):
not old_item.file.startswith(PLUGIN)):
# Ignore media by other addons
continue
except (TypeError, AttributeError):
except AttributeError:
# were not passed a filename; ignore
pass
if new_item.get('id') is None:
identical = old_item.file == new_item['file']
else:
if 'id' in new_item:
identical = (old_item.kodi_id == new_item['id'] and
old_item.kodi_type == new_item['type'])
else:
try:
plex_id = REGEX.findall(new_item['file'])[0]
except IndexError:
LOG.debug('Comparing paths directly as a fallback')
identical = old_item.file == new_item['file']
else:
identical = plex_id == old_item.plex_id
if j == 0 and identical:
del old[j], index[j]
break
elif identical:
log.debug('Detected playqueue item %s moved to position %s'
% (i+j, i))
PL.move_playlist_item(playqueue, i + j, i)
LOG.debug('Detected playqueue item %s moved to position %s',
i + j, i)
with LOCK:
PL.move_playlist_item(playqueue, i + j, i)
del old[j], index[j]
break
else:
log.debug('Detected new Kodi element at position %s: %s '
% (i, new_item))
if playqueue.ID is None:
PL.init_Plex_playlist(playqueue,
kodi_item=new_item)
else:
PL.add_item_to_PMS_playlist(playqueue,
i,
kodi_item=new_item)
for j in range(i, len(index)):
index[j] += 1
LOG.debug('Detected new Kodi element at position %s: %s ',
i, new_item)
with LOCK:
try:
if playqueue.id is None:
PL.init_Plex_playlist(playqueue, kodi_item=new_item)
else:
PL.add_item_to_PMS_playlist(playqueue,
i,
kodi_item=new_item)
except PL.PlaylistError:
# Could not add the element
pass
except IndexError:
# This is really a hack - happens when using Addon Paths
# and repeatedly starting the same element. Kodi will
# then not pass kodi id nor file path AND will also not
# start-up playback. Hence kodimonitor kicks off
# playback. Also see kodimonitor.py - _playlist_onadd()
pass
else:
for j in range(i, len(index)):
index[j] += 1
for i in reversed(index):
log.debug('Detected deletion of playqueue element at pos %s' % i)
PL.delete_playlist_item_from_PMS(playqueue, i)
log.debug('Done comparing playqueues')
if self.stopped():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
LOG.debug('Detected deletion of playqueue element at pos %s', i)
with LOCK:
PL.delete_playlist_item_from_PMS(playqueue, i)
LOG.debug('Done comparing playqueues')
def run(self):
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
log.info("----===## Starting PlayQueue client ##===----")
# Initialize the playqueues, if Kodi already got items in them
for playqueue in self.playqueues:
for i, item in enumerate(PL.get_kodi_playlist_items(playqueue)):
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():
stopped = self.stopped
suspended = self.suspended
LOG.info("----===## Starting PlayqueueMonitor ##===----")
while not stopped():
while suspended():
if stopped():
break
sleep(1000)
with lock:
for playqueue in self.playqueues:
kodi_playqueue = PL.get_kodi_playlist_items(playqueue)
if playqueue.old_kodi_pl != kodi_playqueue:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
if playqueue.old_kodi_pl != kodi_pl:
if playqueue.id is None and (not state.DIRECT_PATHS or
state.CONTEXT_MENU_PLAY):
# Only initialize if directly fired up using direct
# paths. Otherwise let default.py do its magic
LOG.debug('Not yet initiating playback')
else:
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_playqueue)
playqueue.old_kodi_pl = list(kodi_playqueue)
# Still sleep a bit so Kodi does not become
# unresponsive
sleep(10)
continue
self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl)
sleep(200)
log.info("----===## PlayQueue client stopped ##===----")
LOG.info("----===## PlayqueueMonitor stopped ##===----")

View file

@ -1,69 +1,56 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from downloadutils import DownloadUtils as DU
import logging
from downloadutils import DownloadUtils
from utils import window, settings, tryEncode, language as lang, dialog
from utils import window, settings, language as lang, dialog, try_encode
import variables as v
import PlexAPI
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
class PlayUtils():
def __init__(self, item):
self.item = item
self.API = PlexAPI.API(item)
self.doUtils = DownloadUtils().downloadUrl
self.machineIdentifier = window('plex_machineIdentifier')
def getPlayUrl(self, partNumber=None):
def __init__(self, api, playqueue_item):
"""
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)
playurl is utf-8 encoded!
playurl is in unicode!
"""
self.API.setPartNumber(partNumber)
self.API.getMediastreamNumber()
self.api.mediastream_number()
playurl = self.isDirectPlay()
if playurl is not None:
log.info("File is direct playing.")
playurl = tryEncode(playurl)
# Set playmethod property
window('plex_%s.playmethod' % playurl, "DirectPlay")
LOG.info("File is direct playing.")
self.item.playmethod = 'DirectPlay'
elif self.isDirectStream():
log.info("File is direct streaming.")
playurl = tryEncode(
self.API.getTranscodeVideoPath('DirectStream'))
# Set playmethod property
window('plex_%s.playmethod' % playurl, "DirectStream")
LOG.info("File is direct streaming.")
playurl = self.api.transcode_video_path('DirectStream')
self.item.playmethod = 'DirectStream'
else:
log.info("File is transcoding.")
playurl = tryEncode(self.API.getTranscodeVideoPath(
LOG.info("File is transcoding.")
playurl = self.api.transcode_video_path(
'Transcode',
quality={
'maxVideoBitrate': self.get_bitrate(),
'videoResolution': self.get_resolution(),
'videoQuality': '100',
'mediaBufferSize': int(settings('kodi_video_cache'))/1024,
}))
# Set playmethod property
window('plex_%s.playmethod' % playurl, value="Transcode")
log.info("The playurl is: %s" % playurl)
})
self.item.playmethod = 'Transcode'
LOG.info("The playurl is: %s", playurl)
self.item.file = playurl
return playurl
def isDirectPlay(self):
@ -71,45 +58,28 @@ class PlayUtils():
Returns the path/playurl if we can direct play, None otherwise
"""
# True for e.g. plex.tv watch later
if self.API.shouldStream() is True:
log.info("Plex item optimized for direct streaming")
if self.api.should_stream() is True:
LOG.info("Plex item optimized for direct streaming")
return
# Check whether we have a strm file that we need to throw at Kodi 1:1
path = self.api.file_path()
if path is not None and path.endswith('.strm'):
LOG.info('.strm file detected')
playurl = self.api.validate_playurl(path,
self.api.plex_type(),
force_check=True)
return playurl
# set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0'
if settings('playType') != "0":
# User forcing to play via HTTP
log.info("User chose to not direct play")
LOG.info("User chose to not direct play")
return
if self.mustTranscode():
return
return self.API.validatePlayurl(self.API.getFilePath(),
self.API.getType(),
forceCheck=True)
def directPlay(self):
try:
playurl = self.item['MediaSources'][0]['Path']
except (IndexError, KeyError):
playurl = self.item['Path']
if self.item.get('VideoType'):
# Specific format modification
if self.item['VideoType'] == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif self.item['VideoType'] == "BluRay":
playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol
if playurl.startswith('\\\\'):
playurl = playurl.replace("\\\\", "smb://")
playurl = playurl.replace("\\", "/")
if "apple.com" in playurl:
USER_AGENT = "QuickTime/7.7.4"
playurl += "?|User-Agent=%s" % USER_AGENT
return playurl
return self.api.validate_playurl(path,
self.api.plex_type(),
force_check=True)
def mustTranscode(self):
"""
@ -117,46 +87,48 @@ class PlayUtils():
- codec is in h265
- 10bit video codec
- HEVC codec
- window variable 'plex_forcetranscode' set to 'true'
- playqueue_item force_transcode is set to True
- state variable FORCE_TRANSCODE set to True
(excepting trailers etc.)
- video bitrate above specified settings bitrate
if the corresponding file settings are set to 'true'
"""
if self.API.getType() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
log.info('Plex clip or music track, not transcoding')
if self.api.plex_type() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
LOG.info('Plex clip or music track, not transcoding')
return False
videoCodec = self.API.getVideoCodec()
log.info("videoCodec: %s" % videoCodec)
if window('plex_forcetranscode') == 'true':
log.info('User chose to force-transcode')
return True
if (settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10'):
log.info('Option to transcode 10bit video content enabled.')
videoCodec = self.api.video_codec()
LOG.info("videoCodec: %s" % videoCodec)
if self.item.force_transcode is True:
LOG.info('User chose to force-transcode')
return True
codec = videoCodec['videocodec']
if codec is None:
# e.g. trailers. Avoids TypeError with "'h265' in codec"
log.info('No codec from PMS, not transcoding.')
LOG.info('No codec from PMS, not transcoding.')
return False
if ((settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
LOG.info('Option to transcode 10bit h264 video content enabled.')
return True
try:
bitrate = int(videoCodec['bitrate'])
except (TypeError, ValueError):
log.info('No video bitrate from PMS, not transcoding.')
LOG.info('No video bitrate from PMS, not transcoding.')
return False
if bitrate > self.get_max_bitrate():
log.info('Video bitrate of %s is higher than the maximal video'
LOG.info('Video bitrate of %s is higher than the maximal video'
'bitrate of %s that the user chose. Transcoding'
% (bitrate, self.get_max_bitrate()))
return True
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
log.info('No video resolution from PMS, not transcoding.')
LOG.info('No video resolution from PMS, not transcoding.')
return False
if 'h265' in codec or 'hevc' in codec:
if resolution >= self.getH265():
log.info("Option to transcode h265/HEVC enabled. Resolution "
LOG.info("Option to transcode h265/HEVC enabled. Resolution "
"of the media: %s, transcoding limit resolution: %s"
% (str(resolution), str(self.getH265())))
return True
@ -164,12 +136,12 @@ class PlayUtils():
def isDirectStream(self):
# Never transcode Music
if self.API.getType() == 'track':
if self.api.plex_type() == 'track':
return True
# set to 'Transcode=2'
if settings('playType') == "2":
# User forcing to play via HTTP
log.info("User chose to transcode")
LOG.info("User chose to transcode")
return False
if self.mustTranscode():
return False
@ -251,7 +223,7 @@ class PlayUtils():
}
return res[chosen]
def audioSubsPref(self, listitem, url, part=None):
def audio_subtitle_prefs(self, listitem):
"""
For transcoding only
@ -259,15 +231,13 @@ class PlayUtils():
stream by a PUT request to the PMS
"""
# Set media and part where we're at
if self.API.mediastream is None:
self.API.getMediastreamNumber()
if part is None:
part = 0
if self.api.mediastream is None:
self.api.mediastream_number()
try:
mediastreams = self.item[self.API.mediastream][part]
mediastreams = self.api.plex_media_streams()
except (TypeError, IndexError):
log.error('Could not get media %s, part %s'
% (self.API.mediastream, part))
LOG.error('Could not get media %s, part %s',
self.api.mediastream, self.api.part)
return
part_id = mediastreams.attrib['id']
audio_streams_list = []
@ -292,19 +262,19 @@ class PlayUtils():
# Audio
if typus == "2":
codec = stream.attrib.get('codec')
channelLayout = stream.attrib.get('audioChannelLayout', "")
channellayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb+1,
stream.attrib['language'],
codec,
channelLayout)
except:
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb+1,
lang(39707), # unknown
codec,
channelLayout)
channellayout)
audio_streams_list.append(index)
audio_streams.append(tryEncode(track))
audio_streams.append(try_encode(track))
audio_numb += 1
# Subtitles
@ -326,17 +296,17 @@ class PlayUtils():
if downloadable:
# We do know the language - temporarily download
if 'language' in stream.attrib:
path = self.API.download_external_subtitles(
path = self.api.download_external_subtitles(
'{server}%s' % stream.attrib['key'],
"subtitle.%s.%s" % (stream.attrib['language'],
"subtitle.%s.%s" % (stream.attrib['languageCode'],
stream.attrib['codec']))
# We don't know the language - no need to download
else:
path = self.API.addPlexCredentialsToUrl(
path = self.api.attach_plex_token_to_url(
"%s%s" % (window('pms_server'),
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(tryEncode(path))
download_subs.append(try_encode(path))
else:
track = "%s (%s)" % (track, lang(39710)) # burn-in
if stream.attrib.get('selected') == '1' and downloadable:
@ -345,7 +315,7 @@ class PlayUtils():
default_sub = index
subtitle_streams_list.append(index)
subtitle_streams.append(tryEncode(track))
subtitle_streams.append(try_encode(track))
sub_num += 1
if audio_numb > 1:
@ -356,9 +326,9 @@ class PlayUtils():
'audioStreamID': audio_streams_list[resp],
'allParts': 1
}
self.doUtils('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
if sub_num == 1:
# No subtitles
@ -367,7 +337,7 @@ class PlayUtils():
select_subs_index = None
if (settings('pickPlexSubtitles') == 'true' and
default_sub is not None):
log.info('Using default Plex subtitle: %s' % default_sub)
LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub
else:
resp = dialog('select', lang(33014), subtitle_streams)
@ -377,26 +347,18 @@ class PlayUtils():
# User selected no subtitles or backed out of dialog
select_subs_index = ''
log.debug('Adding external subtitles: %s' % download_subs)
LOG.debug('Adding external subtitles: %s', download_subs)
# Enable Kodi to switch autonomously to downloadable subtitles
if download_subs:
listitem.setSubtitles(download_subs)
# Don't additionally burn in subtitles
if select_subs_index in downloadable_streams:
for i, stream in enumerate(downloadable_streams):
if stream == select_subs_index:
# Set the correct subtitle
window('plex_%s.subtitle' % tryEncode(url), value=str(i))
break
# Don't additionally burn in subtitles
select_subs_index = ''
else:
window('plex_%s.subtitle' % tryEncode(url), value='None')
# Now prep the PMS for our choice
args = {
'subtitleStreamID': select_subs_index,
'allParts': 1
}
self.doUtils('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)

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 traceback
import string
@ -7,7 +7,7 @@ from socket import error as socket_error
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@ -17,20 +17,20 @@ class RequestMgr:
self.conns = {}
def getConnection(self, protocol, host, port):
conn = self.conns.get(protocol+host+str(port), False)
conn = self.conns.get(protocol + host + str(port), False)
if not conn:
if protocol == "https":
conn = httplib.HTTPSConnection(host, port)
else:
conn = httplib.HTTPConnection(host, port)
self.conns[protocol+host+str(port)] = conn
self.conns[protocol + host + str(port)] = conn
return conn
def closeConnection(self, protocol, host, port):
conn = self.conns.get(protocol+host+str(port), False)
conn = self.conns.get(protocol + host + str(port), False)
if conn:
conn.close()
self.conns.pop(protocol+host+str(port), None)
self.conns.pop(protocol + host + str(port), None)
def dumpConnections(self):
for conn in self.conns.values():
@ -45,7 +45,7 @@ class RequestMgr:
conn.request("POST", path, body, header)
data = conn.getresponse()
if int(data.status) >= 400:
log.error("HTTP response error: %s" % str(data.status))
LOG.error("HTTP response error: %s" % str(data.status))
# this should return false, but I'm hacking it since iOS
# returns 404 no matter what
return data.read() or True
@ -56,14 +56,14 @@ class RequestMgr:
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
pass
else:
log.error("Unable to connect to %s\nReason:" % host)
log.error(traceback.print_exc())
self.conns.pop(protocol+host+str(port), None)
LOG.error("Unable to connect to %s\nReason:" % host)
LOG.error(traceback.print_exc())
self.conns.pop(protocol + host + str(port), None)
if conn:
conn.close()
return False
except Exception as e:
log.error("Exception encountered: %s" % e)
LOG.error("Exception encountered: %s", e)
# Close connection just in case
try:
conn.close()
@ -76,7 +76,7 @@ class RequestMgr:
newpath = path + '?'
pairs = []
for key in params:
pairs.append(str(key)+'='+str(params[key]))
pairs.append(str(key) + '=' + str(params[key]))
newpath += string.join(pairs, '&')
return self.get(host, port, newpath, header, protocol)
@ -87,7 +87,7 @@ class RequestMgr:
conn.request("GET", path, headers=header)
data = conn.getresponse()
if int(data.status) >= 400:
log.error("HTTP response error: %s" % str(data.status))
LOG.error("HTTP response error: %s", str(data.status))
return False
else:
return data.read() or True
@ -96,8 +96,8 @@ class RequestMgr:
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
pass
else:
log.error("Unable to connect to %s\nReason:" % host)
log.error(traceback.print_exc())
self.conns.pop(protocol+host+str(port), None)
LOG.error("Unable to connect to %s\nReason:", host)
LOG.error(traceback.print_exc())
self.conns.pop(protocol + host + str(port), None)
conn.close()
return False

View file

@ -1,53 +1,68 @@
# -*- coding: utf-8 -*-
import logging
"""
Plex Companion listener
"""
from logging import getLogger
from re import sub
from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
from xbmc import sleep
from xbmc import sleep, Player, Monitor
from companion import process_command
from utils import window
from functions import *
import json_rpc as js
from clientinfo import getXArgsDeviceInfo
import variables as v
###############################################################################
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):
"""
BaseHTTPRequestHandler implementation of Plex Companion listener
"""
protocol_version = 'HTTP/1.1'
def __init__(self, *args, **kwargs):
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
self.serverlist = []
def getServerByHost(self, host):
if len(self.serverlist) == 1:
return self.serverlist[0]
for server in self.serverlist:
if (server.get('serverName') in host or
server.get('server') in host):
return server
return {}
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def do_HEAD(self):
log.debug("Serving HEAD request...")
LOG.debug("Serving HEAD request...")
self.answer_request(0)
def do_GET(self):
log.debug("Serving GET request...")
LOG.debug("Serving GET request...")
self.answer_request(1)
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Content-Length', '0')
self.send_header('X-Plex-Client-Identifier',
self.server.settings['uuid'])
self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER)
self.send_header('Content-Type', 'text/plain')
self.send_header('Connection', 'close')
self.send_header('Access-Control-Max-Age', '1209600')
@ -66,7 +81,8 @@ class MyHandler(BaseHTTPRequestHandler):
def sendOK(self):
self.send_response(200)
def response(self, body, headers={}, code=200):
def response(self, body, headers=None, code=200):
headers = {} if headers is None else headers
try:
self.send_response(code)
for key in headers:
@ -79,112 +95,135 @@ class MyHandler(BaseHTTPRequestHandler):
except:
pass
def answer_request(self, sendData):
def answer_request(self, send_data):
self.serverlist = self.server.client.getServerList()
subMgr = self.server.subscriptionManager
js = self.server.jsonClass
settings = self.server.settings
sub_mgr = self.server.subscription_manager
try:
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
log.debug("remote request_path: %s" % request_path)
log.debug("params received from remote: %s" % params)
subMgr.updateCommandID(self.headers.get(
'X-Plex-Client-Identifier',
self.client_address[0]),
params.get('commandID', False))
if request_path == "version":
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
LOG.debug("remote request_path: %s", request_path)
LOG.debug("params received from remote: %s", params)
sub_mgr.update_command_id(self.headers.get(
'X-Plex-Client-Identifier', self.client_address[0]),
params.get('commandID'))
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% v.ADDON_VERSION)
elif request_path == "verify":
self.response("XBMC JSON connection test:\n" + js.ping())
elif request_path == 'resources':
self.response(
RESOURCES_XML.format(
title=v.DEVICENAME,
machineIdentifier=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(
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% 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())),
msg,
{
'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':
'X-Plex-Client-Identifier',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/xml'
'Content-Type': 'text/xml;charset=utf-8'
})
elif not sub_mgr.stop_sent_to_web:
sub_mgr.stop_sent_to_web = True
LOG.debug('Signaling STOP to Plex Web')
self.response(
msg,
{
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0',
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '1209600',
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
})
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:
# Throw it to companion.py
process_command(request_path, params, self.server.queue)
self.response('', js.getPlexHeaders())
subMgr.notify()
except:
log.error('Error encountered. Traceback:')
import traceback
log.error(traceback.print_exc())
# Fail connection with HTTP 500 error - has been open too long
self.response(
'Need to close this connection on the PKC side',
{
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0',
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '1209600',
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
},
code=500)
elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False))
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):
"""
Using ThreadingMixIn Thread magic
"""
daemon_threads = True
def __init__(self, client, subscriptionManager, jsonClass, settings,
queue, *args, **kwargs):
def __init__(self, client, subscription_manager, *args, **kwargs):
"""
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
date serverlist without instantiating anything
same for SubscriptionManager and jsonClass
same for SubscriptionMgr
"""
self.client = client
self.subscriptionManager = subscriptionManager
self.jsonClass = jsonClass
self.settings = settings
self.queue = queue
self.subscription_manager = subscription_manager
HTTPServer.__init__(self, *args, **kwargs)

View file

@ -30,6 +30,7 @@ from xbmc import sleep
import downloadutils
from utils import window, settings, dialog, language
import variables as v
###############################################################################
@ -44,7 +45,6 @@ class plexgdm:
self.discover_message = 'M-SEARCH * HTTP/1.0'
self.client_header = '* HTTP/1.0'
self.client_data = None
self.client_id = None
self._multicast_address = '239.0.0.250'
self.discover_group = (self._multicast_address, 32414)
@ -60,7 +60,7 @@ class plexgdm:
self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl
def clientDetails(self, options):
def clientDetails(self):
self.client_data = (
"Content-Type: plex/media-player\n"
"Resource-Identifier: %s\n"
@ -74,13 +74,12 @@ class plexgdm:
"playqueues\n"
"Device-Class: HTPC\n"
) % (
options['uuid'],
options['client_name'],
options['myport'],
options['addonName'],
options['version']
v.PKC_MACHINE_IDENTIFIER,
v.DEVICENAME,
v.COMPANION_PORT,
v.ADDON_NAME,
v.ADDON_VERSION
)
self.client_id = options['uuid']
def getClientDetails(self):
return self.client_data
@ -211,7 +210,7 @@ class plexgdm:
registered = False
for client in xml:
if (client.attrib.get('machineIdentifier') ==
self.client_id):
v.PKC_MACHINE_IDENTIFIER):
registered = True
if registered:
return True

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
import threading
"""
Manages getting playstate from Kodi and sending it to the PMS as well as
subscribed Plex Companion clients.
"""
from logging import getLogger
from threading import Thread, RLock
import downloadutils
from clientinfo import getXArgsDeviceInfo
from utils import window
import PlexFunctions as pf
from downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis, LockFunction
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:
def __init__(self, jsonClass, RequestMgr, player, mgr):
STREAM_DETAILS = {
'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.subscribers = {}
self.info = {}
self.lastkey = ""
self.containerKey = ""
self.ratingkey = ""
self.lastplayers = {}
self.lastinfo = {
'video': {},
'audio': {},
'picture': {}
}
self.volume = 0
self.mute = '0'
self.server = ""
self.protocol = "http"
self.port = ""
self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.isplaying = False
# 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.playqueue = mgr.playqueue
self.request_mgr = request_mgr
self.js = jsonClass
self.RequestMgr = RequestMgr
def getServerByHost(self, host):
def _server_by_host(self, host):
if len(self.serverlist) == 1:
return self.serverlist[0]
for server in self.serverlist:
@ -52,281 +138,347 @@ class SubscriptionManager:
return server
return {}
def getVolume(self):
self.volume, self.mute = self.js.getVolume()
@LOCKER.lockthis
def msg(self, players):
msg = getXMLHeader()
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
msg += ' machineIdentifier="%s" location="fullScreenVideo">' % window('plex_client_Id')
msg += self.getTimelineXML(self.js.getAudioPlayerId(players), plex_audio())
msg += self.getTimelineXML(self.js.getPhotoPlayerId(players), plex_photo())
msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video())
msg += "\n</MediaContainer>"
return msg
"""
Returns a timeline xml as str
(xml containing video, audio, photo player state)
"""
self.isplaying = False
answ = str(XML)
timelines = {
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):
if playerid is not None:
info = self.getPlayerProperties(playerid)
# save this info off so the server update can use it too
self.playerprops[playerid] = info;
status = info['state']
time = info['time']
else:
status = "stopped"
time = 0
ret = "\n"+' <Timeline state="%s" time="%s" type="%s"' % (status, time, ptype)
if playerid is None:
ret += ' />'
return ret
@staticmethod
def _dict_to_xml(dictionary):
"""
Returns the string 'key1="value1" key2="value2" ...' for dictionary
"""
answ = ''
for key, value in dictionary.iteritems():
answ += '%s="%s" ' % (key, value)
return answ
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')
if pbmc_server:
(self.protocol, self.server, self.port) = \
pbmc_server.split(':')
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
keyid = None
count = 0
while not keyid:
if count > 300:
break
keyid = window('plex_currently_playing_itemid')
xbmc.sleep(100)
count += 1
if keyid:
self.lastkey = "/library/metadata/%s" % keyid
self.ratingkey = keyid
ret += ' key="%s"' % self.lastkey
ret += ' ratingKey="%s"' % self.ratingkey
serv = self.getServerByHost(self.server)
if info.get('playQueueID'):
self.containerKey = "/playQueues/%s" % info.get('playQueueID')
ret += ' playQueueID="%s"' % info.get('playQueueID')
ret += ' playQueueVersion="%s"' % info.get('playQueueVersion')
ret += ' playQueueItemID="%s"' % info.get('playQueueItemID')
ret += ' containerKey="%s"' % self.containerKey
ret += ' guid="%s"' % info['guid']
elif keyid:
self.containerKey = self.lastkey
ret += ' containerKey="%s"' % self.containerKey
ret += ' duration="%s"' % info['duration']
ret += ' controllable="%s"' % self.controllable()
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
ret += ' protocol="%s"' % serv.get('protocol', "http")
ret += ' address="%s"' % serv.get('server', self.server)
ret += ' port="%s"' % serv.get('port', self.port)
ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle']
ret += ' mute="%s"' % self.mute
ret += ' repeat="%s"' % info['repeat']
ret += ' itemType="%s"' % info['itemType']
status = 'paused' if int(info['speed']) == 0 else 'playing'
duration = kodi_time_to_millis(info['totaltime'])
shuffle = '1' if info['shuffled'] else '0'
mute = '1' if info['muted'] is True else '0'
answ = {
'location': 'fullScreenVideo',
'controllable': CONTROLLABLE[ptype],
'protocol': self.protocol,
'address': self.server,
'port': self.port,
'machineIdentifier': window('plex_machineIdentifier'),
'state': status,
'type': ptype,
'itemType': ptype,
'time': kodi_time_to_millis(info['time']),
'duration': duration,
'seekRange': '0-%s' % duration,
'shuffle': shuffle,
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
'volume': info['volume'],
'mute': mute,
'mediaIndex': 0, # Still to implement from here
'partIndex':0,
'partCount': 1,
'providerIdentifier': 'com.plexapp.plugins.library',
}
# Get the plex id from the PKC playqueue not info, as Kodi jumps to next
# playqueue element way BEFORE kodi monitor onplayback is called
if item.plex_id:
answ['key'] = '/library/metadata/%s' % item.plex_id
answ['ratingKey'] = item.plex_id
# PlayQueue stuff
if info['container_key']:
answ['containerKey'] = info['container_key']
if (info['container_key'] is not None and
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:
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
elif info['plex_transient_token']:
ret += ' token="%s"' % info['plex_transient_token']
# Might need an update in the future
if ptype == 'video':
ret += ' subtitleStreamID="-1"'
ret += ' audioStreamID="-1"'
answ['token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id:
answ['audioStreamID'] = strm_id
else:
LOG.error('We could not select a Plex audiostream')
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 += '/>'
return ret
def signal_stop(self):
"""
Externally called on PKC shutdown to ensure that PKC signals a stop to
the PMS. Otherwise, PKC might be stuck at "currently playing"
"""
LOG.info('Signaling a complete stop to PMS')
# 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):
if commandID and self.subscribers.get(uuid, False):
self.subscribers[uuid].commandID = int(commandID)
def _plex_stream_index(self, playerid, stream_type):
"""
Returns the current Plex stream index [str] for the player playerid
def notify(self, event=False):
self.cleanup()
# Don't tell anyone if we don't know a Plex ID and are still playing
# (e.g. no stop called). Used for e.g. PVR/TV without PKC usage
if (not window('plex_currently_playing_itemid')
and not self.lastplayers):
return True
players = self.js.getPlayers()
# fetch the message, subscribers or not, since the server
# will need the info anyway
msg = self.msg(players)
if self.subscribers:
with threading.RLock():
for sub in self.subscribers.values():
sub.send_update(msg, len(players) == 0)
self.notifyServer(players)
self.lastplayers = players
stream_type: 'video', 'audio', 'subtitle'
"""
playqueue = PQ.PLAYQUEUES[playerid]
info = state.PLAYER_STATES[playerid]
return playqueue.items[info['position']].plex_stream_index(
info[STREAM_DETAILS[stream_type]]['index'], stream_type)
@LOCKER.lockthis
def update_command_id(self, uuid, command_id):
"""
Updates the Plex Companien client with the machine identifier uuid with
command_id
"""
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
def _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
def notifyServer(self, players):
for typus, p in players.iteritems():
info = self.playerprops[p.get('playerid')]
self._sendNotification(info, int(p['playerid']))
self.lastinfo[typus] = info
# Cross the one of the list
@LOCKER.lockthis
def notify(self):
"""
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
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:
del self.lastplayers[typus]
except KeyError:
pass
# Process the players we have left (to signal a stop)
for typus, p in self.lastplayers.iteritems():
self.lastinfo[typus]['state'] = 'stopped'
self._sendNotification(self.lastinfo[typus], int(p['playerid']))
for player in self.lastplayers.values():
self.last_params['state'] = 'stopped'
self._send_pms_notification(player['playerid'], self.last_params)
def _sendNotification(self, info, playerid):
playqueue = self.playqueue.playqueues[playerid]
xargs = getXArgsDeviceInfo()
def _get_pms_params(self, playerid):
info = state.PLAYER_STATES[playerid]
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 = {
'containerKey': self.containerKey or "/library/metadata/900000",
'key': self.lastkey or "/library/metadata/900000",
'ratingKey': self.ratingkey or "900000",
'state': info['state'],
'time': info['time'],
'duration': info['duration']
'state': status,
'ratingKey': item.plex_id,
'key': '/library/metadata/%s' % item.plex_id,
'time': kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime'])
}
if info['container_key'] is not None:
# params['containerKey'] = info['container_key']
if info['container_key'].startswith('/playQueues/'):
# 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:
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
xargs['X-Plex-Token'] = playqueue.plex_transient_token
if info.get('playQueueID'):
params['containerKey'] = '/playQueues/%s' % info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion']
params['playQueueItemID'] = info['playQueueItemID']
serv = self.getServerByHost(self.server)
elif state.PMS_TOKEN:
xargs['X-Plex-Token'] = state.PMS_TOKEN
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'),
serv.get('port', '32400'))
self.doUtils(url, parameters=params, headerOptions=xargs)
log.debug("Sent server notification with parameters: %s to %s"
% (params, url))
DU().downloadUrl(url,
authenticate=False,
parameters=xargs,
headerOverride=HEADERS_PMS)
LOG.debug("Sent server notification with parameters: %s to %s",
xargs, url)
def controllable(self):
return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause"
@LOCKER.lockthis
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):
sub = Subscriber(protocol,
host,
port,
uuid,
commandID,
self,
self.RequestMgr)
with threading.RLock():
self.subscribers[sub.uuid] = sub
return sub
@LOCKER.lockthis
def remove_subscriber(self, uuid):
"""
Removes a connected Plex Companion subscriber with machine identifier
uuid from PKC notifications.
(Calls the cleanup() method of the subscriber)
"""
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
def removeSubscriber(self, uuid):
with threading.RLock():
for sub in self.subscribers.values():
if sub.uuid == uuid or sub.host == uuid:
sub.cleanup()
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
def _cleanup(self):
for subscriber in self.subscribers.values():
if subscriber.age > 30:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
class Subscriber:
def __init__(self, protocol, host, port, uuid, commandID,
subMgr, RequestMgr):
class Subscriber(object):
"""
Plex Companion subscribing device
"""
def __init__(self, protocol, host, port, uuid, command_id, sub_mgr,
request_mgr):
self.protocol = protocol or "http"
self.host = host
self.port = port or 32400
self.uuid = uuid or host
self.commandID = int(commandID) or 0
self.navlocationsent = False
self.command_id = int(command_id) or 0
self.age = 0
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.subMgr = subMgr
self.RequestMgr = RequestMgr
self.sub_mgr = sub_mgr
self.request_mgr = request_mgr
def __eq__(self, other):
return self.uuid == other.uuid
def tostr(self):
return "uuid=%s,commandID=%i" % (self.uuid, self.commandID)
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 :-(
"""
response = self.doUtils(url,
postBody=msg,
action_type="POST")
if response in [False, None, 401]:
self.subMgr.removeSubscriber(self.uuid)
response = DU().downloadUrl(url,
action_type="POST",
postBody=msg,
authenticate=False,
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 -*-
###############################################################################
from logging import getLogger
from utils import kodiSQL
import logging
from utils import kodi_sql
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
"""
def __enter__(self):
self.plexconn = kodiSQL('plex')
self.plexconn = kodi_sql('plex')
return Plex_DB_Functions(self.plexconn.cursor())
def __exit__(self, type, value, traceback):
@ -220,17 +220,13 @@ class Plex_DB_Functions():
None if not found
"""
query = '''
SELECT kodi_id, kodi_fileid, kodi_pathid,
parent_id, kodi_type, plex_type
FROM plex
WHERE plex_id = ?
SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type,
plex_type
FROM plex WHERE plex_id = ?
LIMIT 1
'''
try:
self.plexcursor.execute(query, (plex_id,))
item = self.plexcursor.fetchone()
return item
except:
return None
self.plexcursor.execute(query, (plex_id,))
return self.plexcursor.fetchone()
def getItem_byWildId(self, plex_id):
"""
@ -272,14 +268,13 @@ class Plex_DB_Functions():
def getItem_byParentId(self, parent_id, kodi_type):
"""
Returns the tuple (plex_id, kodi_id, kodi_fileid) for parent_id,
Returns a list of tuples (plex_id, kodi_id, kodi_fileid) for parent_id,
kodi_type
"""
query = '''
SELECT plex_id, kodi_id, kodi_fileid
FROM plex
WHERE parent_id = ?
AND kodi_type = ?"
WHERE parent_id = ? AND kodi_type = ?
'''
self.plexcursor.execute(query, (parent_id, kodi_type,))
return self.plexcursor.fetchall()
@ -297,7 +292,7 @@ class Plex_DB_Functions():
self.plexcursor.execute(query, (parent_id, kodi_type,))
return self.plexcursor.fetchall()
def getChecksum(self, plex_type):
def checksum(self, plex_type):
"""
Returns a list of tuples (plex_id, checksum) for plex_type
"""
@ -383,8 +378,8 @@ class Plex_DB_Functions():
"""
Removes the one entry with plex_id
"""
query = "DELETE FROM plex WHERE plex_id = ?"
self.plexcursor.execute(query, (plex_id,))
self.plexcursor.execute('DELETE FROM plex WHERE plex_id = ?',
(plex_id,))
def removeWildItem(self, plex_id):
"""

View file

@ -28,6 +28,10 @@ DIRECT_PATHS = False
INDICATE_MEDIA_VERSIONS = False
# Do we need to run a special library scan?
RUN_LIB_SCAN = None
# Number of items to fetch and display in widgets
FETCH_PMS_ITEM_NUMBER = None
# Hack to force Kodi widget for "in progress" to show up if it was empty before
FORCE_RELOAD_SKIN = True
# Stemming from the PKC settings.xml
# Shall we show Kodi dialogs when synching?
@ -38,8 +42,8 @@ KODI_DB_CHECKED = False
ENABLE_MUSIC = True
# How often shall we sync?
FULL_SYNC_INTERVALL = 0
# Background Sync enabled at all?
BACKGROUND_SYNC = True
# Background Sync disabled?
BACKGROUND_SYNC_DISABLED = False
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
BACKGROUNDSYNC_SAFTYMARGIN = 0
@ -63,14 +67,93 @@ remapSMBmusicNew = None
remapSMBphotoOrg = None
remapSMBphotoNew = None
# Shall we verify SSL certificates?
VERIFY_SSL_CERT = False
# Do we have an ssl certificate for PKC we need to use?
SSL_CERT_PATH = None
# Along with window('plex_authenticated')
AUTHENTICATED = False
# plex.tv username
PLEX_USERNAME = None
# Token for that user for plex.tv
PLEX_TOKEN = None
# Plex token for the active PMS for the active user
# (might be diffent to PLEX_TOKEN)
PMS_TOKEN = None
# Plex ID of that user (e.g. for plex.tv) as a STRING
PLEX_USER_ID = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
PLEX_TRANSIENT_TOKEN = None
# Plex Companion Queue()
COMPANION_QUEUE = None
# Command Pipeline Queue()
COMMAND_PIPELINE_QUEUE = None
# Websocket_client queue to communicate with librarysync
WEBSOCKET_QUEUE = None
# 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 -*-
###############################################################################
import logging
import threading
from logging import getLogger
from threading import Thread
import xbmc
import xbmcgui
from xbmc import sleep, executebuiltin, translatePath
import xbmcaddon
from xbmcvfs import exists
from utils import window, settings, language as lang, thread_methods
import downloadutils
import PlexAPI
from PlexFunctions import GetMachineIdentifier
from utils import window, settings, language as lang, thread_methods, dialog
from downloadutils import DownloadUtils as DU
import plex_tv
import PlexFunctions as PF
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
class UserClient(threading.Thread):
class UserClient(Thread):
# Borg - multiple instances, shared state
__shared_state = {}
def __init__(self, callback=None):
def __init__(self):
self.__dict__ = self.__shared_state
if callback is not None:
self.mgr = callback
self.auth = True
self.retry = 0
@ -47,9 +41,9 @@ class UserClient(threading.Thread):
self.userSettings = None
self.addon = xbmcaddon.Addon()
self.doUtils = downloadutils.DownloadUtils()
self.doUtils = DU()
threading.Thread.__init__(self)
Thread.__init__(self)
def getUsername(self):
"""
@ -57,10 +51,10 @@ class UserClient(threading.Thread):
"""
username = settings('username')
if not username:
log.debug("No username saved, trying to get Plex username")
LOG.debug("No username saved, trying to get Plex username")
username = settings('plexLogin')
if not username:
log.debug("Also no Plex username found")
LOG.debug("Also no Plex username found")
return ""
return username
@ -75,7 +69,7 @@ class UserClient(threading.Thread):
server = host + ":" + port
if not host:
log.debug("No server information saved.")
LOG.debug("No server information saved.")
return False
# If https is true
@ -86,11 +80,11 @@ class UserClient(threading.Thread):
server = "http://%s" % server
# User entered IP; we need to get the machineIdentifier
if self.machineIdentifier == '' and prefix is True:
self.machineIdentifier = GetMachineIdentifier(server)
self.machineIdentifier = PF.GetMachineIdentifier(server)
if self.machineIdentifier is None:
self.machineIdentifier = ''
settings('plex_machineIdentifier', value=self.machineIdentifier)
log.debug('Returning active server: %s' % server)
LOG.debug('Returning active server: %s', server)
return server
def getSSLverify(self):
@ -103,10 +97,10 @@ class UserClient(threading.Thread):
else settings('sslcert')
def setUserPref(self):
log.debug('Setting user preferences')
LOG.debug('Setting user preferences')
# Only try to get user avatar if there is a token
if self.currToken:
url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser)
url = PF.GetUserArtworkURL(self.currUser)
if url:
window('PlexUserImage', value=url)
# Set resume point max
@ -118,7 +112,7 @@ class UserClient(threading.Thread):
return True
def loadCurrUser(self, username, userId, usertoken, authenticated=False):
log.debug('Loading current user')
LOG.debug('Loading current user')
doUtils = self.doUtils
self.currToken = usertoken
@ -129,25 +123,26 @@ class UserClient(threading.Thread):
if authenticated is False:
if self.currServer is None:
return False
log.debug('Testing validity of current token')
res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
token=self.currToken,
verifySSL=self.ssl)
LOG.debug('Testing validity of current token')
res = PF.check_connection(self.currServer,
token=self.currToken,
verifySSL=self.ssl)
if res is False:
# PMS probably offline
return False
elif res == 401:
log.error('Token is no longer valid')
LOG.error('Token is no longer valid')
return 401
elif res >= 400:
log.error('Answer from PMS is not as expected. Retrying')
LOG.error('Answer from PMS is not as expected. Retrying')
return False
# Set to windows property
state.PLEX_USER_ID = userId or None
state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '')
window('pms_token', value=self.currToken)
window('pms_token', value=usertoken)
state.PMS_TOKEN = usertoken
# This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv
window('plex_token', value=settings('plexToken'))
@ -184,31 +179,29 @@ class UserClient(threading.Thread):
return True
def authenticate(self):
log.debug('Authenticating user')
dialog = xbmcgui.Dialog()
LOG.debug('Authenticating user')
# Give attempts at entering password / selecting user
if self.retry >= 2:
log.error("Too many retries to login.")
LOG.error("Too many retries to login.")
state.PMS_STATUS = 'Stop'
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
dialog('ok', lang(33001), lang(39023))
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
return False
# Get /profile/addon_data
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile'))
addondir = translatePath(self.addon.getAddonInfo('profile'))
# If there's no settings.xml
if not exists("%ssettings.xml" % addondir):
log.error("Error, no settings.xml found.")
LOG.error("Error, no settings.xml found.")
self.auth = False
return False
server = self.getServer()
# If there is no server we can connect to
if not server:
log.info("Missing server information.")
LOG.info("Missing server information.")
self.auth = False
return False
@ -219,7 +212,7 @@ class UserClient(threading.Thread):
enforceLogin = settings('enforceUserLogin')
# Found a user in the settings, try to authenticate
if username and enforceLogin == 'false':
log.debug('Trying to authenticate with old settings')
LOG.debug('Trying to authenticate with old settings')
answ = self.loadCurrUser(username,
userId,
usertoken,
@ -228,21 +221,19 @@ class UserClient(threading.Thread):
# SUCCESS: loaded a user from the settings
return True
elif answ == 401:
log.error("User token no longer valid. Sign user out")
LOG.error("User token no longer valid. Sign user out")
settings('username', value='')
settings('userid', value='')
settings('accessToken', value='')
else:
log.debug("Could not yet authenticate user")
LOG.debug("Could not yet authenticate user")
return False
plx = PlexAPI.PlexAPI()
# Could not use settings - try to get Plex user list from plex.tv
plextoken = settings('plexToken')
if plextoken:
log.info("Trying to connect to plex.tv to get a user list")
userInfo = plx.ChoosePlexHomeUser(plextoken)
LOG.info("Trying to connect to plex.tv to get a user list")
userInfo = plex_tv.choose_home_user(plextoken)
if userInfo is False:
# FAILURE: Something went wrong, try again
self.auth = True
@ -252,7 +243,7 @@ class UserClient(threading.Thread):
userId = userInfo['userid']
usertoken = userInfo['token']
else:
log.info("Trying to authenticate without a token")
LOG.info("Trying to authenticate without a token")
username = ''
userId = ''
usertoken = ''
@ -260,20 +251,21 @@ class UserClient(threading.Thread):
if self.loadCurrUser(username, userId, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings
return True
else:
# FAILUR: Something went wrong, try again
self.auth = True
self.retry += 1
return False
# Something went wrong, try again
self.auth = True
self.retry += 1
return False
def resetClient(self):
log.debug("Reset UserClient authentication.")
LOG.debug("Reset UserClient authentication.")
self.doUtils.stopSession()
window('plex_authenticated', clear=True)
state.AUTHENTICATED = False
window('pms_token', clear=True)
state.PLEX_TOKEN = None
state.PLEX_TRANSIENT_TOKEN = None
state.PMS_TOKEN = None
window('plex_token', clear=True)
window('pms_server', clear=True)
window('plex_machineIdentifier', clear=True)
@ -287,9 +279,6 @@ class UserClient(threading.Thread):
settings('userid', value='')
settings('accessToken', value='')
# Reset token in downloads
self.doUtils.setToken('')
self.currToken = None
self.auth = True
self.currUser = None
@ -297,17 +286,17 @@ class UserClient(threading.Thread):
self.retry = 0
def run(self):
log.info("----===## Starting UserClient ##===----")
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
while not thread_stopped():
while thread_suspended():
if thread_stopped():
LOG.info("----===## Starting UserClient ##===----")
stopped = self.stopped
suspended = self.suspended
while not stopped():
while suspended():
if stopped():
break
xbmc.sleep(1000)
sleep(1000)
if state.PMS_STATUS == "Stop":
xbmc.sleep(500)
sleep(500)
continue
# Verify the connection status to server
@ -320,7 +309,7 @@ class UserClient(threading.Thread):
state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth')
self.resetClient()
xbmc.sleep(3000)
sleep(3000)
if self.auth and (self.currUser is None):
# Try to authenticate user
@ -330,9 +319,9 @@ class UserClient(threading.Thread):
self.auth = False
if self.authenticate():
# Successfully authenticated and loaded a user
log.info("Successfully authenticated!")
log.info("Current user: %s" % self.currUser)
log.info("Current userId: %s" % state.PLEX_USER_ID)
LOG.info("Successfully authenticated!")
LOG.info("Current user: %s", self.currUser)
LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True)
@ -346,10 +335,10 @@ class UserClient(threading.Thread):
# Or retried too many times
if server and state.PMS_STATUS != "Stop":
# Only if there's information found to login
log.debug("Server found: %s" % server)
LOG.debug("Server found: %s", server)
self.auth = True
# Minimize CPU load
xbmc.sleep(100)
sleep(100)
log.info("##===---- UserClient Stopped ----===##")
LOG.info("##===---- UserClient Stopped ----===##")

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!
def tryDecode(string, encoding='utf-8'):
def try_decode(string, encoding='utf-8'):
"""
Will try to decode string (encoded) using encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
@ -22,15 +22,23 @@ def tryDecode(string, encoding='utf-8'):
return string
# Percent of playback progress for watching item as partially watched. Anything
# more and item will NOT be marked as partially, but fully watched
MARK_PLAYED_AT = 0.9
# How many seconds of playback do we ignore before marking an item as partially
# watched?
IGNORE_SECONDS_AT_START = 60
_ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect'
ADDON_ID = 'plugin.video.plexkodiconnect'
ADDON_VERSION = _ADDON.getAddonInfo('version')
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
KODI_PROFILE = try_decode(xbmc.translatePath("special://profile"))
if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX"
@ -49,7 +57,7 @@ elif xbmc.getCondVisibility('system.platform.android'):
else:
PLATFORM = "Unknown"
DEVICENAME = tryDecode(_ADDON.getSetting('deviceName'))
DEVICENAME = try_decode(_ADDON.getSetting('deviceName'))
DEVICENAME = DEVICENAME.replace(":", "")
DEVICENAME = DEVICENAME.replace("/", "-")
DEVICENAME = DEVICENAME.replace("\\", "-")
@ -60,7 +68,15 @@ 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
_DB_VIDEO_VERSION = {
@ -71,7 +87,7 @@ _DB_VIDEO_VERSION = {
17: 107, # Krypton
18: 108 # Leia
}
DB_VIDEO_PATH = tryDecode(xbmc.translatePath(
DB_VIDEO_PATH = try_decode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = {
@ -82,7 +98,7 @@ _DB_MUSIC_VERSION = {
17: 60, # Krypton
18: 62 # Leia
}
DB_MUSIC_PATH = tryDecode(xbmc.translatePath(
DB_MUSIC_PATH = try_decode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = {
@ -93,12 +109,12 @@ _DB_TEXTURE_VERSION = {
17: 13, # Krypton
18: 13 # Leia
}
DB_TEXTURE_PATH = tryDecode(xbmc.translatePath(
DB_TEXTURE_PATH = try_decode(xbmc.translatePath(
"special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]))
DB_PLEX_PATH = tryDecode(xbmc.translatePath("special://database/plex.db"))
DB_PLEX_PATH = try_decode(xbmc.translatePath("special://database/plex.db"))
EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath(
EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
"special://profile/addon_data/%s/temp/" % ADDON_ID))
@ -123,6 +139,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo'
PLEX_TYPE_PHOTO = 'photo'
# Used for /:/timeline XML messages
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
PLEX_PLAYLIST_TYPE_PHOTO = 'photo'
KODI_PLAYLIST_TYPE_VIDEO = 'video'
KODI_PLAYLIST_TYPE_AUDIO = 'audio'
KODI_PLAYLIST_TYPE_PHOTO = 'picture'
KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = {
PLEX_PLAYLIST_TYPE_VIDEO: KODI_PLAYLIST_TYPE_VIDEO,
PLEX_PLAYLIST_TYPE_AUDIO: KODI_PLAYLIST_TYPE_AUDIO,
PLEX_PLAYLIST_TYPE_PHOTO: KODI_PLAYLIST_TYPE_PHOTO
}
# All the Kodi types as e.g. used in the JSON API
KODI_TYPE_VIDEO = 'video'
@ -142,9 +172,6 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
KODI_TYPE_PHOTO = 'photo'
# Translation tables
KODI_VIDEOTYPES = (
KODI_TYPE_VIDEO,
KODI_TYPE_MOVIE,
@ -154,12 +181,29 @@ KODI_VIDEOTYPES = (
KODI_TYPE_SET
)
PLEX_VIDEOTYPES = (
PLEX_TYPE_MOVIE,
PLEX_TYPE_CLIP,
PLEX_TYPE_EPISODE,
PLEX_TYPE_SEASON,
PLEX_TYPE_SHOW
)
KODI_AUDIOTYPES = (
KODI_TYPE_SONG,
KODI_TYPE_ALBUM,
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 = {
PLEX_TYPE_MOVIE: 'Movies',
PLEX_TYPE_SEASON: 'TVShows',
@ -193,6 +237,20 @@ KODITYPE_FROM_PLEXTYPE = {
'XXXXXXX': 'genre'
}
PLEX_TYPE_FROM_KODI_TYPE = {
KODI_TYPE_VIDEO: PLEX_TYPE_VIDEO,
KODI_TYPE_MOVIE: PLEX_TYPE_MOVIE,
KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE,
KODI_TYPE_SEASON: PLEX_TYPE_SEASON,
KODI_TYPE_SHOW: PLEX_TYPE_SHOW,
KODI_TYPE_CLIP: PLEX_TYPE_CLIP,
KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST,
KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM,
KODI_TYPE_SONG: PLEX_TYPE_SONG,
KODI_TYPE_AUDIO: PLEX_TYPE_AUDIO,
KODI_TYPE_PHOTO: PLEX_TYPE_PHOTO
}
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
@ -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 = {
PLEX_TYPE_MOVIE: 'movie',
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:
# http://kodi.wiki/view/Features_and_supported_codecs#Format_support (RAW image
# formats, BMP, JPEG, GIF, PNG, TIFF, MNG, ICO, PCX and Targa/TGA)
@ -360,3 +466,21 @@ SORT_METHODS_ALBUMS = (
'SORT_METHOD_ARTIST',
'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 -*-
###############################################################################
import logging
from logging import getLogger
from shutil import copytree
import xml.etree.ElementTree as etree
from os import makedirs
@ -8,13 +8,14 @@ from os import makedirs
import xbmc
from xbmcvfs import exists
from utils import window, settings, language as lang, tryEncode, indent, \
normalize_nodes, exists_dir, tryDecode
from utils import window, settings, language as lang, try_encode, indent, \
normalize_nodes, exists_dir, try_decode
import variables as v
import state
###############################################################################
log = logging.getLogger("PLEX."+__name__)
log = getLogger("PLEX."+__name__)
###############################################################################
# Paths are strings, NOT unicode!
@ -46,7 +47,7 @@ class VideoNodes(object):
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
# Plex: reassign mediatype due to Kodi inner workings
# How many items do we get at most?
limit = window('fetch_pms_item_number')
limit = state.FETCH_PMS_ITEM_NUMBER
mediatypes = {
'movie': 'movies',
'show': 'tvshows',
@ -62,9 +63,9 @@ class VideoNodes(object):
dirname = viewid
# Returns strings
path = tryDecode(xbmc.translatePath(
path = try_decode(xbmc.translatePath(
"special://profile/library/video/"))
nodepath = tryDecode(xbmc.translatePath(
nodepath = try_decode(xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname))
if delete:
@ -77,9 +78,9 @@ class VideoNodes(object):
# Verify the video directory
if not exists_dir(path):
copytree(
src=tryDecode(xbmc.translatePath(
src=try_decode(xbmc.translatePath(
"special://xbmc/system/library/video")),
dst=tryDecode(xbmc.translatePath(
dst=try_decode(xbmc.translatePath(
"special://profile/library/video")))
# Create the node directory
@ -292,7 +293,7 @@ class VideoNodes(object):
# To do: add our photos nodes to kodi picture sources somehow
continue
if exists(tryEncode(nodeXML)):
if exists(try_encode(nodeXML)):
# Don't recreate xml if already exists
continue
@ -378,9 +379,9 @@ class VideoNodes(object):
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
tagname = tryEncode(tagname)
cleantagname = tryDecode(normalize_nodes(tagname))
nodepath = tryDecode(xbmc.translatePath(
tagname = try_encode(tagname)
cleantagname = try_decode(normalize_nodes(tagname))
nodepath = try_decode(xbmc.translatePath(
"special://profile/library/video/"))
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname
@ -394,9 +395,9 @@ class VideoNodes(object):
if not exists_dir(nodepath):
# We need to copy over the default items
copytree(
src=tryDecode(xbmc.translatePath(
src=try_decode(xbmc.translatePath(
"special://xbmc/system/library/video")),
dst=tryDecode(xbmc.translatePath(
dst=try_decode(xbmc.translatePath(
"special://profile/library/video")))
labels = {
@ -411,7 +412,7 @@ class VideoNodes(object):
window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=itemtype)
if exists(tryEncode(nodeXML)):
if exists(try_encode(nodeXML)):
# Don't recreate xml if already exists
return

View file

@ -292,7 +292,7 @@ class ABNF(object):
opcode: operation code. please see OPCODE_XXX.
"""
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
data = utils.tryEncode(data)
data = utils.try_encode(data)
# mask must be set if send data from client
return ABNF(1, 0, 0, 0, opcode, 1, data)
@ -504,7 +504,8 @@ class WebSocket(object):
self.connected = True
def _validate_header(self, headers, key):
@staticmethod
def _validate_header(headers, key):
for k, v in _HEADERS_TO_CHECK.iteritems():
r = headers.get(k, None)
if not r:
@ -598,7 +599,7 @@ class WebSocket(object):
return value: string(byte array) value.
"""
opcode, data = self.recv_data()
_, data = self.recv_data()
return data
def recv_data(self):
@ -620,7 +621,6 @@ class WebSocket(object):
self._cont_data[1] += frame.data
else:
self._cont_data = [frame.opcode, frame.data]
if frame.fin:
data = self._cont_data
self._cont_data = None
@ -740,7 +740,7 @@ class WebSocket(object):
def _recv(self, bufsize):
try:
bytes = self.sock.recv(bufsize)
bytes_ = self.sock.recv(bufsize)
except socket.timeout as e:
raise WebSocketTimeoutException(e.args[0])
except SSLError as e:
@ -748,17 +748,17 @@ class WebSocket(object):
raise WebSocketTimeoutException(e.args[0])
else:
raise
if not bytes:
if not bytes_:
raise WebSocketConnectionClosedException()
return bytes
return bytes_
def _recv_strict(self, bufsize):
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
while shortage > 0:
bytes = self._recv(shortage)
self._recv_buffer.append(bytes)
shortage -= len(bytes)
bytes_ = self._recv(shortage)
self._recv_buffer.append(bytes_)
shortage -= len(bytes_)
unified = "".join(self._recv_buffer)
if shortage == 0:
self._recv_buffer = []
@ -783,7 +783,7 @@ class WebSocketApp(object):
Higher level of APIs are provided.
The interface is like JavaScript WebSocket object.
"""
def __init__(self, url, header=[],
def __init__(self, url, header=None,
on_open=None, on_message=None, on_error=None,
on_close=None, keep_running=True, get_mask_key=None):
"""
@ -807,7 +807,7 @@ class WebSocketApp(object):
docstring for more information
"""
self.url = url
self.header = header
self.header = [] if header is None else header
self.on_open = on_open
self.on_message = on_message
self.on_error = on_error
@ -830,12 +830,12 @@ class WebSocketApp(object):
close websocket connection.
"""
self.keep_running = False
if(self.sock != None):
self.sock.close()
if self.sock != None:
self.sock.close()
def _send_ping(self, interval):
while True:
for i in range(interval):
for _ in range(interval):
time.sleep(1)
if not self.keep_running:
return
@ -878,8 +878,7 @@ class WebSocketApp(object):
if data is None or self.keep_running == False:
break
self._callback(self.on_message, data)
self._callback(self.on_message, data)
except Exception, e:
#print str(e.args[0])
if "timed out" not in e.args[0]:

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from logging import getLogger
import websocket
from json import loads
import xml.etree.ElementTree as etree
from threading import Thread
from Queue import Queue
from ssl import CERT_NONE
from xbmc import sleep
@ -14,10 +13,11 @@ from xbmc import sleep
from utils import window, settings, thread_methods
from companion import process_command
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):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
def __init__(self, callback=None):
if callback is not None:
self.mgr = callback
def __init__(self):
self.ws = None
Thread.__init__(self)
super(WebSocket, self).__init__()
def process(self, opcode, message):
raise NotImplementedError
@ -56,26 +54,23 @@ class WebSocket(Thread):
raise NotImplementedError
def run(self):
log.info("----===## Starting %s ##===----" % self.__class__.__name__)
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
counter = 0
handshake_counter = 0
thread_stopped = self.thread_stopped
thread_suspended = self.thread_suspended
while not thread_stopped():
stopped = self.stopped
suspended = self.suspended
while not stopped():
# In the event the server goes offline
while thread_suspended():
while suspended():
# Set in service.py
if self.ws is not None:
try:
self.ws.shutdown()
except:
pass
self.ws.close()
self.ws = None
if thread_stopped():
if stopped():
# Abort was requested while waiting. We should exit
log.info("##===---- %s Stopped ----===##"
% self.__class__.__name__)
LOG.info("##===---- %s Stopped ----===##",
self.__class__.__name__)
return
sleep(1000)
try:
@ -84,8 +79,8 @@ class WebSocket(Thread):
# No worries if read timed out
pass
except websocket.WebSocketConnectionClosedException:
log.info("%s: connection closed, (re)connecting"
% self.__class__.__name__)
LOG.info("%s: connection closed, (re)connecting",
self.__class__.__name__)
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
@ -96,7 +91,7 @@ class WebSocket(Thread):
enable_multithread=True)
except IOError:
# Server is probably offline
log.info("%s: Error connecting" % self.__class__.__name__)
LOG.info("%s: Error connecting", self.__class__.__name__)
self.ws = None
counter += 1
if counter > 3:
@ -104,68 +99,54 @@ class WebSocket(Thread):
self.IOError_response()
sleep(1000)
except websocket.WebSocketTimeoutException:
log.info("%s: Timeout while connecting, trying again"
% self.__class__.__name__)
LOG.info("%s: Timeout while connecting, trying again",
self.__class__.__name__)
self.ws = None
sleep(1000)
except websocket.WebSocketException as e:
log.info('%s: WebSocketException: %s'
% (self.__class__.__name__, e))
if 'Handshake Status 401' in e.args:
LOG.info('%s: WebSocketException: %s',
self.__class__.__name__, e)
if ('Handshake Status 401' in e.args
or 'Handshake Status 403' in e.args):
handshake_counter += 1
if handshake_counter >= 5:
log.info('%s: Error in handshake detected. '
'Stopping now'
% self.__class__.__name__)
LOG.info('%s: Error in handshake detected. '
'Stopping now', self.__class__.__name__)
break
self.ws = None
sleep(1000)
except Exception as e:
log.error('%s: Unknown exception encountered when '
'connecting: %s' % (self.__class__.__name__, e))
LOG.error('%s: Unknown exception encountered when '
'connecting: %s', self.__class__.__name__, e)
import traceback
log.error("%s: Traceback:\n%s"
% (self.__class__.__name__,
traceback.format_exc()))
LOG.error("%s: Traceback:\n%s",
self.__class__.__name__, traceback.format_exc())
self.ws = None
sleep(1000)
else:
counter = 0
handshake_counter = 0
except Exception as e:
log.error("%s: Unknown exception encountered: %s"
% (self.__class__.__name__, e))
LOG.error("%s: Unknown exception encountered: %s",
self.__class__.__name__, e)
import traceback
log.error("%s: Traceback:\n%s"
% (self.__class__.__name__,
traceback.format_exc()))
try:
self.ws.shutdown()
except:
pass
LOG.error("%s: Traceback:\n%s",
self.__class__.__name__, traceback.format_exc())
if self.ws is not None:
self.ws.close()
self.ws = None
log.info("##===---- %s Stopped ----===##" % self.__class__.__name__)
def stopThread(self):
"""
Overwrite this method from thread_methods to close websockets
"""
log.info("Stopping %s thread." % self.__class__.__name__)
self.__threadStopped = True
try:
self.ws.shutdown()
except:
pass
# Close websocket connection on shutdown
if self.ws is not None:
self.ws.close()
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD'])
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'BACKGROUND_SYNC_DISABLED'])
class PMS_Websocket(WebSocket):
"""
Websocket connection with the PMS for Plex Companion
"""
# Communication with librarysync
queue = Queue()
def getUri(self):
server = window('pms_server')
# Get the appropriate prefix for the websocket
@ -180,8 +161,8 @@ class PMS_Websocket(WebSocket):
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
log.debug("%s: Uri: %s, sslopt: %s"
% (self.__class__.__name__, uri, sslopt))
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)
return uri, sslopt
def process(self, opcode, message):
@ -191,38 +172,38 @@ class PMS_Websocket(WebSocket):
try:
message = loads(message)
except ValueError:
log.error('%s: Error decoding message from websocket'
% self.__class__.__name__)
log.error(message)
LOG.error('%s: Error decoding message from websocket',
self.__class__.__name__)
LOG.error(message)
return
try:
message = message['NotificationContainer']
except KeyError:
log.error('%s: Could not parse PMS message: %s'
% (self.__class__.__name__, message))
LOG.error('%s: Could not parse PMS message: %s',
self.__class__.__name__, message)
return
# Triage
typus = message.get('type')
if typus is None:
log.error('%s: No message type, dropping message: %s'
% (self.__class__.__name__, message))
LOG.error('%s: No message type, dropping message: %s',
self.__class__.__name__, message)
return
log.debug('%s: Received message from PMS server: %s'
% (self.__class__.__name__, message))
LOG.debug('%s: Received message from PMS server: %s',
self.__class__.__name__, message)
# Drop everything we're not interested in
if typus not in ('playing', 'timeline', 'activity'):
return
elif typus == 'activity' and state.DB_SCAN is True:
# Only add to processing if PKC is NOT doing a lib scan (and thus
# possibly causing these reprocessing messages en mass)
log.debug('%s: Dropping message as PKC is currently synching'
% self.__class__.__name__)
LOG.debug('%s: Dropping message as PKC is currently synching',
self.__class__.__name__)
else:
# Put PMS message on queue and let libsync take care of it
self.queue.put(message)
state.WEBSOCKET_QUEUE.put(message)
def IOError_response(self):
log.warn("Repeatedly could not connect to PMS, "
LOG.warn("Repeatedly could not connect to PMS, "
"declaring the connection dead")
window('plex_online', value='false')
@ -233,72 +214,70 @@ class Alexa_Websocket(WebSocket):
Can't use thread_methods!
"""
__thread_stopped = False
__thread_suspended = False
thread_stopped = False
thread_suspended = False
def getUri(self):
self.plex_client_Id = window('plex_client_Id')
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (state.PLEX_USER_ID,
self.plex_client_Id, state.PLEX_TOKEN))
v.PKC_MACHINE_IDENTIFIER,
state.PLEX_TOKEN))
sslopt = {}
log.debug("%s: Uri: %s, sslopt: %s"
% (self.__class__.__name__, uri, sslopt))
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)
return uri, sslopt
def process(self, opcode, message):
if opcode not in self.opcode_data:
return
log.debug('%s: Received the following message from Alexa:'
% self.__class__.__name__)
log.debug('%s: %s' % (self.__class__.__name__, message))
LOG.debug('%s: Received the following message from Alexa:',
self.__class__.__name__)
LOG.debug('%s: %s', self.__class__.__name__, message)
try:
message = etree.fromstring(message)
except Exception as ex:
log.error('%s: Error decoding message from Alexa: %s'
% (self.__class__.__name__, ex))
LOG.error('%s: Error decoding message from Alexa: %s',
self.__class__.__name__, ex)
return
try:
if message.attrib['command'] == 'processRemoteControlCommand':
message = message[0]
else:
log.error('%s: Unknown Alexa message received'
% self.__class__.__name__)
LOG.error('%s: Unknown Alexa message received',
self.__class__.__name__)
return
except:
log.error('%s: Could not parse Alexa message'
% self.__class__.__name__)
LOG.error('%s: Could not parse Alexa message',
self.__class__.__name__)
return
process_command(message.attrib['path'][1:],
message.attrib,
queue=self.mgr.plexCompanion.queue)
process_command(message.attrib['path'][1:], message.attrib)
def IOError_response(self):
pass
# Path in thread_methods
def stop_thread(self):
self.__thread_stopped = True
def stop(self):
self.thread_stopped = True
def suspend_thread(self):
self.__thread_suspended = True
def suspend(self):
self.thread_suspended = True
def resume_thread(self):
self.__thread_suspended = False
def resume(self):
self.thread_suspended = False
def thread_stopped(self):
if self.__thread_stopped is True:
def stopped(self):
if self.thread_stopped is True:
return True
if state.STOP_PKC:
return True
return False
# The culprit
def thread_suspended(self):
def suspended(self):
"""
Overwrite method since we need to check for plex token
"""
if self.__thread_suspended is True:
if self.thread_suspended is True:
return True
if not state.PLEX_TOKEN:
return True

View file

@ -38,11 +38,21 @@
<setting type="lsep" label="39700" />
<setting id="enable_alexa" label="39701" type="bool" default="true"/>
<setting type="lsep" label="" />
<setting id="enableContext" type="bool" label="30413" default="true" />
<setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-1,true)" subsetting="true" />
<!-- Different settings that are not visible - to avoid warnings in the log -->
<setting id="skipContextMenu" type="bool" label="30520" default="false"/>
<setting id="plex_restricteduser" type="bool" default="false" 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="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 label="30506"><!-- Sync Options -->
@ -112,6 +122,7 @@
<setting id="bestTrailer" type="bool" label="30542" default="true" />
<setting id="force_transcode_pix" type="bool" label="30545" default="false" />
<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 label="30544"><!-- artwork -->
@ -134,6 +145,7 @@
<category label="39073"><!-- Appearance Tweaks -->
<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 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-->

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
###############################################################################
import logging
from logging import getLogger
from os import path as os_path
from sys import path as sys_path, argv
@ -11,50 +9,46 @@ from xbmcaddon import Addon
###############################################################################
_addon = Addon(id='plugin.video.plexkodiconnect')
_ADDON = Addon(id='plugin.video.plexkodiconnect')
try:
_addon_path = _addon.getAddonInfo('path').decode('utf-8')
_ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8')
except TypeError:
_addon_path = _addon.getAddonInfo('path').decode()
_ADDON_PATH = _ADDON.getAddonInfo('path').decode()
try:
_base_resource = translatePath(os_path.join(
_addon_path,
_BASE_RESOURCE = translatePath(os_path.join(
_ADDON_PATH,
'resources',
'lib')).decode('utf-8')
except TypeError:
_base_resource = translatePath(os_path.join(
_addon_path,
_BASE_RESOURCE = translatePath(os_path.join(
_ADDON_PATH,
'resources',
'lib')).decode()
sys_path.append(_base_resource)
sys_path.append(_BASE_RESOURCE)
###############################################################################
from utils import settings, window, language as lang, dialog, tryDecode
from utils import settings, window, language as lang, dialog
from userclient import UserClient
import initialsetup
from kodimonitor import KodiMonitor
from kodimonitor import KodiMonitor, SpecialMonitor
from librarysync import LibrarySync
import videonodes
from websocket_client import PMS_Websocket, Alexa_Websocket
import downloadutils
from playqueue import Playqueue
import PlexAPI
from PlexFunctions import check_connection
from PlexCompanion import PlexCompanion
from command_pipeline import Monitor_Window
from playback_starter import Playback_Starter
from playqueue import PlayqueueMonitor
from artwork import Image_Cache_Thread
import variables as v
import state
###############################################################################
import loghandler
loghandler.config()
log = logging.getLogger("PLEX.service")
LOG = getLogger("PLEX.service")
###############################################################################
@ -67,61 +61,28 @@ class Service():
ws = None
library = None
plexCompanion = None
playqueue = None
user_running = False
ws_running = False
alexa_running = False
library_running = False
plexCompanion_running = False
playqueue_running = False
kodimonitor_running = False
playback_starter_running = False
image_cache_thread_running = False
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
log.info("======== START %s ========" % v.ADDON_NAME)
log.info("Platform: %s" % v.PLATFORM)
log.info("KODI Version: %s" % v.KODILONGVERSION)
log.info("%s Version: %s" % (v.ADDON_NAME, v.ADDON_VERSION))
log.info("Using plugin paths: %s"
% (settings('useDirectPaths') != "true"))
log.info("Number of sync threads: %s"
% settings('syncThreadNumber'))
log.info("Full sys.argv received: %s" % argv)
# Reset window props for profile switch
properties = [
"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")
LOG.info("======== START %s ========", v.ADDON_NAME)
LOG.info("Platform: %s", v.PLATFORM)
LOG.info("KODI Version: %s", v.KODILONGVERSION)
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == "true")
LOG.info("Number of sync threads: %s", settings('syncThreadNumber'))
LOG.info("Full sys.argv received: %s", argv)
self.monitor = Monitor()
# Load/Reset PKC entirely - important for user/Kodi profile switch
initialsetup.reload_pkc()
def __stop_PKC(self):
"""
@ -136,36 +97,34 @@ class Service():
monitor = self.monitor
kodiProfile = v.KODI_PROFILE
# Detect playback start early on
self.command_pipeline = Monitor_Window(self)
self.command_pipeline.start()
# Server auto-detect
initialsetup.InitialSetup().setup()
# Detect playback start early on
self.command_pipeline = Monitor_Window()
self.command_pipeline.start()
# Initialize important threads, handing over self for callback purposes
self.user = UserClient(self)
self.ws = PMS_Websocket(self)
self.alexa = Alexa_Websocket(self)
self.library = LibrarySync(self)
self.plexCompanion = PlexCompanion(self)
self.playqueue = Playqueue(self)
self.playback_starter = Playback_Starter(self)
self.user = UserClient()
self.ws = PMS_Websocket()
self.alexa = Alexa_Websocket()
self.library = LibrarySync()
self.plexCompanion = PlexCompanion()
self.specialMonitor = SpecialMonitor()
self.playback_starter = Playback_Starter()
self.playqueue = PlayqueueMonitor()
if settings('enableTextureCache') == "true":
self.image_cache_thread = Image_Cache_Thread()
plx = PlexAPI.PlexAPI()
welcome_msg = True
counter = 0
while not __stop_PKC():
if window('plex_kodiProfile') != kodiProfile:
# Profile change happened, terminate this thread and others
log.info("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread."
% (kodiProfile,
window('plex_kodiProfile')))
LOG.info("Kodi profile was: %s and changed to: %s. "
"Terminating old PlexKodiConnect thread.",
kodiProfile, window('plex_kodiProfile'))
break
# Before proceeding, need to make sure:
@ -191,11 +150,8 @@ class Service():
time=2000,
sound=False)
# Start monitoring kodi events
self.kodimonitor_running = KodiMonitor(self)
# Start playqueue client
if not self.playqueue_running:
self.playqueue_running = True
self.playqueue.start()
self.kodimonitor_running = KodiMonitor()
self.specialMonitor.start()
# Start the Websocket Client
if not self.ws_running:
self.ws_running = True
@ -216,6 +172,7 @@ class Service():
if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
self.playqueue.start()
if (not self.image_cache_thread_running and
settings('enableTextureCache') == "true"):
self.image_cache_thread_running = True
@ -225,7 +182,7 @@ class Service():
# Alert user is not authenticated and suppress future
# warning
self.warn_auth = False
log.warn("Not authenticated yet.")
LOG.warn("Not authenticated yet.")
# User access is restricted.
# Keep verifying until access is granted
@ -249,7 +206,7 @@ class Service():
if server is False:
# No server info set in add-on settings
pass
elif plx.CheckConnection(server, verifySSL=True) is False:
elif check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online:
@ -257,7 +214,7 @@ class Service():
window('plex_online', value="false")
# Suspend threads
state.SUSPEND_LIBRARY_THREAD = True
log.error("Plex Media Server went offline")
LOG.error("Plex Media Server went offline")
if settings('show_pms_offline') == 'true':
dialog('notification',
lang(33001),
@ -269,9 +226,9 @@ class Service():
if counter > 20:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.PickPMS()
tmp = setup.pick_pms()
if tmp is not None:
setup.WritePMStoSettings(tmp)
setup.write_pms_to_settings(tmp)
else:
# Server is online
counter = 0
@ -291,7 +248,7 @@ class Service():
icon='{plex}',
time=5000,
sound=False)
log.info("Server %s is online and ready." % server)
LOG.info("Server %s is online and ready.", server)
window('plex_online', value="true")
if state.AUTHENTICATED:
# Server got offline when we were authenticated.
@ -316,29 +273,25 @@ class Service():
# Tell all threads to terminate (e.g. several lib sync threads)
state.STOP_PKC = True
try:
downloadutils.DownloadUtils().stopSession()
except:
pass
window('plex_service_started', clear=True)
log.info("======== STOP %s ========" % v.ADDON_NAME)
LOG.info("======== STOP %s ========", v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation!
if window('plex_service_started') == 'true':
exit = True
EXIT = True
else:
window('plex_service_started', value='true')
exit = False
EXIT = False
# Delay option
delay = int(settings('startupDelay'))
DELAY = int(settings('startupDelay'))
log.info("Delaying Plex startup by: %s sec..." % delay)
if exit:
log.error('PKC service.py already started - exiting this instance')
elif delay and Monitor().waitForAbort(delay):
LOG.info("Delaying Plex startup by: %s sec...", DELAY)
if EXIT:
LOG.error('PKC service.py already started - exiting this instance')
elif DELAY and Monitor().waitForAbort(DELAY):
# Start the service
log.info("Abort requested while waiting. PKC not started.")
LOG.info("Abort requested while waiting. PKC not started.")
else:
Service().ServiceEntryPoint()