diff --git a/resources/lib/db.py b/resources/lib/db.py index d6b1bfaf..140493a3 100644 --- a/resources/lib/db.py +++ b/resources/lib/db.py @@ -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 diff --git a/resources/lib/exceptions.py b/resources/lib/exceptions.py new file mode 100644 index 00000000..396277a0 --- /dev/null +++ b/resources/lib/exceptions.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals + + +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 diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4e0582fc..5dfa7526 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -20,6 +20,7 @@ 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') @@ -30,7 +31,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) @@ -68,7 +69,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')) @@ -181,7 +182,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): @@ -318,7 +319,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 @@ -343,8 +344,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 @@ -359,32 +359,36 @@ 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}} """ - 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 + if not self._switched_to_plex_streams: + self.switch_to_plex_streams() + self._switched_to_plex_streams = True @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) @@ -588,11 +592,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): @@ -618,7 +621,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 diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 310f8b0f..c754436a 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -16,6 +16,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') @@ -192,7 +193,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 @@ -312,7 +313,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 @@ -444,27 +445,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.encode('utf-8')) 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') diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 80bd387a..26bf2c1b 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -15,19 +15,13 @@ from . import utils 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 @@ -155,13 +149,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 + api = None [API] API of xml 1 lvl below 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 @@ -171,7 +166,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 @@ -232,10 +227,10 @@ class PlaylistItem(object): count = 0 if kodi_stream_index == -1: # Kodi telling us "it's the last one" - iterator = list(reversed(self.xml[0][self.part])) + iterator = list(reversed(self.api.plex_media_streams())) kodi_stream_index = 0 else: - iterator = self.xml[0][self.part] + iterator = self.api.plex_media_streams() # Kodi indexes differently than Plex for stream in iterator: if (stream.get('streamType') == stream_type and @@ -276,7 +271,7 @@ class PlaylistItem(object): 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]: + for stream in self.api.plex_media_streams(): if stream.get('streamType') == stream_type \ and stream.get('selected') == '1': return (utils.cast(int, stream.get('id')), @@ -293,9 +288,9 @@ class PlaylistItem(object): if stream_type == '3': streams = accessible_plex_subtitles(self.playmethod, self.file, - self.xml[0][self.part]) + self.api.plex_media_streams()) else: - streams = [x for x in self.xml[0][self.part] + streams = [x for x in self.api.plex_media_streams() if x.get('streamType') == stream_type] return streams @@ -421,14 +416,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 @@ -611,7 +607,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() @@ -619,7 +615,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, @@ -663,7 +659,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 @@ -687,9 +683,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) @@ -737,7 +734,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 ########## diff --git a/resources/lib/playlists/__init__.py b/resources/lib/playlists/__init__.py index 5e612a4c..2ca77fea 100644 --- a/resources/lib/playlists/__init__.py +++ b/resources/lib/playlists/__init__.py @@ -15,13 +15,13 @@ from __future__ import absolute_import, division, unicode_literals 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') diff --git a/resources/lib/playlists/common.py b/resources/lib/playlists/common.py index 96ca151b..7dae1e9a 100644 --- a/resources/lib/playlists/common.py +++ b/resources/lib/playlists/common.py @@ -12,6 +12,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') @@ -20,13 +22,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. diff --git a/resources/lib/playlists/db.py b/resources/lib/playlists/db.py index 673fc02a..66e4d888 100644 --- a/resources/lib/playlists/db.py +++ b/resources/lib/playlists/db.py @@ -7,10 +7,11 @@ module from __future__ import absolute_import, division, unicode_literals 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 path_ops, utils, variables as v +from ..exceptions import PlaylistError ############################################################################### LOG = getLogger('PLEX.playlists.db') @@ -121,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': diff --git a/resources/lib/playlists/kodi_pl.py b/resources/lib/playlists/kodi_pl.py index 39cfa5de..86629d2f 100644 --- a/resources/lib/playlists/kodi_pl.py +++ b/resources/lib/playlists/kodi_pl.py @@ -7,11 +7,13 @@ from __future__ import absolute_import, division, unicode_literals 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+$''') diff --git a/resources/lib/playlists/plex_pl.py b/resources/lib/playlists/plex_pl.py index 2b10af60..2bca0779 100644 --- a/resources/lib/playlists/plex_pl.py +++ b/resources/lib/playlists/plex_pl.py @@ -6,8 +6,9 @@ Create and delete playlists on the Plex side of things from __future__ import absolute_import, division, unicode_literals 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 diff --git a/resources/lib/playlists/pms.py b/resources/lib/playlists/pms.py index b4de8e4a..e13540aa 100644 --- a/resources/lib/playlists/pms.py +++ b/resources/lib/playlists/pms.py @@ -7,11 +7,11 @@ manipulate playlists from __future__ import absolute_import, division, unicode_literals 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') diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 45fb1d50..46edb0bc 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -12,6 +12,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') @@ -88,7 +89,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 @@ -151,7 +152,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') @@ -167,7 +168,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: @@ -196,7 +197,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') diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index e0cbb807..1f135a78 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -21,6 +21,7 @@ from . import playqueue as PQ from . import variables as v from . import backgroundthread from . import app +from . import exceptions ############################################################################### @@ -51,7 +52,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: @@ -64,7 +65,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) diff --git a/resources/lib/subtitles.py b/resources/lib/subtitles.py index 439e6373..3948d678 100644 --- a/resources/lib/subtitles.py +++ b/resources/lib/subtitles.py @@ -9,6 +9,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') @@ -467,7 +468,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