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"?> <?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> <requires>
<import addon="xbmc.python" version="3.0.0"/> <import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.requests" version="2.22.0+matrix.1" /> <import addon="script.module.requests" version="2.22.0+matrix.1" />
@ -91,7 +91,22 @@
<summary lang="ko_KR">Plex를 Kodi에 기본 통합</summary> <summary lang="ko_KR">Plex를 Kodi에 기본 통합</summary>
<description lang="ko_KR">Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오!</description> <description lang="ko_KR">Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오!</description>
<disclaimer lang="ko_KR">자신의 책임하에 사용</disclaimer> <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 - 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 - 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: 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 - 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 - version 3.4.3 for everyone

View file

@ -4,19 +4,13 @@ import sqlite3
from functools import wraps from functools import wraps
from . import variables as v, app from . import variables as v, app
from .exceptions import LockedDatabase
DB_WRITE_ATTEMPTS = 100 DB_WRITE_ATTEMPTS = 100
DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds
DB_CONNECTION_TIMEOUT = 10 DB_CONNECTION_TIMEOUT = 10
class LockedDatabase(Exception):
"""
Dedicated class to make sure we're not silently catching locked DBs.
"""
pass
def catch_operationalerrors(method): def catch_operationalerrors(method):
""" """
sqlite.OperationalError is raised immediately if another DB connection 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'] '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): def get_player_props(playerid):
""" """
Returns a dict for the active Kodi player with the following values: Returns a dict for the active Kodi player with the following values:

View file

@ -602,6 +602,22 @@ class KodiVideoDB(common.KodiDBBase):
return return
return movie_id, typus 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): def get_resume(self, file_id):
""" """
Returns the first resume point in seconds (int) if found, else None for 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_api import API
from .plex_db import PlexDB from .plex_db import PlexDB
from .kodi_db import KodiVideoDB
from . import kodi_db from . import kodi_db
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils, timing, plex_functions as PF from . import utils, timing, plex_functions as PF
from . import json_rpc as js, playqueue as PQ, playlist_func as PL from . import json_rpc as js, playqueue as PQ, playlist_func as PL
from . import backgroundthread, app, variables as v from . import backgroundthread, app, variables as v
from . import exceptions
LOG = getLogger('PLEX.kodimonitor') LOG = getLogger('PLEX.kodimonitor')
@ -28,7 +30,7 @@ class KodiMonitor(xbmc.Monitor):
""" """
def __init__(self): def __init__(self):
self._already_slept = False self._already_slept = False
self._switch_to_plex_streams = None self._switched_to_plex_streams = True
xbmc.Monitor.__init__(self) xbmc.Monitor.__init__(self)
for playerid in app.PLAYSTATE.player_states: for playerid in app.PLAYSTATE.player_states:
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template) app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
@ -66,7 +68,7 @@ class KodiMonitor(xbmc.Monitor):
self.PlayBackStart(data) self.PlayBackStart(data)
elif method == 'Player.OnAVChange': elif method == 'Player.OnAVChange':
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
self.on_av_change() self._on_av_change(data)
elif method == "Player.OnStop": elif method == "Player.OnStop":
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
_playback_cleanup(ended=data.get('end')) _playback_cleanup(ended=data.get('end'))
@ -85,6 +87,7 @@ class KodiMonitor(xbmc.Monitor):
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
self._playlist_onclear(data) self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate": elif method == "VideoLibrary.OnUpdate":
with app.APP.lock_playqueues:
_videolibrary_onupdate(data) _videolibrary_onupdate(data)
elif method == "VideoLibrary.OnRemove": elif method == "VideoLibrary.OnRemove":
pass pass
@ -178,7 +181,7 @@ class KodiMonitor(xbmc.Monitor):
try: try:
for i, item in enumerate(items): for i, item in enumerate(items):
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item) 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) LOG.info('Could not build Plex playlist for: %s', items)
def _json_item(self, playerid): def _json_item(self, playerid):
@ -315,7 +318,7 @@ class KodiMonitor(xbmc.Monitor):
return return
try: try:
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id) item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
except PL.PlaylistError: except exceptions.PlaylistError:
LOG.info('Could not initialize the Plex playlist') LOG.info('Could not initialize the Plex playlist')
return return
item.file = path item.file = path
@ -340,8 +343,7 @@ class KodiMonitor(xbmc.Monitor):
container_key = '/library/metadata/%s' % plex_id container_key = '/library/metadata/%s' % plex_id
# Mechanik for Plex skip intro feature # Mechanik for Plex skip intro feature
if utils.settings('enableSkipIntro') == 'true': if utils.settings('enableSkipIntro') == 'true':
api = API(item.xml) status['intro_markers'] = item.api.intro_markers()
status['intro_markers'] = api.intro_markers()
# Remember the currently playing item # Remember the currently playing item
app.PLAYSTATE.item = item app.PLAYSTATE.item = item
# Remember that this player has been active # Remember that this player has been active
@ -356,60 +358,96 @@ class KodiMonitor(xbmc.Monitor):
status['plex_type'] = plex_type status['plex_type'] = plex_type
status['playmethod'] = item.playmethod status['playmethod'] = item.playmethod
status['playcount'] = item.playcount status['playcount'] = item.playcount
try:
status['external_player'] = app.APP.player.isExternalPlayer() == 1 status['external_player'] = app.APP.player.isExternalPlayer() == 1
except AttributeError:
# Kodi version < 17
pass
LOG.debug('Set the player state: %s', status) LOG.debug('Set the player state: %s', status)
# Workaround for the Kodi add-on Up Next # Workaround for the Kodi add-on Up Next
if not app.SYNC.direct_paths: if not app.SYNC.direct_paths:
_notify_upnext(item) _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 Will be called when Kodi has a video, audio or subtitle stream. Also
happens when the stream changes. 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: playerid = data['player']['playerid']
self.switch_to_plex_streams(self._switch_to_plex_streams) if not playerid == v.KODI_VIDEO_PLAYER_ID:
self._switch_to_plex_streams = None # 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 @staticmethod
def switch_to_plex_streams(item): def switch_to_plex_streams():
""" """
Override Kodi audio and subtitle streams with Plex PMS' selection 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'): for typus in ('audio', 'subtitle'):
try: try:
plex_index, language_tag = item.active_plex_stream_index(typus) plex_index, language_tag = item.active_plex_stream_index(typus)
except TypeError: except TypeError:
if typus == 'subtitle': LOG.debug('Deactivating Kodi subtitles because the PMS '
LOG.info('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles') 'told us to not show any subtitles')
app.APP.player.showSubtitles(False) app.APP.player.showSubtitles(False)
item.current_kodi_sub_stream = False
continue continue
LOG.info('The PMS wants to display %s stream with Plex id %s and ' LOG.debug('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s', 'languageTag %s',
typus, plex_index, language_tag) typus, plex_index, language_tag)
kodi_index = item.kodi_stream_index(plex_index, kodi_index = item.kodi_stream_index(plex_index, typus)
typus)
if kodi_index is None: if kodi_index is None:
LOG.info('Leaving Kodi %s stream settings untouched since we ' LOG.debug('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi' 'could not parse Plex %s stream with id %s to a Kodi'
' index', typus, typus, plex_index) ' index', typus, typus, plex_index)
else: else:
LOG.info('Switching to Kodi %s stream number %s because the ' LOG.debug('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s', 'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index) typus, kodi_index, plex_index)
# If we're choosing an "illegal" index, this function does # If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors # need seem to fail nor log any errors
if typus == 'subtitle': if typus == 'audio':
app.APP.player.setSubtitleStream(kodi_index)
else:
app.APP.player.setAudioStream(kodi_index) 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): def _playback_cleanup(ended=False):
@ -585,11 +623,10 @@ def _next_episode(current_api):
current_api.grandparent_title()) current_api.grandparent_title())
return return
try: try:
next_api = API(xml[counter + 1]) return API(xml[counter + 1])
except IndexError: except IndexError:
# Was the last episode # Was the last episode
return pass
return next_api
def _complete_artwork_keys(info): def _complete_artwork_keys(info):
@ -615,7 +652,7 @@ def _notify_upnext(item):
""" """
if not item.plex_type == v.PLEX_TYPE_EPISODE: if not item.plex_type == v.PLEX_TYPE_EPISODE:
return return
this_api = API(item.xml) this_api = item.api
next_api = _next_episode(this_api) next_api = _next_episode(this_api)
if next_api is None: if next_api is None:
return return
@ -651,17 +688,34 @@ def _videolibrary_onupdate(data):
A specific Kodi library item has been updated. This seems to happen if the 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 user marks an item as watched/unwatched or if playback of the item just
stopped 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 'item' in data else data
item = data.get('item')
if playcount is None or item is None:
return
try: try:
kodi_id = item['id'] kodi_id = item['id']
kodi_type = item['type'] kodi_type = item['type']
except (KeyError, TypeError): except (KeyError, TypeError):
LOG.info("Item is invalid for playstate update.") LOG.debug("Item is invalid for a Plex playstate update")
return 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 \ if app.PLAYSTATE.item and kodi_id == app.PLAYSTATE.item.kodi_id and \
kodi_type == app.PLAYSTATE.item.kodi_type: kodi_type == app.PLAYSTATE.item.kodi_type:
# Kodi updates an item immediately after playback. Hence we do NOT # 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 ..plex_db import PlexDB
from .. import backgroundthread, utils from .. import backgroundthread, utils
from .. import variables as v, app from .. import variables as v, app
from ..exceptions import ProcessingNotDone
logger = getLogger('PLEX.sync.metadata') 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): def processing_is_activated(item_getter):
"""Checks the PKC settings whether processing is even activated.""" """Checks the PKC settings whether processing is even activated."""
if item_getter == 'missing_fanart': 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 Appends either a '\\' or '/' - IRRELEVANT of the host OS!! (os.path.join is
dependant on the host OS) dependant on the host OS)
""" """
if '/' in path: separator = '/' if '/' in path else '\\'
return path + '/' return path if path.endswith(separator) else path + separator
else:
return path + '\\'
def translate_path(path): 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 plex_functions as PF, playlist_func as PL, playqueue as PQ
from . import json_rpc as js, variables as v, utils, transfer from . import json_rpc as js, variables as v, utils, transfer
from . import playback_decision, app from . import playback_decision, app
from . import exceptions
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playback') 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 # Special case - we already got a filled Kodi playqueue
try: try:
_init_existing_kodi_playlist(playqueue, pos) _init_existing_kodi_playlist(playqueue, pos)
except PL.PlaylistError: except exceptions.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed') LOG.error('Playback_init for existing Kodi playlist failed')
_ensure_resolve(abort=True) _ensure_resolve(abort=True)
return return
@ -311,7 +312,7 @@ def _init_existing_kodi_playlist(playqueue, pos):
kodi_items = js.playlist_get_items(playqueue.playlistid) kodi_items = js.playlist_get_items(playqueue.playlistid)
if not kodi_items: if not kodi_items:
LOG.error('No Kodi items returned') 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 = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
item.force_transcode = app.PLAYSTATE.force_transcode item.force_transcode = app.PLAYSTATE.force_transcode
# playqueue.py will add the rest - this will likely put the PMS under # 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) LOG.debug('Concluding playback for playqueue position %s', pos)
item = playqueue.items[pos] item = playqueue.items[pos]
api = API(item.xml) if item.api.mediastream_number() is None:
if api.mediastream_number() is None:
# E.g. user could choose between several media streams and cancelled # E.g. user could choose between several media streams and cancelled
LOG.debug('Did not get a mediastream_number') LOG.debug('Did not get a mediastream_number')
_ensure_resolve() _ensure_resolve()
return return
api.part = item.part or 0 item.api.part = item.part or 0
playback_decision.set_pkc_playmethod(api, item) playback_decision.set_pkc_playmethod(item.api, item)
if not playback_decision.audio_subtitle_prefs(api, item): if not playback_decision.audio_subtitle_prefs(item.api, item):
LOG.info('Did not set audio subtitle prefs, aborting silently') LOG.info('Did not set audio subtitle prefs, aborting silently')
_ensure_resolve() _ensure_resolve()
return return
playback_decision.set_playurl(api, item) playback_decision.set_playurl(item.api, item)
if not item.file: if not item.file:
LOG.info('Did not get a playurl, aborting playback silently') LOG.info('Did not get a playurl, aborting playback silently')
_ensure_resolve() _ensure_resolve()
return return
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False) listitem = item.api.listitem(listitem=transfer.PKCListItem, resume=False)
listitem.setPath(item.file) listitem.setPath(item.file)
if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH: if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH:
listitem.setSubtitles(api.cache_external_subs()) listitem.setSubtitles(item.api.cache_external_subs())
transfer.send(listitem) transfer.send(listitem)
LOG.debug('Done concluding playback') 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 Returns None if user cancelled or we need to abort, True otherwise
""" """
# Set media and part where we're at # Set media and part where we're at
if (api.mediastream is None and if api.mediastream is None and api.mediastream_number() is None:
api.mediastream_number() is None):
return 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: if item.playmethod != v.PLAYBACK_METHOD_TRANSCODE:
return True 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): 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', action_type='PUT',
parameters=args) parameters=args)
select_subs_index = '' # Zero telling the PMS to deactivate subs altogether
select_subs_index = 0
if sub_num == 1: if sub_num == 1:
# Note: we DO need to tell the PMS that we DONT want any sub # Note: we DO need to tell the PMS that we DONT want any sub
# Otherwise, the PMS might pick-up the last one # 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') LOG.info('User chose to not burn-in any subtitles')
else: else:
LOG.info('User chose to burn-in subtitle %s: %s', LOG.info('User chose to burn-in subtitle %s: %s',
select_subs_index, select_subs_index, subtitle_streams[resp])
subtitle_streams[resp])
select_subs_index = subtitle_streams_list[resp - 1] select_subs_index = subtitle_streams_list[resp - 1]
# Now prep the PMS for our choice # Now prep the PMS for our choice
args = { PF.change_subtitle(select_subs_index, part_id)
'subtitleStreamID': select_subs_index,
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
return True return True

View file

@ -11,22 +11,17 @@ from . import plex_functions as PF
from .kodi_db import kodiid_from_filename from .kodi_db import kodiid_from_filename
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils
from .utils import cast
from . import json_rpc as js from . import json_rpc as js
from . import variables as v from . import variables as v
from . import app from . import app
from .exceptions import PlaylistError
from .subtitles import accessible_plex_subtitles from .subtitles import accessible_plex_subtitles
LOG = getLogger('PLEX.playlist_func') LOG = getLogger('PLEX.playlist_func')
class PlaylistError(Exception):
"""
Exception for our playlist constructs
"""
pass
class Playqueue_Object(object): class Playqueue_Object(object):
""" """
PKC object to represent PMS playQueues and Kodi playlist for queueing 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!! file = None [str] Path to the item's file. STRING!!
uri = None [str] PMS path to item; will be auto-set with plex_id uri = None [str] PMS path to item; will be auto-set with plex_id
guid = None [str] Weird Plex guid 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' playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
playcount = None [int] how many times the item has already been played 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 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 part = 0 [int] part number if Plex video consists of mult. parts
force_transcode [bool] defaults to False force_transcode [bool] defaults to False
""" """
def __init__(self): def __init__(self):
self.id = None self.id = None
self._plex_id = None self._plex_id = None
@ -166,7 +162,7 @@ class PlaylistItem(object):
self.file = None self.file = None
self._uri = None self._uri = None
self.guid = None self.guid = None
self.xml = None self.api = None
self.playmethod = None self.playmethod = None
self.playcount = None self.playcount = None
self.offset = None self.offset = None
@ -180,6 +176,16 @@ class PlaylistItem(object):
# False: do NOT resume, don't ask user # False: do NOT resume, don't ask user
# True: do resume, don't ask user # True: do resume, don't ask user
self.resume = None 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 @property
def plex_id(self): def plex_id(self):
@ -195,6 +201,18 @@ class PlaylistItem(object):
def uri(self): def uri(self):
return self._uri 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): def __repr__(self):
return ("{{" return ("{{"
"'id': {self.id}, " "'id': {self.id}, "
@ -211,85 +229,100 @@ class PlaylistItem(object):
"'force_transcode': {self.force_transcode}, " "'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self)) "'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): def plex_stream_index(self, kodi_stream_index, stream_type):
""" """
Pass in the kodi_stream_index [int] in order to receive the Plex stream Pass in the kodi_stream_index [int] in order to receive the Plex stream
index. index [int].
stream_type: 'video', 'audio', 'subtitle' stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful Returns None if unsuccessful
""" """
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] if stream_type == 'audio':
count = 0 return int(self.audio_streams[kodi_stream_index].get('id'))
if kodi_stream_index == -1: elif stream_type == 'subtitle':
# Kodi telling us "it's the last one" try:
iterator = list(reversed(self.xml[0][self.part])) return int(self.subtitle_streams[kodi_stream_index].get('id'))
kodi_stream_index = 0 except (IndexError, TypeError):
else: pass
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
def kodi_stream_index(self, plex_stream_index, stream_type): def kodi_stream_index(self, plex_stream_index, stream_type):
""" """
Pass in the kodi_stream_index [int] in order to receive the Plex stream Pass in the plex_stream_index [int] in order to receive the Kodi stream
index. index [int].
stream_type: 'video', 'audio', 'subtitle' stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful Returns None if unsuccessful
""" """
if plex_stream_index is None: if plex_stream_index is None:
return return
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] for i, stream in enumerate(self._get_iterator(stream_type)):
count = 0 if cast(int, stream.get('id')) == plex_stream_index:
streams = self.sorted_accessible_plex_subtitles(stream_type) return i
for stream in streams:
if utils.cast(int, stream.get('id')) == plex_stream_index:
return count
count += 1
def active_plex_stream_index(self, stream_type): def active_plex_stream_index(self, stream_type):
""" """
Returns the following tuple for the active stream on the Plex side: 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 Returns None if no stream has been selected
""" """
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type] for i, stream in enumerate(self._get_iterator(stream_type)):
for stream in self.xml[0][self.part]: if stream.get('selected') == '1':
if stream.get('streamType') == stream_type \ return (int(stream.get('id')), stream.get('languageTag'))
and stream.get('selected') == '1':
return (utils.cast(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 Call this method if Kodi changed its subtitle and you want Plex to
are used; i.e. Kodi has access to a video's directory. know.
NOT supported: additional subtitles downloaded using the Plex interface
""" """
# The playqueue response from the PMS does not contain a stream filename if subs_enabled:
# thanks Plex try:
if stream_type == '3': plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
streams = accessible_plex_subtitles(self.playmethod, except (IndexError, TypeError):
self.file, LOG.debug('Kodi subtitle change detected to a sub %s that is '
self.xml[0][self.part]) '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: else:
streams = [x for x in self.xml[0][self.part] plex_stream_index = 0
if x.get('streamType') == stream_type] LOG.debug('Kodi subtitle has been deactivated, telling Plex')
return streams 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): def playlist_item_from_kodi(kodi_item):
@ -315,7 +348,7 @@ def playlist_item_from_kodi(kodi_item):
except IndexError: except IndexError:
query = '' query = ''
query = dict(utils.parse_qsl(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') item.plex_type = query.get('itemType')
LOG.debug('Made playlist item from Kodi: %s', item) LOG.debug('Made playlist item from Kodi: %s', item)
return 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.guid = api.guid_html_escaped()
item.playcount = api.viewcount() item.playcount = api.viewcount()
item.offset = api.resume_point() item.offset = api.resume_point()
item.xml = xml_video_element item.api = api
LOG.debug('Created new playlist item from xml: %s', item) LOG.debug('Created new playlist item from xml: %s', item)
return 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). playQueueVersion).
Raises PlaylistError if unsuccessful Raises PlaylistError if unsuccessful
@ -441,17 +475,12 @@ def get_playlist_details_from_xml(playlist, xml):
""" """
if xml is None: if xml is None:
raise PlaylistError('No playlist received for playlist %s' % playlist) raise PlaylistError('No playlist received for playlist %s' % playlist)
playlist.id = utils.cast(int, playlist.id = cast(int, xml.get('%sID' % playlist.kind))
xml.get('%sID' % playlist.kind)) playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
playlist.version = utils.cast(int, playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
xml.get('%sVersion' % playlist.kind)) playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
playlist.shuffled = utils.cast(int,
xml.get('%sShuffled' % playlist.kind))
playlist.selectedItemID = utils.cast(int,
xml.get('%sSelectedItemID'
% playlist.kind)) % playlist.kind))
playlist.selectedItemOffset = utils.cast(int, playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
xml.get('%sSelectedItemOffset'
% playlist.kind)) % playlist.kind))
LOG.debug('Updated playlist from xml: %s', playlist) 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' raise PlaylistError('Could not add item %s to playlist %s'
% (kodi_item, playlist)) % (kodi_item, playlist))
api = API(xml[-1]) api = API(xml[-1])
item.xml = xml[-1] item.api = api
item.id = api.item_id() item.id = api.item_id()
item.guid = api.guid_html_escaped() item.guid = api.guid_html_escaped()
item.offset = api.resume_point() 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) playlist.items.append(item)
if pos == len(playlist.items) - 1: if pos == len(playlist.items) - 1:
# Item was added at the end # Item was added at the end
_get_playListVersion_from_xml(playlist, xml) _update_playlist_version(playlist, xml)
else: else:
# Move the new item to the correct position # Move the new item to the correct position
move_playlist_item(playlist, 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}) {'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None: if item.plex_id is not None:
xml = PF.GetPlexMetadata(item.plex_id) xml = PF.GetPlexMetadata(item.plex_id)
item.xml = xml[-1] item.api = API(xml[-1])
playlist.items.insert(pos, item) playlist.items.insert(pos, item)
return item return item
@ -679,9 +708,10 @@ def move_playlist_item(playlist, before_pos, after_pos):
playlist.id, playlist.id,
playlist.items[before_pos].id, playlist.items[before_pos].id,
playlist.items[after_pos - 1].id) playlist.items[after_pos - 1].id)
# We need to increment the playlistVersion # Tell the PMS that we're moving items around
_get_playListVersion_from_xml( xml = DU().downloadUrl(url, action_type="PUT")
playlist, 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 # Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos)) playlist.items.insert(after_pos, playlist.items.pop(before_pos))
LOG.debug('Done moving for %s', playlist) LOG.debug('Done moving for %s', playlist)
@ -729,7 +759,7 @@ def delete_playlist_item_from_PMS(playlist, pos):
playlist.repeat), playlist.repeat),
action_type="DELETE") action_type="DELETE")
del playlist.items[pos] del playlist.items[pos]
_get_playListVersion_from_xml(playlist, xml) _update_playlist_version(playlist, xml)
# Functions operating on the Kodi playlist objects ########## # Functions operating on the Kodi playlist objects ##########

View file

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

View file

@ -11,6 +11,8 @@ from ..watchdog.observers import Observer
from ..watchdog.utils.bricks import OrderedSetQueue from ..watchdog.utils.bricks import OrderedSetQueue
from .. import path_ops, variables as v, app from .. import path_ops, variables as v, app
from ..exceptions import PlaylistError
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playlists.common') 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 Playlist(object):
""" """
Class representing a synced Playlist with info for both Kodi and Plex. Class representing a synced Playlist with info for both Kodi and Plex.

View file

@ -6,10 +6,12 @@ module
""" """
from logging import getLogger from logging import getLogger
from .common import Playlist, PlaylistError from .common import Playlist
from ..plex_db import PlexDB from ..plex_db import PlexDB
from ..kodi_db import kodiid_from_filename from ..kodi_db import kodiid_from_filename
from .. import utils, variables as v from .. import utils, variables as v
from ..exceptions import PlaylistError
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playlists.db') LOG = getLogger('PLEX.playlists.db')
@ -120,7 +122,7 @@ def m3u_to_plex_ids(playlist):
def playlist_file_to_plex_ids(playlist): def playlist_file_to_plex_ids(playlist):
""" """
Takes the playlist file located at path [unicode] and parses it. 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. item cannot be parsed from Kodi to Plex.
""" """
if playlist.kodi_extension == 'm3u': 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 from logging import getLogger
import re import re
from .common import Playlist, PlaylistError, kodi_playlist_hash from .common import Playlist, kodi_playlist_hash
from . import db, pms from . import db, pms
from ..plex_api import API from ..plex_api import API
from .. import utils, path_ops, variables as v from .. import utils, path_ops, variables as v
from ..exceptions import PlaylistError
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playlists.kodi_pl') LOG = getLogger('PLEX.playlists.kodi_pl')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''') 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 logging import getLogger
from .common import PlaylistError
from . import pms, db from . import pms, db
from ..exceptions import PlaylistError
############################################################################### ###############################################################################
LOG = getLogger('PLEX.playlists.plex_pl') LOG = getLogger('PLEX.playlists.plex_pl')
# Used for updating Plex playlists due to Kodi changes - Plex playlist # Used for updating Plex playlists due to Kodi changes - Plex playlist

View file

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

View file

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

View file

@ -262,6 +262,12 @@ class Base(object):
""" """
return self.xml[self.mediastream][self.part] 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): def plot(self):
""" """
Returns the plot or None. Returns the plot or None.

View file

@ -20,6 +20,7 @@ from . import playqueue as PQ
from . import variables as v from . import variables as v
from . import backgroundthread from . import backgroundthread
from . import app from . import app
from . import exceptions
############################################################################### ###############################################################################
@ -50,7 +51,7 @@ def update_playqueue_from_PMS(playqueue,
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
try: try:
xml = PL.get_PMS_playlist(playqueue, playqueue_id) xml = PL.get_PMS_playlist(playqueue, playqueue_id)
except PL.PlaylistError: except exceptions.PlaylistError:
LOG.error('Could now download playqueue %s', playqueue_id) LOG.error('Could now download playqueue %s', playqueue_id)
return return
if playqueue.id == playqueue_id: if playqueue.id == playqueue_id:
@ -63,7 +64,7 @@ def update_playqueue_from_PMS(playqueue,
# Get new metadata for the playqueue first # Get new metadata for the playqueue first
try: try:
PL.get_playlist_details_from_xml(playqueue, xml) PL.get_playlist_details_from_xml(playqueue, xml)
except PL.PlaylistError: except exceptions.PlaylistError:
LOG.error('Could not get playqueue ID %s', playqueue_id) LOG.error('Could not get playqueue ID %s', playqueue_id)
return return
playqueue.repeat = 0 if not repeat else int(repeat) 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), return DU().downloadUrl(utils.extend_url(url, arguments),
headerOptions=v.STREAMING_HEADERS, headerOptions=v.STREAMING_HEADERS,
reraise=True) 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 app
from . import path_ops from . import path_ops
from . import variables as v from . import variables as v
from .exceptions import SubtitleError
LOG = getLogger('PLEX.subtitles') LOG = getLogger('PLEX.subtitles')
@ -466,7 +467,3 @@ def external_subs_from_filesystem(dirname, filename):
class DummySub(etree.Element): class DummySub(etree.Element):
def __init__(self): def __init__(self):
super(DummySub, self).__init__('Stream-subtitle-dummy') 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_FOLDER = xbmcvfs.translatePath('special://home')
ADDON_PROFILE = xbmcvfs.translatePath(_ADDON.getAddonInfo('profile')) 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) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
@ -88,17 +92,14 @@ MIN_DB_VERSION = '3.2.1'
# Supported databases - version numbers in tuples should decrease # Supported databases - version numbers in tuples should decrease
SUPPORTED_VIDEO_DB = { SUPPORTED_VIDEO_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (119, ), 19: (119, ),
20: (119, ), 20: (119, ),
} }
SUPPORTED_MUSIC_DB = { SUPPORTED_MUSIC_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (82, ), 19: (82, ),
20: (82, ), 20: (82, ),
} }
SUPPORTED_TEXTURE_DB = { SUPPORTED_TEXTURE_DB = {
# Kodi 19 - EXTREMLY EXPERIMENTAL!
19: (13, ), 19: (13, ),
20: (13, ), 20: (13, ),
} }