Merge branch 'develop' into translations

This commit is contained in:
tomkat83 2017-03-07 14:24:02 +01:00
commit 5f91580e76
31 changed files with 814 additions and 427 deletions

View file

@ -2,6 +2,17 @@
Thanks a ton for contributing to PlexKodiConnect!
## Feature requests
* Are you missing a certain functionality? Then [visit feathub.com](http://feathub.com/croneter/PlexKodiConnect)
## Issues
* Something not working like it's supposed to? Then [open a new issue report](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug)
## Translations
* Want to help translate PlexKodiConnect? Then go [visit crowdin.com](https://crowdin.com/project/plexkodiconnect/invite)
## Programming
@ -9,7 +20,3 @@ Thanks a ton for contributing to PlexKodiConnect!
* Thanks if you can follow the Python style guide [PEP8](https://www.python.org/dev/peps/pep-0008/) to keep things neat and clean
* Thanks if you add some comments to make your code more readable ;-)
## Translations
* Please [only use crowdin.com](https://crowdin.com/project/plexkodiconnect/invite) to help with translations. Don't use Github pull requests.

View file

@ -1,3 +1,9 @@
##Status
[![GitHub issues](https://img.shields.io/github/issues/croneter/PlexKodiConnect.svg?maxAge=60&style=flat-square)](https://github.com/croneter/PlexKodiConnect/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/croneter/PlexKodiConnect.svg?maxAge=60&style=flat-square)](https://github.com/croneter/PlexKodiConnect/pulls)
# PlexKodiConnect (PKC)
**Combine the best frontend media player Kodi with the best multimedia backend server Plex**
@ -19,7 +25,7 @@ Please help translate PlexKodiConnect into your language: [visit crowdin.com](ht
* [**What is currently supported?**](#what-is-currently-supported)
* [**Known Larger Issues**](#known-larger-issues)
* [**Issues being worked on**](#issues-being-worked-on)
* [**Pipeline for future development**](#what-could-be-in-the-pipeline-for-future-development)
* [**Requests for new features**](#requests-for-new-features)
* [**Checkout the PKC Wiki**](#checkout-the-pkc-wiki)
* [**Credits**](#credits)
@ -77,6 +83,7 @@ PKC currently provides the following features:
+ German
+ Czech, thanks @Pavuucek
+ Spanish, thanks @bartolomesoriano
+ Danish, thanks @FIGHT
+ More coming up: [you can help!](https://crowdin.com/project/plexkodiconnect/invite)
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
- [Plex Companion](https://support.plex.tv/hc/en-us/sections/200276908-Plex-Companion): fling Plex media (or anything else) from other Plex devices to PlexKodiConnect
@ -113,12 +120,9 @@ However, some changes to individual items are instantly detected, e.g. if you ma
Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues). Before you open your own issue, please read [How to report a bug](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug).
### What could be in the pipeline for future development?
### Requests for new features
- Plex channels
- Movie extras (trailers already work)
- Playlists
- Music Videos
[![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect)
### Checkout the PKC Wiki
The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions. You can even edit the wiki yourself!

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect"
version="1.5.14"
version="1.6.4"
provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
@ -27,12 +27,16 @@
<summary lang="cs">Úplná integrace Plexu do Kodi</summary>
<summary lang="de">Komplette Integration von Plex in Kodi</summary>
<summary lang="es">Native Integration of Plex into Kodi</summary>
<summary lang="dk">Indbygget Integration af Plex i Kodi</summary>
<summary lang="nl">Directe integratie van Plex in Kodi</summary>
<description lang="en">Connect Kodi to your Plex Media Server. This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases (as this plugin directly changes them). Use at your own risk!</description>
<description lang="en_gb">Connect Kodi to your Plex Media Server. This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases (as this plugin directly changes them). Use at your own risk!</description>
<description lang="en_us">Connect Kodi to your Plex Media Server. This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases (as this plugin directly changes them). Use at your own risk!</description>
<description lang="cs">Připojte Kodi ke svému Plex Media Serveru. Tento doplněk předpokládá, že spravujete veškerá svá videa pomocí Plexu (nikoliv pomocí Kodi). Můžete přijít o data uložená ve video a hudební databázi Kodi (tento doplněk je přímo mění). Používejte na vlastní nebezpečí!</description>
<description lang="de">Verbindet Kodi mit deinem Plex Media Server. Dieses Addon geht davon aus, dass du all deine Videos mit Plex verwaltest (und keine direkt mit Kodi). Du wirst möglicherweise Daten verlieren, die bereits in der Kodi Video- und/oder Musik-Datenbank gespeichert sind (da dieses Addon beide Datenbanken direkt verändert). Verwende auf eigene Gefahr!</description>
<description lang="es">Connect Kodi to your Plex Media Server. This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases (as this plugin directly changes them). Use at your own risk!</description>
<description lang="dk">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
<description lang="nl">Verbind Kodi met je Plex Media Server. Deze plugin gaat ervan uit dat je al je video's met Plex (en niet met Kodi) beheerd. Je kunt gegevens reeds opgeslagen in de databases voor video en muziek van Kodi (deze plugin wijzigt deze gegevens direct) verliezen. Gebruik op eigen risico!</description>
<platform>all</platform>
<license>GPL v2.0</license>
<forum>https://forums.plex.tv</forum>

View file

@ -1,3 +1,51 @@
version 1.6.4 (beta only)
- Amazon Alexa support! Be mindful to check the Alexa forum thread first; there are still many issues completely unrelated to PKC
- Enable skipping for Plex Companion
- Set default companion name to PlexKodiConnect
version 1.6.3
- Fix UnicodeEncodeError for non ASCII filenames in playback_starter
- Cleanup playlist/playqueue string/unicode
version 1.6.2
- Fix Plex Web Issue, thanks @AllanMar
- Fix TypeError on manually entering PMS port
- Fix KeyError
- Update Danish translation
- Update readme
version 1.6.1
- New Danish translation, thanks @Osberg
- Fix UnicodeDecodeError for non-ASCII filenames
- Better error handling for Plex Companion
- Fix ValueError for Watch Later
- Try to skip new PMS items we've already processed
- Fix TypeError
version 1.6.0
A DATABASE RESET IS ABSOLUTELY NECESSARY if you're not using beta PKC
Make previous version available for everyone. The highlights:
- New Spanish translation, thanks @bartolomesoriano
- New Czech translation, thanks @Pavuucek
- Plex Companion is completely rewired and should now handly anything you throw at it
- Early compatibility with Kodi 18 Leia
- New playback startup mechanism for plugin paths
- Code rebranding from Emby to Plex, including a plex.db database :-)
- Fixes to Kodi ratings
- Fix playstate and PMS item changes not working/not propagating anymore (caused by a change Plex made with the websocket interface)
- Improvements to the way PKC behaves if the PMS goes offline
- New setting to always transcode if the video bitrate is above a certain threshold (will not work with direct paths)
- Be smarter when deciding when to transcode
- Only sign the user out if the PMS says so
- Cache missing artwork on PKC startup
- Lots of code refactoring and code optimizations
- Tons of fixes
version 1.5.15 (beta only)
- Fix ratings for movies
- Fixes to Plex Companion
- Always run only one instance of PKC
version 1.5.14 (beta only)
- Krypton: Fix ratings for episodes and TV shows
- Plex Companion: Fix KeyError for Plex Web

View file

@ -161,8 +161,7 @@ class Main():
modes[mode](itemid, params=argv[2])
elif mode == 'Plex_Node':
modes[mode](params.get('id'),
params.get('viewOffset'),
params.get('plex_type'))
params.get('viewOffset'))
else:
modes[mode]()
else:

View file

@ -513,4 +513,7 @@
<string id="39601">Could not stop the database from running. Please try again later.</string>
<string id="39602">Remove all cached artwork? (recommended!)</string>
<string id="39603">Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended and unnecessary!)</string>
</strings>
<string id="39700">Amazon Alexa (Voice Recognition)</string>
<string id="39701">Alexa aktivieren</string>
</strings>

View file

@ -513,4 +513,7 @@
<string id="39601">Kodi Datenbank konnte nicht gestoppt werden. Bitte später erneut versuchen.</string>
<string id="39602">Alle zwischengespeicherten Bilder löschen? (empfohlen!)</string>
<string id="39603">Alle PlexKodiConnect Einstellungen zurücksetzen? (normalerweise NICHT empfohlen und nicht nötig!)</string>
<string id="39700">Amazon Alexa (Spracherkennung)</string>
<string id="39701">Alexa aktivieren</string>
</strings>

View file

@ -1256,26 +1256,26 @@ class API():
favorite = False
try:
playcount = int(item['viewCount'])
except KeyError:
except (KeyError, ValueError):
playcount = None
played = True if playcount else False
try:
lastPlayedDate = DateToKodi(int(item['lastViewedAt']))
except KeyError:
except (KeyError, ValueError):
lastPlayedDate = None
try:
userrating = int(float(item['userRating']))
except KeyError:
except (KeyError, ValueError):
userrating = 0
try:
rating = float(item['audienceRating'])
except KeyError:
except (KeyError, ValueError):
try:
rating = float(item['rating'])
except KeyError:
except (KeyError, ValueError):
rating = 0.0
resume, runtime = self.getRuntime()

View file

@ -6,14 +6,16 @@ from socket import SHUT_RDWR
from xbmc import sleep
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods, \
window
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API
import player
from entrypoint import Plex_Node
from variables import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE
import variables as v
###############################################################################
@ -35,7 +37,7 @@ class PlexCompanion(Thread):
# Start GDM for server/client discovery
self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings)
log.debug("Registration string is: %s "
log.debug("Registration string is:\n%s"
% self.client.getClientDetails())
# kodi player instance
self.player = player.Player()
@ -77,20 +79,42 @@ class PlexCompanion(Thread):
log.debug('Processing: %s' % task)
data = task['data']
if (task['action'] == 'playlist' and
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')
self.mgr.playqueue.init_playqueue_from_plex_children(
api.getRatingKey())
else:
thread = Thread(target=Plex_Node,
args=('{server}%s' % data.get('key'),
data.get('offset'),
True,
False),)
thread.setDaemon(True)
thread.start()
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
# E.g. watch later initiated by Companion
thread = Thread(target=Plex_Node,
args=('{server}%s' % data.get('key'),
data.get('offset'),
data.get('type'),
True),)
thread.setDaemon(True)
thread.start()
elif task['action'] == 'playlist':
# Get the playqueue ID
try:
_, ID, query = ParseContainerKey(data['containerKey'])
typus, ID, query = ParseContainerKey(data['containerKey'])
except Exception as e:
log.error('Exception while processing: %s' % e)
import traceback
@ -98,14 +122,19 @@ class PlexCompanion(Thread):
return
try:
playqueue = self.mgr.playqueue.get_playqueue_from_type(
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['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(
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
ID,
@ -126,6 +155,7 @@ class PlexCompanion(Thread):
jsonClass, requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100)
self.queue = queue
if settings('plexCompanion') == 'true':
# Start up httpd
@ -188,6 +218,7 @@ class PlexCompanion(Thread):
log.debug("Client is no longer registered. "
"Plex Companion still running on port %s"
% self.settings['myport'])
client.register_as_client()
# Get and set servers
if message_count % 30 == 0:
subscriptionManager.serverlist = client.getServerList()
@ -209,7 +240,7 @@ class PlexCompanion(Thread):
queue.task_done()
# Don't sleep
continue
sleep(20)
sleep(50)
client.stop_all()
if httpd:

View file

@ -99,7 +99,7 @@ def setKodiWebServerDetails():
result = loads(result)
try:
xbmc_username = result['result']['value']
except TypeError:
except (TypeError, KeyError):
pass
web_pass = {
"jsonrpc": "2.0",

View file

@ -39,7 +39,7 @@ def getXArgsDeviceInfo(options=None):
'X-Plex-Product': v.ADDON_NAME,
'X-Plex-Version': v.ADDON_VERSION,
'X-Plex-Client-Identifier': getDeviceId(),
'X-Plex-Provides': 'client,controller,player',
'X-Plex-Provides': 'client,controller,player,pubsub-player',
}
if window('pms_token'):
xargs['X-Plex-Token'] = window('pms_token')

187
resources/lib/companion.py Normal file
View file

@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
import logging
from re import compile as re_compile
from xbmc import Player
from utils import JSONRPC
from variables import ALEXA_TO_COMPANION
from playqueue import Playqueue
from PlexFunctions import GetPlexKeyNumber
###############################################################################
log = logging.getLogger("PLEX."+__name__)
REGEX_PLAYQUEUES = re_compile(r'''/playQueues/(\d+)$''')
###############################################################################
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):
"""
typus: one of the Kodi types, e.g. audio or video
Returns None if nothing was found
"""
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))
found = True
playqueues = Playqueue()
for (player, ID) in getPlayers().iteritems():
playqueue = playqueues.get_playqueue_from_type(player)
for i, item in enumerate(playqueue.items):
if item.ID == playQueueItemID or item.plex_id == plex_id:
break
else:
log.debug('Item not found to skip to')
found = False
if found:
Player().play(playqueue.kodi_pl, None, False, i)
def convert_alexa_to_companion(dictionary):
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):
"""
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:
# We need to tell service.py
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
queue.put({
'action': action,
'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})
elif request_path == "player/playback/play":
for playerid in getPlayerIds():
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
"play": True})
elif request_path == "player/playback/pause":
for playerid in getPlayerIds():
JSONRPC("Player.PlayPause").execute({"playerid": playerid,
"play": False})
elif request_path == "player/playback/stop":
for playerid in getPlayerIds():
JSONRPC("Player.Stop").execute({"playerid": playerid})
elif request_path == "player/playback/seekTo":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute(
{"playerid": playerid,
"value": millisToTime(params.get('offset', 0))})
elif request_path == "player/playback/stepForward":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute({"playerid": playerid,
"value": "smallforward"})
elif request_path == "player/playback/stepBack":
for playerid in getPlayerIds():
JSONRPC("Player.Seek").execute({"playerid": playerid,
"value": "smallbackward"})
elif request_path == "player/playback/skipNext":
for playerid in getPlayerIds():
JSONRPC("Player.GoTo").execute({"playerid": playerid,
"to": "next"})
elif request_path == "player/playback/skipPrevious":
for playerid in getPlayerIds():
JSONRPC("Player.GoTo").execute({"playerid": playerid,
"to": "previous"})
elif request_path == "player/playback/skipTo":
skipTo(params)
elif request_path == "player/navigation/moveUp":
JSONRPC("Input.Up").execute()
elif request_path == "player/navigation/moveDown":
JSONRPC("Input.Down").execute()
elif request_path == "player/navigation/moveLeft":
JSONRPC("Input.Left").execute()
elif request_path == "player/navigation/moveRight":
JSONRPC("Input.Right").execute()
elif request_path == "player/navigation/select":
JSONRPC("Input.Select").execute()
elif request_path == "player/navigation/home":
JSONRPC("Input.Home").execute()
elif request_path == "player/navigation/back":
JSONRPC("Input.Back").execute()
else:
log.error('Unknown request path: %s' % request_path)

View file

@ -14,6 +14,7 @@ from utils import window, settings, language as lang, dialog, tryDecode,\
tryEncode, CatchExceptions, JSONRPC
import downloadutils
import playbackutils as pbutils
import plexdb_functions as plexdb
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
GetMachineIdentifier
@ -96,7 +97,7 @@ def togglePlexTV():
sound=False)
def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
def Plex_Node(url, viewOffset, playdirectly=False, node=True):
"""
Called only for a SINGLE element for Plex.tv watch later
@ -120,11 +121,25 @@ def Plex_Node(url, viewOffset, plex_type, playdirectly=False):
else:
window('plex_customplaylist.seektime', value=str(viewOffset))
log.info('Set resume point to %s' % str(viewOffset))
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]
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 = Playqueue().get_playqueue_from_type(typus)
result = pbutils.PlaybackUtils(xml, playqueue).play(
None,
kodi_id='plexnode',
plex_id,
kodi_id=kodi_id,
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
if result.listitem:
listitem = convert_PKC_to_listitem(result.listitem)

View file

@ -6,13 +6,12 @@ import logging
import xbmc
import xbmcgui
from utils import settings, window, language as lang
from utils import settings, window, language as lang, tryEncode
import downloadutils
from userclient import UserClient
from PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier, get_PMS_settings
import variables as v
###############################################################################
@ -257,7 +256,8 @@ class InitialSetup():
log.warn('Not authorized even though we are signed '
' in to plex.tv correctly')
self.dialog.ok(lang(29999), '%s %s'
% lang(39214) + server['name'])
% (lang(39214),
tryEncode(server['name'])))
return
else:
return

View file

@ -316,14 +316,14 @@ class Movies(Items):
# Update the movie entry
if v.KODIVERSION >= 17:
# update new ratings Kodi 17
ratingid = self.kodi_db.get_ratingid(movieid,
v.KODI_TYPE_MOVIE)
rating_id = self.kodi_db.get_ratingid(movieid,
v.KODI_TYPE_MOVIE)
self.kodi_db.update_ratings(movieid,
v.KODI_TYPE_MOVIE,
"default",
rating,
votecount,
ratingid)
rating_id)
# update new uniqueid Kodi 17
uniqueid = self.kodi_db.get_uniqueid(movieid,
v.KODI_TYPE_MOVIE)
@ -342,10 +342,10 @@ class Movies(Items):
WHERE idMovie = ?
'''
kodicursor.execute(query, (title, plot, shortplot, tagline,
votecount, rating, writer, year, imdb, sorttitle, runtime,
mpaa, genre, director, title, studio, trailer, country,
playurl, pathid, fileid, year, userdata['UserRating'],
movieid))
votecount, rating_id, writer, year, imdb, sorttitle,
runtime, mpaa, genre, director, title, studio, trailer,
country, playurl, pathid, fileid, year,
userdata['UserRating'], movieid))
else:
query = '''
UPDATE movie
@ -365,7 +365,8 @@ class Movies(Items):
log.info("ADD movie itemid: %s - Title: %s" % (itemid, title))
if v.KODIVERSION >= 17:
# add new ratings Kodi 17
self.kodi_db.add_ratings(self.kodi_db.create_entry_rating(),
rating_id = self.kodi_db.create_entry_rating()
self.kodi_db.add_ratings(rating_id,
movieid,
v.KODI_TYPE_MOVIE,
"default",
@ -385,9 +386,9 @@ class Movies(Items):
?, ?, ?, ?, ?, ?, ?)
'''
kodicursor.execute(query, (movieid, fileid, title, plot,
shortplot, tagline, votecount, rating, writer, year, imdb,
sorttitle, runtime, mpaa, genre, director, title, studio,
trailer, country, playurl, pathid, year,
shortplot, tagline, votecount, rating_id, writer, year,
imdb, sorttitle, runtime, mpaa, genre, director, title,
studio, trailer, country, playurl, pathid, year,
userdata['UserRating']))
else:
query = '''

View file

@ -321,6 +321,11 @@ class LibrarySync(Thread):
def __init__(self, callback=None):
self.mgr = callback
# Dict of items we just processed in order to prevent a reprocessing
# caused by websocket
self.just_processed = {}
# How long do we wait until we start re-processing? (in seconds)
self.ignore_just_processed = 10*60
self.itemsToProcess = []
self.sessionKeys = []
self.fanartqueue = Queue.Queue()
@ -532,6 +537,9 @@ class LibrarySync(Thread):
# True: we're syncing only the delta, e.g. different checksum
self.compare = not repair
# Empty our list of item's we've just processed in the past
self.just_processed = {}
self.new_items_only = True
# This will also update playstates and userratings!
log.info('Running fullsync for NEW PMS items with repair=%s' % repair)
@ -884,6 +892,7 @@ class LibrarySync(Thread):
self.allPlexElementsId APPENDED(!!) dict
= {itemid: checksum}
"""
now = getUnixTimestamp()
if self.new_items_only is True:
# Only process Plex items that Kodi does not already have in lib
for item in xml:
@ -903,6 +912,7 @@ class LibrarySync(Thread):
'title': item.attrib.get('title', 'Missing Title'),
'mediaType': item.attrib.get('type')
})
self.just_processed[itemId] = now
return
if self.compare:
@ -928,6 +938,7 @@ class LibrarySync(Thread):
'title': item.attrib.get('title', 'Missing Title'),
'mediaType': item.attrib.get('type')
})
self.just_processed[itemId] = now
else:
# Initial or repair sync: get all Plex movies
for item in xml:
@ -946,6 +957,7 @@ class LibrarySync(Thread):
'title': item.attrib.get('title', 'Missing Title'),
'mediaType': item.attrib.get('type')
})
self.just_processed[itemId] = now
def GetAndProcessXMLs(self, itemType):
"""
@ -1450,6 +1462,8 @@ class LibrarySync(Thread):
continue
else:
successful = self.process_newitems(item)
if successful:
self.just_processed[str(item['ratingKey'])] = now
if successful and settings('FanartTV') == 'true':
plex_type = v.PLEX_TYPE_FROM_WEBSOCKET[item['type']]
if plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW):
@ -1534,6 +1548,7 @@ class LibrarySync(Thread):
PMS is messing with the library items, e.g. new or changed. Put in our
"processing queue" for later
"""
now = getUnixTimestamp()
for item in data:
if 'tv.plex' in item.get('identifier', ''):
# Ommit Plex DVR messages - the Plex IDs are not corresponding
@ -1548,6 +1563,14 @@ class LibrarySync(Thread):
if plex_id == '0':
log.error('Received malformed PMS message: %s' % item)
continue
try:
if (now - self.just_processed[plex_id] <
self.ignore_just_processed and state != 9):
log.debug('We just processed %s: ignoring' % plex_id)
continue
except KeyError:
# Item has NOT just been processed
pass
# Have we already added this element?
for existingItem in self.itemsToProcess:
if existingItem['ratingKey'] == plex_id:

View file

@ -63,7 +63,7 @@ class MyFormatter(logging.Formatter):
# Replace the original format with one customized by logging level
if record.levelno in (logging.DEBUG, logging.ERROR):
self._fmt = '%(name)s -> %(levelname)s:: %(message)s'
self._fmt = '%(name)s -> %(levelname)s: %(message)s'
# Call the original formatter class to do the grunt work
result = logging.Formatter.format(self, record)

View file

@ -138,7 +138,10 @@ class PlaybackUtils():
plex_lib_UUID,
mediatype=api.getType(),
trailers=trailers)
get_playlist_details_from_xml(playqueue, xml=xml)
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
@ -287,16 +290,19 @@ class PlaybackUtils():
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:
if add_item_to_kodi_playlist(self.playqueue,
self.currentPosition,
kodi_id=db_item[0],
kodi_type=db_item[4]) is True:
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,
@ -306,8 +312,9 @@ class PlaybackUtils():
else:
# Item not in Kodi DB
self.add_trailer(item)
self.playqueue.items[self.currentPosition - 1].ID = item.get(
'%sItemID' % self.playqueue.kind)
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!

View file

@ -3,7 +3,7 @@ from urllib import quote
import plexdb_functions as plexdb
from downloadutils import DownloadUtils as DU
from utils import JSONRPC, tryEncode
from utils import JSONRPC, tryEncode, tryDecode
from PlexAPI import API
###############################################################################
@ -36,7 +36,11 @@ class Playlist_Object_Baseclase(object):
answ += "items: %s, " % self.items
for key in self.__dict__:
if key not in ("ID", 'items'):
answ += '%s: %s, ' % (key, getattr(self, key))
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):
@ -73,14 +77,18 @@ class Playlist_Item(object):
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
uri = None # Weird Plex uri path involving plex_UUID
file = None # Path to the item's file. STRING!!
uri = None # Weird Plex uri path involving plex_UUID. STRING!
guid = None # Weird Plex guid
def __repr__(self):
answ = "<%s: " % (self.__class__.__name__)
for key in self.__dict__:
answ += '%s: %s, ' % (key, getattr(self, key))
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] + ">"
@ -110,6 +118,7 @@ def playlist_item_from_kodi(kodi_item):
# 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)
return item
@ -128,6 +137,10 @@ def playlist_item_from_plex(plex_id):
item.kodi_type = plex_dbitem[4]
except:
raise KeyError('Could not find plex_id %s in database' % 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)
return item
@ -209,15 +222,14 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
"""
if xml is None:
xml = get_PMS_playlist(playlist, playlist_id)
try:
xml.attrib['%sVersion' % playlist.kind]
except:
log.error('Could not process Plex playlist')
return
# Clear our existing playlist and the associated Kodi playlist
playlist.clear()
# Set new values
get_playlist_details_from_xml(playlist, xml)
try:
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not update playlist from PMS')
return
for plex_item in xml:
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
if playlist_item is not None:
@ -231,19 +243,23 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
WILL ALSO UPDATE OUR PLAYLISTS
"""
log.debug('Initializing the playlist %s on the Plex side' % playlist)
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
params = {
'next': 0,
'type': playlist.type,
'uri': item.uri
}
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST",
parameters=params)
get_playlist_details_from_xml(playlist, xml)
try:
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
params = {
'next': 0,
'type': playlist.type,
'uri': item.uri
}
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST",
parameters=params)
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not init Plex playlist')
return
item.ID = xml[-1].attrib['%sItemID' % playlist.kind]
playlist.items.append(item)
log.debug('Initialized the playlist on the Plex side: %s' % playlist)
@ -255,6 +271,8 @@ def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
Adds a listitem to both the Kodi and Plex playlist at position pos [int].
If file is not None, file will overrule kodi_id!
file: str!!
"""
log.debug('add_listitem_to_playlist at position %s. Playlist before add: '
'%s' % (pos, playlist))
@ -282,6 +300,8 @@ 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!
"""
log.debug('add_item_to_playlist. Playlist before adding: %s' % playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
@ -305,7 +325,11 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
log.debug('Adding new item plex_id: %s, kodi_item: %s on the Plex side at '
'position %s for %s' % (plex_id, kodi_item, pos, playlist))
if plex_id:
item = playlist_item_from_plex(plex_id)
try:
item = playlist_item_from_plex(plex_id)
except KeyError:
log.error('Could not add new item to the PMS playlist')
return
else:
item = playlist_item_from_kodi(kodi_item)
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.ID, item.uri)
@ -342,6 +366,8 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
Returns False if unsuccessful
file: str!
"""
log.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
'only at position %s for %s'
@ -418,11 +444,9 @@ def refresh_playlist_from_PMS(playlist):
"""
xml = get_PMS_playlist(playlist)
try:
xml.attrib['%sVersion' % playlist.kind]
except:
log.error('Could not download Plex playlist.')
return
get_playlist_details_from_xml(playlist, xml)
get_playlist_details_from_xml(playlist, xml)
except KeyError:
log.error('Could not refresh playlist from PMS')
def delete_playlist_item_from_PMS(playlist, pos):
@ -469,8 +493,9 @@ def get_kodi_playqueues():
try:
queues = queues['result']
except KeyError:
raise KeyError('Could not get Kodi playqueues. JSON Result was: %s'
% queues)
log.error('Could not get Kodi playqueues. JSON Result was: %s'
% queues)
queues = []
return queues
@ -490,7 +515,7 @@ def add_to_Kodi_playlist(playlist, xml_video_element):
if item.kodi_id:
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
else:
params['item'] = {'file': tryEncode(item.file)}
params['item'] = {'file': item.file}
reply = JSONRPC('Playlist.Add').execute(params)
if reply.get('error') is not None:
log.error('Could not add item %s to Kodi playlist. Error: %s'
@ -506,6 +531,8 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
Adds an xbmc listitem to the Kodi playlist.xml_video_element
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
file: string!
"""
log.debug('Insert listitem at position %s for Kodi only for %s'
% (pos, playlist))

View file

@ -7,8 +7,10 @@ from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO
from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend
import playlist_func as PL
from PlexFunctions import ConvertPlexToKodiTime
from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren
from PlexAPI import API
from playbackutils import PlaybackUtils
import variables as v
###############################################################################
log = logging.getLogger("PLEX."+__name__)
@ -31,6 +33,8 @@ class Playqueue(Thread):
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
@ -69,6 +73,25 @@ class Playqueue(Thread):
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
"""
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 update_playqueue_from_PMS(self,
playqueue,
playqueue_id=None,
@ -85,11 +108,12 @@ class Playqueue(Thread):
'%s, repeat %s' % (playqueue_id, offset, repeat))
with lock:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
if xml is None:
playqueue.clear()
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except KeyError:
log.error('Could not get playqueue ID %s' % playqueue_id)
return
playqueue.clear()
PL.get_playlist_details_from_xml(playqueue, xml)
PlaybackUtils(xml, playqueue).play_all()
playqueue.repeat = 0 if not repeat else int(repeat)
window('plex_customplaylist', value="true")

View file

@ -9,6 +9,7 @@ import xbmcgui
import xbmcvfs
from utils import window, settings, tryEncode, language as lang
import variables as v
import PlexAPI
@ -160,11 +161,11 @@ class PlayUtils():
- video bitrate above specified settings bitrate
if the corresponding file settings are set to 'true'
"""
videoCodec = self.API.getVideoCodec()
log.info("videoCodec: %s" % videoCodec)
if self.API.getType() in ('clip', 'track'):
if self.API.getType() 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

View file

@ -57,7 +57,7 @@ def plex_type(xbmc_type):
def getXMLHeader():
return '<?xml version="1.0" encoding="utf-8" ?>\r\n'
return '<?xml version="1.0" encoding="utf-8" ?>\n'
def getOKMsg():

View file

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
import logging
import re
from re import sub
from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
from xbmc import sleep
from companion import process_command
from utils import window
from functions import *
@ -19,7 +21,6 @@ log = logging.getLogger("PLEX."+__name__)
class MyHandler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
regex = re.compile(r'''/playQueues/(\d+)$''')
def __init__(self, *args, **kwargs):
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
@ -58,7 +59,7 @@ class MyHandler(BaseHTTPRequestHandler):
'x-plex-version, x-plex-platform-version, x-plex-username, '
'x-plex-client-identifier, x-plex-target-client-identifier, '
'x-plex-device-name, x-plex-platform, x-plex-product, accept, '
'x-plex-device')
'x-plex-device, x-plex-device-screen-resolution')
self.end_headers()
self.wfile.close()
@ -83,11 +84,10 @@ class MyHandler(BaseHTTPRequestHandler):
subMgr = self.server.subscriptionManager
js = self.server.jsonClass
settings = self.server.settings
queue = self.server.queue
try:
request_path = self.path[1:]
request_path = re.sub(r"\?.*", "", request_path)
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
params = {}
@ -101,10 +101,10 @@ class MyHandler(BaseHTTPRequestHandler):
params.get('commandID', False))
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\r\nVersion: %s"
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% settings['version'])
elif request_path == "verify":
self.response("XBMC JSON connection test:\r\n" +
self.response("XBMC JSON connection test:\n" +
js.jsonrpc("ping"))
elif "resources" == request_path:
resp = ('%s'
@ -145,9 +145,9 @@ class MyHandler(BaseHTTPRequestHandler):
sleep(950)
commandID = params.get('commandID', 0)
self.response(
re.sub(r"INSERTCOMMANDID",
str(commandID),
subMgr.msg(js.getPlayers())),
sub(r"INSERTCOMMANDID",
str(commandID),
subMgr.msg(js.getPlayers())),
{
'X-Plex-Client-Identifier': settings['uuid'],
'Access-Control-Expose-Headers':
@ -160,121 +160,11 @@ class MyHandler(BaseHTTPRequestHandler):
uuid = self.headers.get('X-Plex-Client-Identifier', False) \
or self.client_address[0]
subMgr.removeSubscriber(uuid)
elif request_path == "player/playback/setParameters":
self.response(getOKMsg(), js.getPlexHeaders())
if 'volume' in params:
volume = int(params['volume'])
log.debug("adjusting the volume to %s%%" % volume)
js.jsonrpc("Application.SetVolume",
{"volume": volume})
elif "/playMedia" in request_path:
self.response(getOKMsg(), js.getPlexHeaders())
offset = params.get('viewOffset', params.get('offset', "0"))
protocol = params.get('protocol', "http")
address = params.get('address', self.client_address[0])
server = self.getServerByHost(address)
port = params.get('port', server.get('port', '32400'))
try:
containerKey = urlparse(params.get('containerKey')).path
except:
containerKey = ''
try:
playQueueID = self.regex.findall(containerKey)[0]
except IndexError:
playQueueID = ''
# We need to tell service.py
queue.put({
'action': 'playlist',
'data': params
})
subMgr.lastkey = params['key']
subMgr.containerKey = containerKey
subMgr.playQueueID = playQueueID
subMgr.server = server.get('server', 'localhost')
subMgr.port = port
subMgr.protocol = protocol
subMgr.notify()
elif request_path == "player/playback/play":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.PlayPause",
{"playerid": playerid, "play": True})
subMgr.notify()
elif request_path == "player/playback/pause":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.PlayPause",
{"playerid": playerid, "play": False})
subMgr.notify()
elif request_path == "player/playback/stop":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Stop", {"playerid": playerid})
subMgr.notify()
elif request_path == "player/playback/seekTo":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": millisToTime(
params.get('offset', 0))})
subMgr.notify()
elif request_path == "player/playback/stepForward":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": "smallforward"})
subMgr.notify()
elif request_path == "player/playback/stepBack":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": "smallbackward"})
subMgr.notify()
elif request_path == "player/playback/skipNext":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.GoTo",
{"playerid": playerid,
"to": "next"})
subMgr.notify()
elif request_path == "player/playback/skipPrevious":
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.GoTo",
{"playerid": playerid,
"to": "previous"})
subMgr.notify()
elif request_path == "player/playback/skipTo":
js.skipTo(params.get('key').rsplit('/', 1)[1],
params.get('type'))
subMgr.notify()
elif request_path == "player/navigation/moveUp":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Up")
elif request_path == "player/navigation/moveDown":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Down")
elif request_path == "player/navigation/moveLeft":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Left")
elif request_path == "player/navigation/moveRight":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Right")
elif request_path == "player/navigation/select":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Select")
elif request_path == "player/navigation/home":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Home")
elif request_path == "player/navigation/back":
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Back")
else:
log.error('Unknown request path: %s' % request_path)
# Throw it to companion.py
process_command(request_path, params, self.server.queue)
self.response(getOKMsg(), js.getPlexHeaders())
subMgr.notify()
except:
log.error('Error encountered. Traceback:')
import traceback

View file

@ -57,23 +57,22 @@ class plexgdm:
self._discovery_is_running = False
self._registration_is_running = False
self.discovery_complete = False
self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl
def clientDetails(self, options):
self.client_data = (
"Content-Type: plex/media-player\r\n"
"Resource-Identifier: %s\r\n"
"Name: %s\r\n"
"Port: %s\r\n"
"Product: %s\r\n"
"Version: %s\r\n"
"Protocol: plex\r\n"
"Protocol-Version: 1\r\n"
"Content-Type: plex/media-player\n"
"Resource-Identifier: %s\n"
"Name: %s\n"
"Port: %s\n"
"Product: %s\n"
"Version: %s\n"
"Protocol: plex\n"
"Protocol-Version: 1\n"
"Protocol-Capabilities: timeline,playback,navigation,"
"playqueues\r\n"
"Device-Class: HTPC"
"playqueues\n"
"Device-Class: HTPC\n"
) % (
options['uuid'],
options['client_name'],
@ -86,10 +85,25 @@ class plexgdm:
def getClientDetails(self):
return self.client_data
def register_as_client(self):
"""
Registers PKC's Plex Companion to the PMS
"""
try:
log.debug("Sending registration data: HELLO %s\n%s"
% (self.client_header, self.client_data))
self.update_sock.sendto("HELLO %s\n%s"
% (self.client_header, self.client_data),
self.client_register_group)
log.debug('(Re-)registering PKC Plex Companion successful')
except:
log.error("Unable to send registration message")
def client_update(self):
update_sock = socket.socket(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
self.update_sock = socket.socket(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
update_sock = self.update_sock
# Set socket reuse, may not work on all OSs.
try:
@ -129,16 +143,9 @@ class plexgdm:
self._multicast_address) +
socket.inet_aton('0.0.0.0'))
update_sock.setblocking(0)
log.debug("Sending registration data: HELLO %s\r\n%s"
% (self.client_header, self.client_data))
# Send initial client registration
try:
update_sock.sendto("HELLO %s\r\n%s"
% (self.client_header, self.client_data),
self.client_register_group)
except:
log.error("Unable to send registration message")
self.register_as_client()
# Now, listen format client discovery reguests and respond.
while self._registration_is_running:
@ -153,7 +160,7 @@ class plexgdm:
log.debug("Detected client discovery request from %s. "
" Replying" % str(addr))
try:
update_sock.sendto("HTTP/1.0 200 OK\r\n%s"
update_sock.sendto("HTTP/1.0 200 OK\n%s"
% self.client_data,
addr)
except:
@ -165,10 +172,10 @@ class plexgdm:
log.info("Client Update loop stopped")
# When we are finished, then send a final goodbye message to
# deregister cleanly.
log.debug("Sending registration data: BYE %s\r\n%s"
log.debug("Sending registration data: BYE %s\n%s"
% (self.client_header, self.client_data))
try:
update_sock.sendto("BYE %s\r\n%s"
update_sock.sendto("BYE %s\n%s"
% (self.client_header, self.client_data),
self.client_register_group)
except:
@ -176,41 +183,41 @@ class plexgdm:
self.client_registered = False
def check_client_registration(self):
if not self.client_registered:
log.debug('Client has not been marked as registered')
return False
if not self.server_list:
log.info("Server list is empty. Unable to check")
return False
for server in self.server_list:
if server['uuid'] == window('plex_machineIdentifier'):
media_server = server['server']
media_port = server['port']
scheme = server['protocol']
break
else:
log.info("Did not find our server!")
return False
if self.client_registered and self.discovery_complete:
if not self.server_list:
log.info("Server list is empty. Unable to check")
return False
try:
for server in self.server_list:
if server['uuid'] == window('plex_machineIdentifier'):
media_server = server['server']
media_port = server['port']
scheme = server['protocol']
break
else:
log.info("Did not find our server!")
return False
log.debug("Checking server [%s] on port [%s]"
% (media_server, media_port))
client_result = self.download(
'%s://%s:%s/clients' % (scheme, media_server, media_port))
registered = False
for client in client_result:
if (client.attrib.get('machineIdentifier') ==
self.client_id):
registered = True
if registered:
log.debug("Client registration successful. "
"Client data is: %s" % client_result)
return True
else:
log.info("Client registration not found. "
"Client data is: %s" % client_result)
except:
log.error("Unable to check status")
pass
log.debug("Checking server [%s] on port [%s]"
% (media_server, media_port))
xml = self.download(
'%s://%s:%s/clients' % (scheme, media_server, media_port))
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not download clients for %s' % media_server)
return False
registered = False
for client in xml:
if (client.attrib.get('machineIdentifier') ==
self.client_id):
registered = True
if registered:
return True
else:
log.info("Client registration not found. "
"Client data is: %s" % xml)
return False
def getServerList(self):

View file

@ -26,7 +26,7 @@ def getSettings():
options['gdm_debug'] = settings('companionGDMDebugging')
options['gdm_debug'] = True if options['gdm_debug'] == 'true' else False
options['client_name'] = settings('deviceName')
options['client_name'] = v.DEVICENAME
# XBMC web server options
options['webserver_enabled'] = (getGUI('webserver') == "true")

View file

@ -71,7 +71,7 @@ class SubscriptionManager:
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 += "\r\n</MediaContainer>"
msg += "\n</MediaContainer>"
return msg
def getTimelineXML(self, playerid, ptype):
@ -84,7 +84,7 @@ class SubscriptionManager:
else:
state = "stopped"
time = 0
ret = "\r\n"+' <Timeline state="%s" time="%s" type="%s"' % (state, time, ptype)
ret = "\n"+' <Timeline state="%s" time="%s" type="%s"' % (state, time, ptype)
if playerid is None:
ret += ' seekRange="0-0"'
ret += ' />'
@ -312,7 +312,7 @@ class Subscriber:
else:
self.navlocationsent = True
msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg)
log.debug("sending xml to subscriber %s: %s" % (self.tostr(), 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))

View file

@ -133,8 +133,7 @@ def dialog(typus, *args, **kwargs):
'{ipaddress}': xbmcgui.INPUT_IPADDRESS,
'{password}': xbmcgui.INPUT_PASSWORD
}
for key, value in types.iteritems():
kwargs['type'] = kwargs['type'].replace(key, value)
kwargs['type'] = types[kwargs['type']]
if "heading" in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{plex}",
language(29999))

View file

@ -46,13 +46,18 @@ elif xbmc.getCondVisibility('system.platform.android'):
else:
PLATFORM = "Unknown"
if _ADDON.getSetting('deviceNameOpt') == "false":
# Use Kodi's deviceName
DEVICENAME = tryDecode(xbmc.getInfoLabel('System.FriendlyName'))
else:
DEVICENAME = tryDecode(_ADDON.getSetting('deviceName'))
DEVICENAME = DEVICENAME.replace("\"", "_")
DEVICENAME = DEVICENAME.replace("/", "_")
DEVICENAME = tryDecode(_ADDON.getSetting('deviceName'))
DEVICENAME = DEVICENAME.replace(":", "")
DEVICENAME = DEVICENAME.replace("/", "-")
DEVICENAME = DEVICENAME.replace("\\", "-")
DEVICENAME = DEVICENAME.replace("<", "")
DEVICENAME = DEVICENAME.replace(">", "")
DEVICENAME = DEVICENAME.replace("*", "")
DEVICENAME = DEVICENAME.replace("?", "")
DEVICENAME = DEVICENAME.replace('|', "")
DEVICENAME = DEVICENAME.replace('(', "")
DEVICENAME = DEVICENAME.replace(')', "")
DEVICENAME = DEVICENAME.strip()
# Database paths
_DB_VIDEO_VERSION = {
@ -248,3 +253,16 @@ KODI_SUPPORTED_IMAGES = (
'.pcx',
'.tga'
)
# Translation table from Alexa websocket commands to Plex Companion
ALEXA_TO_COMPANION = {
'queryKey': 'key',
'queryOffset': 'offset',
'queryMachineIdentifier': 'machineIdentifier',
'queryProtocol': 'protocol',
'queryAddress': 'address',
'queryPort': 'port',
'queryContainerKey': 'containerKey',
'queryToken': 'token',
}

View file

@ -4,6 +4,7 @@
import logging
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
@ -12,6 +13,7 @@ from xbmc import sleep
from utils import window, settings, ThreadMethodsAdditionalSuspend, \
ThreadMethods
from companion import process_command
###############################################################################
@ -29,10 +31,151 @@ class WebSocket(Thread):
if callback is not None:
self.mgr = callback
self.ws = None
# Communication with librarysync
self.queue = Queue()
Thread.__init__(self)
def process(self, opcode, message):
raise NotImplementedError
def receive(self, ws):
# Not connected yet
if ws is None:
raise websocket.WebSocketConnectionClosedException
frame = ws.recv_frame()
if not frame:
raise websocket.WebSocketException("Not a valid frame %s" % frame)
elif frame.opcode in self.opcode_data:
return frame.opcode, frame.data
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
ws.send_close()
return frame.opcode, None
elif frame.opcode == websocket.ABNF.OPCODE_PING:
ws.pong("Hi!")
return None, None
def getUri(self):
raise NotImplementedError
def run(self):
log.info("----===## Starting %s ##===----" % self.__class__.__name__)
counter = 0
handshake_counter = 0
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
while not threadStopped():
# In the event the server goes offline
while threadSuspended():
# Set in service.py
if self.ws is not None:
try:
self.ws.shutdown()
except:
pass
self.ws = None
if threadStopped():
# Abort was requested while waiting. We should exit
log.info("##===---- %s Stopped ----===##"
% self.__class__.__name__)
return
sleep(1000)
try:
self.process(*self.receive(self.ws))
except websocket.WebSocketTimeoutException:
# No worries if read timed out
pass
except websocket.WebSocketConnectionClosedException:
log.info("Connection closed, (re)connecting")
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
self.ws = websocket.create_connection(
uri,
timeout=1,
sslopt=sslopt,
enable_multithread=True)
except IOError:
# Server is probably offline
log.info("Error connecting")
self.ws = None
counter += 1
if counter > 3:
counter = 0
self.IOError_response()
sleep(1000)
except websocket.WebSocketTimeoutException:
log.info("timeout while connecting, trying again")
self.ws = None
sleep(1000)
except websocket.WebSocketException as e:
log.info('WebSocketException: %s' % e)
if 'Handshake Status 401' in e.args:
handshake_counter += 1
if handshake_counter >= 5:
log.info('Error in handshake detected. Stopping '
'%s now' % self.__class__.__name__)
break
self.ws = None
sleep(1000)
except Exception as e:
log.error("Unknown exception encountered in connecting: %s"
% e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
self.ws = None
sleep(1000)
else:
counter = 0
handshake_counter = 0
except Exception as e:
log.error("Unknown exception encountered: %s" % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
try:
self.ws.shutdown()
except:
pass
self.ws = None
log.info("##===---- %s Stopped ----===##" % self.__class__.__name__)
def stopThread(self):
"""
Overwrite this method from ThreadMethods to close websockets
"""
log.info("Stopping %s thread." % self.__class__.__name__)
self._threadStopped = True
try:
self.ws.shutdown()
except:
pass
class PMS_Websocket(WebSocket):
"""
Websocket connection with the PMS for Plex Companion
"""
# Communication with librarysync
queue = Queue()
def getUri(self):
server = window('pms_server')
# Need to use plex.tv token, if any. NOT user token
token = window('plex_token')
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
else:
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
if token:
uri += '?X-Plex-Token=%s' % token
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt
def process(self, opcode, message):
if opcode not in self.opcode_data:
return False
@ -62,131 +205,58 @@ class WebSocket(Thread):
self.queue.put(message)
return True
def receive(self, ws):
# Not connected yet
if ws is None:
raise websocket.WebSocketConnectionClosedException
def IOError_response(self):
log.warn("Repeatedly could not connect to PMS, "
"declaring the connection dead")
window('plex_online', value='false')
frame = ws.recv_frame()
if not frame:
raise websocket.WebSocketException("Not a valid frame %s" % frame)
elif frame.opcode in self.opcode_data:
return frame.opcode, frame.data
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
ws.send_close()
return frame.opcode, None
elif frame.opcode == websocket.ABNF.OPCODE_PING:
ws.pong("Hi!")
return None, None
class Alexa_Websocket(WebSocket):
"""
Websocket connection to talk to Amazon Alexa
"""
def getUri(self):
server = window('pms_server')
# Need to use plex.tv token, if any. NOT user token
token = window('plex_token')
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
else:
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
if token:
uri += '?X-Plex-Token=%s' % token
self.plex_client_Id = window('plex_client_Id')
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (window('currUserId'),
self.plex_client_Id,
window('plex_token')))
sslopt = {}
if settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
log.debug("Uri: %s, sslopt: %s" % (uri, sslopt))
return uri, sslopt
def run(self):
log.info("----===## Starting WebSocketClient ##===----")
counter = 0
handshake_counter = 0
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
while not threadStopped():
# In the event the server goes offline
while threadSuspended():
# Set in service.py
if self.ws is not None:
try:
self.ws.shutdown()
except:
pass
self.ws = None
if threadStopped():
# Abort was requested while waiting. We should exit
log.info("##===---- WebSocketClient Stopped ----===##")
return
sleep(1000)
try:
self.process(*self.receive(self.ws))
except websocket.WebSocketTimeoutException:
# No worries if read timed out
pass
except websocket.WebSocketConnectionClosedException:
log.info("Connection closed, (re)connecting")
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
self.ws = websocket.create_connection(
uri,
timeout=1,
sslopt=sslopt,
enable_multithread=True)
except IOError:
# Server is probably offline
log.info("Error connecting")
self.ws = None
counter += 1
if counter > 3:
log.warn("Repeatedly could not connect to PMS, "
"declaring the connection dead")
window('plex_online', value='false')
counter = 0
sleep(1000)
except websocket.WebSocketTimeoutException:
log.info("timeout while connecting, trying again")
self.ws = None
sleep(1000)
except websocket.WebSocketException as e:
log.info('WebSocketException: %s' % e)
if 'Handshake Status 401' in e.args:
handshake_counter += 1
if handshake_counter >= 5:
log.info('Error in handshake detected. Stopping '
'WebSocketClient now')
break
self.ws = None
sleep(1000)
except Exception as e:
log.error("Unknown exception encountered in connecting: %s"
% e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
self.ws = None
sleep(1000)
else:
counter = 0
handshake_counter = 0
except Exception as e:
log.error("Unknown exception encountered: %s" % e)
try:
self.ws.shutdown()
except:
pass
self.ws = None
log.info("##===---- WebSocketClient Stopped ----===##")
def stopThread(self):
"""
Overwrite this method from ThreadMethods to close websockets
"""
log.info("Stopping websocket client thread.")
self._threadStopped = True
def process(self, opcode, message):
if opcode not in self.opcode_data:
return False
log.debug('Received the following message from Alexa:')
log.debug(message)
try:
self.ws.shutdown()
message = etree.fromstring(message)
except Exception as ex:
log.error('Error decoding message from Alexa: %s' % ex)
return False
try:
if message.attrib['command'] == 'processRemoteControlCommand':
message = message[0]
else:
log.error('Unknown Alexa message received')
return False
except:
pass
log.error('Could not parse Alexa message')
return False
process_command(message.attrib['path'][1:],
message.attrib,
queue=self.mgr.plexCompanion.queue)
return True
def IOError_response(self):
pass
def threadSuspended(self):
"""
Overwrite to ignore library sync stuff and allow to check for
plex_restricteduser
"""
return (self._threadSuspended or
window('plex_restricteduser') == 'true' or
not window('plex_token'))

View file

@ -21,13 +21,9 @@
</category>
<category label="Plex">
<setting id="enableContext" type="bool" label="30413" default="true" />
<setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-1,true)" subsetting="true" />
<setting type="lsep" label="plex.tv"/>
<setting id="plex_status" label="39071" type="text" default="Not logged in to plex.tv" enable="false" /><!-- Current plex.tv status: -->
<setting id="plexLogin" label="Plex user:" type="text" default="" enable="false" />
<setting type="sep" text=""/>
<setting id="myplexlogin" label="39025" type="bool" default="true" /> <!-- Log into plex.tv on startup -->
<setting label="39209" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=togglePlexTV)" option="close" />
<setting id="plexhome" label="Plex home in use" type="bool" default="" visible="false" />
@ -36,11 +32,14 @@
<setting type="lsep" label="39008" />
<setting id="plexCompanion" label="39004" type="bool" default="true" />
<setting id="deviceNameOpt" label="30504" type="bool" default="false" subsetting="true" visible="eq(-1,true)" />
<setting id="deviceName" label="30016" type="text" visible="eq(-1,true)" default="Kodi" subsetting="true" />
<setting id="companionPort" label="39005" type="number" default="3005" option="int" visible="eq(-3,true)" subsetting="true" />
<setting id="companionUpdatePort" label="39078" type="number" default="32412" option="int" visible="eq(-4,true)" subsetting="true" />
<setting id="deviceName" label="30016" type="text" visible="eq(-1,true)" default="PlexKodiConnect" subsetting="true" />
<setting id="companionPort" label="39005" type="number" default="3005" option="int" visible="eq(-2,true)" subsetting="true" />
<setting id="companionUpdatePort" label="39078" type="number" default="32412" option="int" visible="eq(-3,true)" subsetting="true" />
<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" />
<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"/>

View file

@ -36,7 +36,7 @@ import initialsetup
from kodimonitor import KodiMonitor
from librarysync import LibrarySync
import videonodes
from websocket_client import WebSocket
from websocket_client import PMS_Websocket, Alexa_Websocket
import downloadutils
from playqueue import Playqueue
@ -70,6 +70,7 @@ class Service():
user_running = False
ws_running = False
alexa_running = False
library_running = False
plexCompanion_running = False
playqueue_running = False
@ -148,7 +149,8 @@ class Service():
# Initialize important threads, handing over self for callback purposes
self.user = UserClient(self)
self.ws = WebSocket(self)
self.ws = PMS_Websocket(self)
self.alexa = Alexa_Websocket(self)
self.library = LibrarySync(self)
self.plexCompanion = PlexCompanion(self)
self.playqueue = Playqueue(self)
@ -201,6 +203,11 @@ class Service():
if not self.ws_running:
self.ws_running = True
self.ws.start()
# Start the Alexa thread
if (not self.alexa_running and
settings('enable_alexa') == 'true'):
self.alexa_running = True
self.alexa.start()
# Start the syncing thread
if not self.library_running:
self.library_running = True
@ -326,6 +333,10 @@ class Service():
self.ws.stopThread()
except:
log.warn('Websocket client already shut down')
try:
self.alexa.stopThread()
except:
log.warn('Websocket client already shut down')
try:
self.user.stopThread()
except:
@ -334,14 +345,23 @@ class Service():
downloadutils.DownloadUtils().stopSession()
except:
pass
window('plex_service_started', clear=True)
log.warn("======== STOP %s ========" % v.ADDON_NAME)
# Safety net - Kody starts PKC twice upon first installation!
if window('plex_service_started') == 'true':
exit = True
else:
window('plex_service_started', value='true')
exit = False
# Delay option
delay = int(settings('startupDelay'))
log.warn("Delaying Plex startup by: %s sec..." % delay)
if delay and Monitor().waitForAbort(delay):
if exit:
log.error('PKC service.py already started - exiting this instance')
elif delay and Monitor().waitForAbort(delay):
# Start the service
log.warn("Abort requested while waiting. PKC not started.")
else: