Merge pull request #1632 from croneter/python3-beta

Bump python3 master
This commit is contained in:
croneter 2021-09-24 14:26:15 +02:00 committed by GitHub
commit 6c44b5e392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 417 additions and 209 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="3.4.4" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="3.5.0" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.requests" version="2.22.0+matrix.1" />
@ -91,7 +91,22 @@
<summary lang="ko_KR">Plex를 Kodi에 기본 통합</summary>
<description lang="ko_KR">Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오!</description>
<disclaimer lang="ko_KR">자신의 책임하에 사용</disclaimer>
<news>version 3.4.4:
<news>version 3.5.0:
- versions 3.4.5-3.4.7 for everyone
version 3.4.7 (beta only):
- Tell the PMS if a video's audio stream or potentially subtitle stream has changed. For subtitles, this functionality is broken due to a Kodi bug
- Transcoding: Fix Plex burning-in subtitles when it should not
- Large refactoring of playlist and playqueue code
- Refactor usage of a media part's id
version 3.4.6 (beta only):
- Fix RecursionError if a video lies in a root directory
version 3.4.5 (beta only):
- Implement "Reset resume position" from the Kodi context menu
version 3.4.4:
- Initial compatibility with Kodi 20 Nexus. Keep in mind that development for Kodi Nexus has not even officially reached alpha stage - any issues you encounter are probably caused by that
- version 3.4.3 for everyone

View File

@ -1,3 +1,18 @@
version 3.5.0:
- versions 3.4.5-3.4.7 for everyone
version 3.4.7 (beta only):
- Tell the PMS if a video's audio stream or potentially subtitle stream has changed. For subtitles, this functionality is broken due to a Kodi bug
- Transcoding: Fix Plex burning-in subtitles when it should not
- Large refactoring of playlist and playqueue code
- Refactor usage of a media part's id
version 3.4.6 (beta only):
- Fix RecursionError if a video lies in a root directory
version 3.4.5 (beta only):
- Implement "Reset resume position" from the Kodi context menu
version 3.4.4:
- Initial compatibility with Kodi 20 Nexus. Keep in mind that development for Kodi Nexus has not even officially reached alpha stage - any issues you encounter are probably caused by that
- version 3.4.3 for everyone

View File

@ -4,19 +4,13 @@ import sqlite3
from functools import wraps
from . import variables as v, app
from .exceptions import LockedDatabase
DB_WRITE_ATTEMPTS = 100
DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds
DB_CONNECTION_TIMEOUT = 10
class LockedDatabase(Exception):
"""
Dedicated class to make sure we're not silently catching locked DBs.
"""
pass
def catch_operationalerrors(method):
"""
sqlite.OperationalError is raised immediately if another DB connection

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class PlaylistError(Exception):
"""
Exception for our playlist constructs
"""
pass
class LockedDatabase(Exception):
"""
Dedicated class to make sure we're not silently catching locked DBs.
"""
pass
class SubtitleError(Exception):
"""
Exceptions relating to subtitles
"""
pass
class ProcessingNotDone(Exception):
"""
Exception to detect whether we've completed our sync and did not have to
abort or suspend.
"""
pass

View File

@ -420,6 +420,41 @@ def get_item(playerid):
'properties': ['title', 'file']})['result']['item']
def get_current_audio_stream_index(playerid):
"""
Returns the currently active audio stream index [int]
"""
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['currentaudiostream']})['result']['currentaudiostream']['index']
def get_current_subtitle_stream_index(playerid):
"""
Returns the currently active subtitle stream index [int] or None if there
are no subs
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
JSON reply won't change even though subtitles are changed :-(
"""
try:
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['currentsubtitle', ]})['result']['currentsubtitle']['index']
except KeyError:
pass
def get_subtitle_enabled(playerid):
"""
Returns True if a subtitle is currently enabled, False otherwise.
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
JSON reply won't change even though subtitles are changed :-(
"""
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['subtitleenabled', ]})['result']['subtitleenabled']
def get_player_props(playerid):
"""
Returns a dict for the active Kodi player with the following values:

View File

@ -602,6 +602,22 @@ class KodiVideoDB(common.KodiDBBase):
return
return movie_id, typus
def file_id_from_id(self, kodi_id, kodi_type):
"""
Returns the Kodi file_id for the item with kodi_id and kodi_type or
None
"""
if kodi_type == v.KODI_TYPE_MOVIE:
identifier = 'idMovie'
elif kodi_type == v.KODI_TYPE_EPISODE:
identifier = 'idEpisode'
self.cursor.execute('SELECT idFile FROM %s WHERE %s = ? LIMIT 1'
% (kodi_type, identifier), (kodi_id, ))
try:
return self.cursor.fetchone()[0]
except TypeError:
pass
def get_resume(self, file_id):
"""
Returns the first resume point in seconds (int) if found, else None for

View File

@ -13,11 +13,13 @@ import xbmc
from .plex_api import API
from .plex_db import PlexDB
from .kodi_db import KodiVideoDB
from . import kodi_db
from .downloadutils import DownloadUtils as DU
from . import utils, timing, plex_functions as PF
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
from . import backgroundthread, app, variables as v
from . import exceptions
LOG = getLogger('PLEX.kodimonitor')
@ -28,7 +30,7 @@ class KodiMonitor(xbmc.Monitor):
"""
def __init__(self):
self._already_slept = False
self._switch_to_plex_streams = None
self._switched_to_plex_streams = True
xbmc.Monitor.__init__(self)
for playerid in app.PLAYSTATE.player_states:
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
@ -66,7 +68,7 @@ class KodiMonitor(xbmc.Monitor):
self.PlayBackStart(data)
elif method == 'Player.OnAVChange':
with app.APP.lock_playqueues:
self.on_av_change()
self._on_av_change(data)
elif method == "Player.OnStop":
with app.APP.lock_playqueues:
_playback_cleanup(ended=data.get('end'))
@ -85,7 +87,8 @@ class KodiMonitor(xbmc.Monitor):
with app.APP.lock_playqueues:
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
_videolibrary_onupdate(data)
with app.APP.lock_playqueues:
_videolibrary_onupdate(data)
elif method == "VideoLibrary.OnRemove":
pass
elif method == "System.OnSleep":
@ -178,7 +181,7 @@ class KodiMonitor(xbmc.Monitor):
try:
for i, item in enumerate(items):
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.info('Could not build Plex playlist for: %s', items)
def _json_item(self, playerid):
@ -315,7 +318,7 @@ class KodiMonitor(xbmc.Monitor):
return
try:
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.info('Could not initialize the Plex playlist')
return
item.file = path
@ -340,8 +343,7 @@ class KodiMonitor(xbmc.Monitor):
container_key = '/library/metadata/%s' % plex_id
# Mechanik for Plex skip intro feature
if utils.settings('enableSkipIntro') == 'true':
api = API(item.xml)
status['intro_markers'] = api.intro_markers()
status['intro_markers'] = item.api.intro_markers()
# Remember the currently playing item
app.PLAYSTATE.item = item
# Remember that this player has been active
@ -356,60 +358,96 @@ class KodiMonitor(xbmc.Monitor):
status['plex_type'] = plex_type
status['playmethod'] = item.playmethod
status['playcount'] = item.playcount
try:
status['external_player'] = app.APP.player.isExternalPlayer() == 1
except AttributeError:
# Kodi version < 17
pass
status['external_player'] = app.APP.player.isExternalPlayer() == 1
LOG.debug('Set the player state: %s', status)
# Workaround for the Kodi add-on Up Next
if not app.SYNC.direct_paths:
_notify_upnext(item)
self._switch_to_plex_streams = item
self._switched_to_plex_streams = False
def on_av_change(self):
def _on_av_change(self, data):
"""
Will be called when Kodi has a video, audio or subtitle stream. Also
happens when the stream changes.
Example data as returned by Kodi:
{'item': {'id': 5, 'type': 'movie'},
'player': {'playerid': 1, 'speed': 1}}
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE!
Kodi subs will never change. Also see json_rpc.py
"""
if self._switch_to_plex_streams is not None:
self.switch_to_plex_streams(self._switch_to_plex_streams)
self._switch_to_plex_streams = None
playerid = data['player']['playerid']
if not playerid == v.KODI_VIDEO_PLAYER_ID:
# We're just messing with Kodi's videoplayer
return
if not self._switched_to_plex_streams:
# We need to switch to the Plex streams ONCE upon playback start
# after onavchange has been fired
self.switch_to_plex_streams()
self._switched_to_plex_streams = True
else:
item = app.PLAYSTATE.item
if item is None:
# Player might've quit
return
kodi_audio_stream = js.get_current_audio_stream_index(playerid)
sub_enabled = js.get_subtitle_enabled(playerid)
kodi_sub_stream = js.get_current_subtitle_stream_index(playerid)
# Audio
if kodi_audio_stream != item.current_kodi_audio_stream:
item.on_kodi_audio_stream_change(kodi_audio_stream)
# Subtitles - CURRENTLY BROKEN ON THE KODI SIDE!
# current_kodi_sub_stream may also be zero
subs_off = (None, False)
if ((sub_enabled and item.current_kodi_sub_stream in subs_off)
or (not sub_enabled and item.current_kodi_sub_stream not in subs_off)
or (kodi_sub_stream is not None
and kodi_sub_stream != item.current_kodi_sub_stream)):
item.on_kodi_subtitle_stream_change(kodi_sub_stream,
sub_enabled)
@staticmethod
def switch_to_plex_streams(item):
def switch_to_plex_streams():
"""
Override Kodi audio and subtitle streams with Plex PMS' selection
"""
item = app.PLAYSTATE.item
if item is None:
# Player might've quit
return
for typus in ('audio', 'subtitle'):
try:
plex_index, language_tag = item.active_plex_stream_index(typus)
except TypeError:
if typus == 'subtitle':
LOG.info('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles')
app.APP.player.showSubtitles(False)
LOG.debug('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles')
app.APP.player.showSubtitles(False)
item.current_kodi_sub_stream = False
continue
LOG.info('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s',
typus, plex_index, language_tag)
kodi_index = item.kodi_stream_index(plex_index,
typus)
LOG.debug('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s',
typus, plex_index, language_tag)
kodi_index = item.kodi_stream_index(plex_index, typus)
if kodi_index is None:
LOG.info('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi '
'index', typus, typus, plex_index)
LOG.debug('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi'
' index', typus, typus, plex_index)
else:
LOG.info('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index)
LOG.debug('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index)
# If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors
if typus == 'subtitle':
app.APP.player.setSubtitleStream(kodi_index)
else:
if typus == 'audio':
app.APP.player.setAudioStream(kodi_index)
else:
app.APP.player.setSubtitleStream(kodi_index)
if typus == 'audio':
item.current_kodi_audio_stream = kodi_index
else:
item.current_kodi_sub_stream = kodi_index
def _playback_cleanup(ended=False):
@ -585,11 +623,10 @@ def _next_episode(current_api):
current_api.grandparent_title())
return
try:
next_api = API(xml[counter + 1])
return API(xml[counter + 1])
except IndexError:
# Was the last episode
return
return next_api
pass
def _complete_artwork_keys(info):
@ -615,7 +652,7 @@ def _notify_upnext(item):
"""
if not item.plex_type == v.PLEX_TYPE_EPISODE:
return
this_api = API(item.xml)
this_api = item.api
next_api = _next_episode(this_api)
if next_api is None:
return
@ -651,17 +688,34 @@ def _videolibrary_onupdate(data):
A specific Kodi library item has been updated. This seems to happen if the
user marks an item as watched/unwatched or if playback of the item just
stopped
2 kinds of messages possible, e.g.
Method: VideoLibrary.OnUpdate Data: ("Reset resume position" and also
fired just after stopping playback - BEFORE OnStop fires)
{'id': 1, 'type': 'movie'}
Method: VideoLibrary.OnUpdate Data: ("Mark as watched")
{'item': {'id': 1, 'type': 'movie'}, 'playcount': 1}
"""
playcount = data.get('playcount')
item = data.get('item')
if playcount is None or item is None:
return
item = data.get('item') if 'item' in data else data
try:
kodi_id = item['id']
kodi_type = item['type']
except (KeyError, TypeError):
LOG.info("Item is invalid for playstate update.")
LOG.debug("Item is invalid for a Plex playstate update")
return
playcount = data.get('playcount')
if playcount is None:
# "Reset resume position"
# Kodi might set as watched or unwatched!
with KodiVideoDB(lock=False) as kodidb:
file_id = kodidb.file_id_from_id(kodi_id, kodi_type)
if file_id is None:
return
if kodidb.get_resume(file_id):
# We do have an existing bookmark entry - not toggling to
# either watched or unwatched on the Plex side
return
playcount = kodidb.get_playcount(file_id) or 0
if app.PLAYSTATE.item and kodi_id == app.PLAYSTATE.item.kodi_id and \
kodi_type == app.PLAYSTATE.item.kodi_type:
# Kodi updates an item immediately after playback. Hence we do NOT

View File

@ -5,6 +5,7 @@ from . import additional_metadata_tmdb
from ..plex_db import PlexDB
from .. import backgroundthread, utils
from .. import variables as v, app
from ..exceptions import ProcessingNotDone
logger = getLogger('PLEX.sync.metadata')
@ -22,12 +23,6 @@ SUPPORTED_METADATA = {
}
class ProcessingNotDone(Exception):
"""Exception to detect whether we've completed our sync and did not have to
abort or suspend."""
pass
def processing_is_activated(item_getter):
"""Checks the PKC settings whether processing is even activated."""
if item_getter == 'missing_fanart':

View File

@ -31,10 +31,8 @@ def append_os_sep(path):
Appends either a '\\' or '/' - IRRELEVANT of the host OS!! (os.path.join is
dependant on the host OS)
"""
if '/' in path:
return path + '/'
else:
return path + '\\'
separator = '/' if '/' in path else '\\'
return path if path.endswith(separator) else path + separator
def translate_path(path):

View File

@ -15,6 +15,7 @@ from .kodi_db import KodiVideoDB
from . import plex_functions as PF, playlist_func as PL, playqueue as PQ
from . import json_rpc as js, variables as v, utils, transfer
from . import playback_decision, app
from . import exceptions
###############################################################################
LOG = getLogger('PLEX.playback')
@ -191,7 +192,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos, resume):
# Special case - we already got a filled Kodi playqueue
try:
_init_existing_kodi_playlist(playqueue, pos)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed')
_ensure_resolve(abort=True)
return
@ -311,7 +312,7 @@ def _init_existing_kodi_playlist(playqueue, pos):
kodi_items = js.playlist_get_items(playqueue.playlistid)
if not kodi_items:
LOG.error('No Kodi items returned')
raise PL.PlaylistError('No Kodi items returned')
raise exceptions.PlaylistError('No Kodi items returned')
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
item.force_transcode = app.PLAYSTATE.force_transcode
# playqueue.py will add the rest - this will likely put the PMS under
@ -443,27 +444,26 @@ def _conclude_playback(playqueue, pos):
"""
LOG.debug('Concluding playback for playqueue position %s', pos)
item = playqueue.items[pos]
api = API(item.xml)
if api.mediastream_number() is None:
if item.api.mediastream_number() is None:
# E.g. user could choose between several media streams and cancelled
LOG.debug('Did not get a mediastream_number')
_ensure_resolve()
return
api.part = item.part or 0
playback_decision.set_pkc_playmethod(api, item)
if not playback_decision.audio_subtitle_prefs(api, item):
item.api.part = item.part or 0
playback_decision.set_pkc_playmethod(item.api, item)
if not playback_decision.audio_subtitle_prefs(item.api, item):
LOG.info('Did not set audio subtitle prefs, aborting silently')
_ensure_resolve()
return
playback_decision.set_playurl(api, item)
playback_decision.set_playurl(item.api, item)
if not item.file:
LOG.info('Did not get a playurl, aborting playback silently')
_ensure_resolve()
return
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
listitem = item.api.listitem(listitem=transfer.PKCListItem, resume=False)
listitem.setPath(item.file)
if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH:
listitem.setSubtitles(api.cache_external_subs())
listitem.setSubtitles(item.api.cache_external_subs())
transfer.send(listitem)
LOG.debug('Done concluding playback')

View File

@ -328,19 +328,12 @@ def audio_subtitle_prefs(api, item):
Returns None if user cancelled or we need to abort, True otherwise
"""
# Set media and part where we're at
if (api.mediastream is None and
api.mediastream_number() is None):
if api.mediastream is None and api.mediastream_number() is None:
return
try:
mediastreams = api.plex_media_streams()
except (TypeError, IndexError):
LOG.error('Could not get media %s, part %s',
api.mediastream, api.part)
return
part_id = mediastreams.attrib['id']
if item.playmethod != v.PLAYBACK_METHOD_TRANSCODE:
return True
return setup_transcoding_audio_subtitle_prefs(mediastreams, part_id)
return setup_transcoding_audio_subtitle_prefs(api.plex_media_streams(),
api.part_id())
def setup_transcoding_audio_subtitle_prefs(mediastreams, part_id):
@ -425,7 +418,8 @@ def setup_transcoding_audio_subtitle_prefs(mediastreams, part_id):
action_type='PUT',
parameters=args)
select_subs_index = ''
# Zero telling the PMS to deactivate subs altogether
select_subs_index = 0
if sub_num == 1:
# Note: we DO need to tell the PMS that we DONT want any sub
# Otherwise, the PMS might pick-up the last one
@ -444,15 +438,8 @@ def setup_transcoding_audio_subtitle_prefs(mediastreams, part_id):
LOG.info('User chose to not burn-in any subtitles')
else:
LOG.info('User chose to burn-in subtitle %s: %s',
select_subs_index,
subtitle_streams[resp])
select_subs_index, subtitle_streams[resp])
select_subs_index = subtitle_streams_list[resp - 1]
# Now prep the PMS for our choice
args = {
'subtitleStreamID': select_subs_index,
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
PF.change_subtitle(select_subs_index, part_id)
return True

View File

@ -11,22 +11,17 @@ from . import plex_functions as PF
from .kodi_db import kodiid_from_filename
from .downloadutils import DownloadUtils as DU
from . import utils
from .utils import cast
from . import json_rpc as js
from . import variables as v
from . import app
from .exceptions import PlaylistError
from .subtitles import accessible_plex_subtitles
LOG = getLogger('PLEX.playlist_func')
class PlaylistError(Exception):
"""
Exception for our playlist constructs
"""
pass
class Playqueue_Object(object):
"""
PKC object to represent PMS playQueues and Kodi playlist for queueing
@ -150,13 +145,14 @@ class PlaylistItem(object):
file = None [str] Path to the item's file. STRING!!
uri = None [str] PMS path to item; will be auto-set with plex_id
guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
api = None [API] API of xml 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
playcount = None [int] how many times the item has already been played
offset = None [int] the item's view offset UPON START in Plex time
part = 0 [int] part number if Plex video consists of mult. parts
force_transcode [bool] defaults to False
"""
def __init__(self):
self.id = None
self._plex_id = None
@ -166,7 +162,7 @@ class PlaylistItem(object):
self.file = None
self._uri = None
self.guid = None
self.xml = None
self.api = None
self.playmethod = None
self.playcount = None
self.offset = None
@ -180,6 +176,16 @@ class PlaylistItem(object):
# False: do NOT resume, don't ask user
# True: do resume, don't ask user
self.resume = None
# Get the Plex audio and subtitle streams in the same order as Kodi
# uses them (Kodi uses indexes to activate them, not ids like Plex)
self._streams_have_been_processed = False
self._audio_streams = None
self._subtitle_streams = None
# Which Kodi streams are active?
self.current_kodi_audio_stream = None
# False means "deactivated", None means "we do not have a Kodi
# equivalent for this Plex subtitle"
self.current_kodi_sub_stream = None
@property
def plex_id(self):
@ -195,6 +201,18 @@ class PlaylistItem(object):
def uri(self):
return self._uri
@property
def audio_streams(self):
if not self._streams_have_been_processed:
self._process_streams()
return self._audio_streams
@property
def subtitle_streams(self):
if not self._streams_have_been_processed:
self._process_streams()
return self._subtitle_streams
def __repr__(self):
return ("{{"
"'id': {self.id}, "
@ -211,85 +229,100 @@ class PlaylistItem(object):
"'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self))
def _process_streams(self):
"""
Builds audio and subtitle streams and enables matching between Plex
and Kodi using self.audio_streams and self.subtitle_streams
"""
# The playqueue response from the PMS does not contain a stream filename
# thanks Plex
self._subtitle_streams = accessible_plex_subtitles(
self.playmethod,
self.file,
self.api.plex_media_streams())
# Audio streams are much easier - they're always available and sorted
# the same in Kodi and Plex
self._audio_streams = [x for x in self.api.plex_media_streams()
if x.get('streamType') == '2']
self._streams_have_been_processed = True
def _get_iterator(self, stream_type):
if stream_type == 'audio':
return self.audio_streams
elif stream_type == 'subtitle':
return self.subtitle_streams
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.
index [int].
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
count = 0
if kodi_stream_index == -1:
# Kodi telling us "it's the last one"
iterator = list(reversed(self.xml[0][self.part]))
kodi_stream_index = 0
else:
iterator = self.xml[0][self.part]
# Kodi indexes differently than Plex
for stream in iterator:
if (stream.get('streamType') == stream_type and
'key' in stream.attrib):
if count == kodi_stream_index:
return stream.get('id')
count += 1
for stream in iterator:
if (stream.get('streamType') == stream_type and
'key' not in stream.attrib):
if count == kodi_stream_index:
return stream.get('id')
count += 1
if stream_type == 'audio':
return int(self.audio_streams[kodi_stream_index].get('id'))
elif stream_type == 'subtitle':
try:
return int(self.subtitle_streams[kodi_stream_index].get('id'))
except (IndexError, TypeError):
pass
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.
Pass in the plex_stream_index [int] in order to receive the Kodi stream
index [int].
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
if plex_stream_index is None:
return
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
count = 0
streams = self.sorted_accessible_plex_subtitles(stream_type)
for stream in streams:
if utils.cast(int, stream.get('id')) == plex_stream_index:
return count
count += 1
for i, stream in enumerate(self._get_iterator(stream_type)):
if cast(int, stream.get('id')) == plex_stream_index:
return i
def active_plex_stream_index(self, stream_type):
"""
Returns the following tuple for the active stream on the Plex side:
(id [int], languageTag [str])
(Plex stream id [int], languageTag [str] or None)
Returns None if no stream has been selected
"""
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
for stream in self.xml[0][self.part]:
if stream.get('streamType') == stream_type \
and stream.get('selected') == '1':
return (utils.cast(int, stream.get('id')),
stream.get('languageTag'))
for i, stream in enumerate(self._get_iterator(stream_type)):
if stream.get('selected') == '1':
return (int(stream.get('id')), stream.get('languageTag'))
def sorted_accessible_plex_subtitles(self, stream_type):
def on_kodi_subtitle_stream_change(self, kodi_stream_index, subs_enabled):
"""
Returns only the subtitles that Kodi can access when PKC Direct Paths
are used; i.e. Kodi has access to a video's directory.
NOT supported: additional subtitles downloaded using the Plex interface
Call this method if Kodi changed its subtitle and you want Plex to
know.
"""
# The playqueue response from the PMS does not contain a stream filename
# thanks Plex
if stream_type == '3':
streams = accessible_plex_subtitles(self.playmethod,
self.file,
self.xml[0][self.part])
if subs_enabled:
try:
plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
except (IndexError, TypeError):
LOG.debug('Kodi subtitle change detected to a sub %s that is '
'NOT available on the Plex side', kodi_stream_index)
self.current_kodi_sub_stream = None
return
LOG.debug('Kodi subtitle change detected: telling Plex about '
'switch to index %s, Plex stream id %s',
kodi_stream_index, plex_stream_index)
self.current_kodi_sub_stream = kodi_stream_index
else:
streams = [x for x in self.xml[0][self.part]
if x.get('streamType') == stream_type]
return streams
plex_stream_index = 0
LOG.debug('Kodi subtitle has been deactivated, telling Plex')
self.current_kodi_sub_stream = False
PF.change_subtitle(plex_stream_index, self.api.part_id())
def on_kodi_audio_stream_change(self, kodi_stream_index):
"""
Call this method if Kodi changed its audio stream and you want Plex to
know. kodi_stream_index [int]
"""
plex_stream_index = int(self.audio_streams[kodi_stream_index].get('id'))
LOG.debug('Changing Plex audio stream to %s, Kodi index %s',
plex_stream_index, kodi_stream_index)
PF.change_audio_stream(plex_stream_index, self.api.part_id())
self.current_kodi_audio_stream = kodi_stream_index
def playlist_item_from_kodi(kodi_item):
@ -315,7 +348,7 @@ def playlist_item_from_kodi(kodi_item):
except IndexError:
query = ''
query = dict(utils.parse_qsl(query))
item.plex_id = utils.cast(int, query.get('plex_id'))
item.plex_id = cast(int, query.get('plex_id'))
item.plex_type = query.get('itemType')
LOG.debug('Made playlist item from Kodi: %s', item)
return item
@ -413,14 +446,15 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
item.guid = api.guid_html_escaped()
item.playcount = api.viewcount()
item.offset = api.resume_point()
item.xml = xml_video_element
item.api = api
LOG.debug('Created new playlist item from xml: %s', item)
return item
def _get_playListVersion_from_xml(playlist, xml):
def _update_playlist_version(playlist, xml):
"""
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
Takes a PMS xml (one level above the xml-depth where we're usually applying
API()) as input to overwrite the playlist version (e.g. Plex
playQueueVersion).
Raises PlaylistError if unsuccessful
@ -441,18 +475,13 @@ def get_playlist_details_from_xml(playlist, xml):
"""
if xml is None:
raise PlaylistError('No playlist received for playlist %s' % playlist)
playlist.id = utils.cast(int,
xml.get('%sID' % playlist.kind))
playlist.version = utils.cast(int,
xml.get('%sVersion' % playlist.kind))
playlist.shuffled = utils.cast(int,
xml.get('%sShuffled' % playlist.kind))
playlist.selectedItemID = utils.cast(int,
xml.get('%sSelectedItemID'
% playlist.kind))
playlist.selectedItemOffset = utils.cast(int,
xml.get('%sSelectedItemOffset'
% playlist.kind))
playlist.id = cast(int, xml.get('%sID' % playlist.kind))
playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
% playlist.kind))
playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
% playlist.kind))
LOG.debug('Updated playlist from xml: %s', playlist)
@ -603,7 +632,7 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
raise PlaylistError('Could not add item %s to playlist %s'
% (kodi_item, playlist))
api = API(xml[-1])
item.xml = xml[-1]
item.api = api
item.id = api.item_id()
item.guid = api.guid_html_escaped()
item.offset = api.resume_point()
@ -611,7 +640,7 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
playlist.items.append(item)
if pos == len(playlist.items) - 1:
# Item was added at the end
_get_playListVersion_from_xml(playlist, xml)
_update_playlist_version(playlist, xml)
else:
# Move the new item to the correct position
move_playlist_item(playlist,
@ -655,7 +684,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
{'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None:
xml = PF.GetPlexMetadata(item.plex_id)
item.xml = xml[-1]
item.api = API(xml[-1])
playlist.items.insert(pos, item)
return item
@ -679,9 +708,10 @@ def move_playlist_item(playlist, before_pos, after_pos):
playlist.id,
playlist.items[before_pos].id,
playlist.items[after_pos - 1].id)
# We need to increment the playlistVersion
_get_playListVersion_from_xml(
playlist, DU().downloadUrl(url, action_type="PUT"))
# Tell the PMS that we're moving items around
xml = DU().downloadUrl(url, action_type="PUT")
# We need to increment the playlist version for communicating with the PMS
_update_playlist_version(playlist, xml)
# 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)
@ -729,7 +759,7 @@ def delete_playlist_item_from_PMS(playlist, pos):
playlist.repeat),
action_type="DELETE")
del playlist.items[pos]
_get_playListVersion_from_xml(playlist, xml)
_update_playlist_version(playlist, xml)
# Functions operating on the Kodi playlist objects ##########

View File

@ -14,13 +14,13 @@
from logging import getLogger
from sqlite3 import OperationalError
from .common import Playlist, PlaylistError, PlaylistObserver, \
kodi_playlist_hash
from .common import Playlist, PlaylistObserver, kodi_playlist_hash
from . import pms, db, kodi_pl, plex_pl
from ..watchdog import events
from ..plex_api import API
from .. import utils, path_ops, variables as v, app
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists')

View File

@ -11,6 +11,8 @@ from ..watchdog.observers import Observer
from ..watchdog.utils.bricks import OrderedSetQueue
from .. import path_ops, variables as v, app
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.common')
@ -19,13 +21,6 @@ SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED)
###############################################################################
class PlaylistError(Exception):
"""
The one main exception thrown if anything goes awry
"""
pass
class Playlist(object):
"""
Class representing a synced Playlist with info for both Kodi and Plex.

View File

@ -6,10 +6,12 @@ module
"""
from logging import getLogger
from .common import Playlist, PlaylistError
from .common import Playlist
from ..plex_db import PlexDB
from ..kodi_db import kodiid_from_filename
from .. import utils, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.db')
@ -120,7 +122,7 @@ def m3u_to_plex_ids(playlist):
def playlist_file_to_plex_ids(playlist):
"""
Takes the playlist file located at path [unicode] and parses it.
Returns a list of plex_ids (str) or raises PL.PlaylistError if a single
Returns a list of plex_ids (str) or raises PlaylistError if a single
item cannot be parsed from Kodi to Plex.
"""
if playlist.kodi_extension == 'm3u':

View File

@ -6,11 +6,13 @@ Create and delete playlists on the Kodi side of things
from logging import getLogger
import re
from .common import Playlist, PlaylistError, kodi_playlist_hash
from .common import Playlist, kodi_playlist_hash
from . import db, pms
from ..plex_api import API
from .. import utils, path_ops, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.kodi_pl')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')

View File

@ -5,8 +5,9 @@ Create and delete playlists on the Plex side of things
"""
from logging import getLogger
from .common import PlaylistError
from . import pms, db
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.plex_pl')
# Used for updating Plex playlists due to Kodi changes - Plex playlist

View File

@ -6,11 +6,11 @@ manipulate playlists
"""
from logging import getLogger
from .common import PlaylistError
from ..plex_api import API
from ..downloadutils import DownloadUtils as DU
from .. import utils, app, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.pms')

View File

@ -11,6 +11,7 @@ import xbmc
from .plex_api import API
from . import playlist_func as PL, plex_functions as PF
from . import backgroundthread, utils, json_rpc as js, app, variables as v
from . import exceptions
###############################################################################
LOG = getLogger('PLEX.playqueue')
@ -87,7 +88,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
api = API(child)
try:
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Could not add Plex item to our playlist: %s, %s',
child.tag, child.attrib)
playqueue.plex_transient_token = transient_token
@ -150,7 +151,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
i + j, i)
try:
PL.move_playlist_item(playqueue, i + j, i)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Could not modify playqueue positions')
LOG.error('This is likely caused by mixing audio and '
'video tracks in the Kodi playqueue')
@ -166,7 +167,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
PL.add_item_to_plex_playqueue(playqueue,
i,
kodi_item=new_item)
except PL.PlaylistError:
except exceptions.PlaylistError:
# Could not add the element
pass
except KeyError:
@ -195,7 +196,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
LOG.debug('Detected deletion of playqueue element at pos %s', i)
try:
PL.delete_playlist_item_from_PMS(playqueue, i)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Could not delete PMS element from position %s', i)
LOG.error('This is likely caused by mixing audio and '
'video tracks in the Kodi playqueue')

View File

@ -262,6 +262,12 @@ class Base(object):
"""
return self.xml[self.mediastream][self.part]
def part_id(self):
"""
Returns the unique id of the currently active part [int]
"""
return int(self.xml[self.mediastream][self.part].attrib['id'])
def plot(self):
"""
Returns the plot or None.

View File

@ -20,6 +20,7 @@ from . import playqueue as PQ
from . import variables as v
from . import backgroundthread
from . import app
from . import exceptions
###############################################################################
@ -50,7 +51,7 @@ def update_playqueue_from_PMS(playqueue,
with app.APP.lock_playqueues:
try:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Could now download playqueue %s', playqueue_id)
return
if playqueue.id == playqueue_id:
@ -63,7 +64,7 @@ def update_playqueue_from_PMS(playqueue,
# Get new metadata for the playqueue first
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except PL.PlaylistError:
except exceptions.PlaylistError:
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)

View File

@ -1116,3 +1116,35 @@ def playback_decision(path, media, part, playmethod, video=True, args=None):
return DU().downloadUrl(utils.extend_url(url, arguments),
headerOptions=v.STREAMING_HEADERS,
reraise=True)
def change_subtitle(plex_stream_id, part_id):
"""
Tell the PMS to display/burn-in the subtitle stream with id plex_stream_id
for the Part (PMS XML etree tag "Part") with unique id part_id.
- plex_stream_id = 0 will deactivate the subtitle
- We always do this for ALL parts of a video
"""
arguments = {
'subtitleStreamID': plex_stream_id,
'allParts': 1
}
url = '{server}/library/parts/%s' % part_id
return DU().downloadUrl(utils.extend_url(url, arguments),
action_type='PUT')
def change_audio_stream(plex_stream_id, part_id):
"""
Tell the PMS to display/burn-in the subtitle stream with id plex_stream_id
for the Part (PMS XML etree tag "Part") with unique id part_id.
- plex_stream_id = 0 will deactivate the subtitle
- We always do this for ALL parts of a video
"""
arguments = {
'audioStreamID': plex_stream_id,
'allParts': 1
}
url = '{server}/library/parts/%s' % part_id
return DU().downloadUrl(utils.extend_url(url, arguments),
action_type='PUT')

View File

@ -8,6 +8,7 @@ import xml.etree.ElementTree as etree
from . import app
from . import path_ops
from . import variables as v
from .exceptions import SubtitleError
LOG = getLogger('PLEX.subtitles')
@ -466,7 +467,3 @@ def external_subs_from_filesystem(dirname, filename):
class DummySub(etree.Element):
def __init__(self):
super(DummySub, self).__init__('Stream-subtitle-dummy')
class SubtitleError(Exception):
pass

View File

@ -29,6 +29,10 @@ ADDON_PATH = _ADDON.getAddonInfo('path')
ADDON_FOLDER = xbmcvfs.translatePath('special://home')
ADDON_PROFILE = xbmcvfs.translatePath(_ADDON.getAddonInfo('profile'))
# Used e.g. for json_rpc
KODI_VIDEO_PLAYER_ID = 1
KODI_AUDIO_PLAYER_ID = 0
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
@ -88,17 +92,14 @@ MIN_DB_VERSION = '3.2.1'
# Supported databases - version numbers in tuples should decrease
SUPPORTED_VIDEO_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (119, ),
20: (119, ),
}
SUPPORTED_MUSIC_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (82, ),
20: (82, ),
}
SUPPORTED_TEXTURE_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (13, ),
20: (13, ),
}