diff --git a/script.plexmod/addon.xml b/script.plexmod/addon.xml index 37e9d6038..b456c8ab3 100644 --- a/script.plexmod/addon.xml +++ b/script.plexmod/addon.xml @@ -1,7 +1,7 @@ @@ -18,14 +18,22 @@ PlexMod for Kodi + PlexMod para Kodi + PlexMod für Kodi Unofficial Plex for Kodi add-on + Complemento no oficial de Plex para Kodi + Inoffizielles Plex für Kodi add-on + This add-on is not supported by Plex + Este add-on no está respaldado por Plex + Dieses Addon wird nicht von der Firma Plex Inc. unterstützt + GPL-2.0-only https://forums.plex.tv/t/plexmod-for-kodi-18-19-20-21/481208 https://www.plex.tv https://github.com/pannal/plex-for-kodi all -0.7.4 +- Based on 0.7.5-rev2 icon.png diff --git a/script.plexmod/changelog.txt b/script.plexmod/changelog.txt index c0f17d187..86b324f76 100644 --- a/script.plexmod/changelog.txt +++ b/script.plexmod/changelog.txt @@ -1,3 +1,64 @@ +[- 0.7.5-rev2 -] +- Core: asyncio Kodi compat +- Fix: transcoding is broken due to deepcopy usage +- Add IMDB ID to video info +- Fix: Libraries: Missing items when filters applied and collections exist (thanks @bowlingbeeg ) +- Fix: Libraries: Removed most filters from collections view as they don’t work (thanks @bowlingbeeg) +- Fix: Chapters not available in episodes during playback after manually changing watch status +- Fix: next episode receives resume state from previously resumed episode when pressing NEXT +- Fix: Items with non-existant files get removed from the home hubs when visited +- Fix: When using ACTION_PLAYER_PLAY to autoplay an item on Home that resumes, all title2 titles of the underlying screen disappear +- Fix: PPI: CPU core usage overlaps items when playing something with enabled subtitles +- Fix: SeekDialog/Handler/Player: Edge case where progress wasn't updated in certain situations +- Fix: Non-Home-Hubs: hub elements limited to 10 after modifying an underlying item +- Update Spanish translations (thanks @Deci8BelioS) +- TV Shows: Allow deleting TV shows if possible +- TV Shows: Allow deleting seasons if possible +- TV Shows: Add CONTEXT_MENU handler on seasons, allowing changing watched status and deletion +- TV Shows: More accurately show progress bar including in progress episodes +- Player: Don’t use old resume info when going to next video in playlist +- Player: Simplify and optimize stop/end/next logic +- Core: Fall back properly when Kodi version string couldn’t be parsed +- Core: Correctly reload addon settings on maximize from minimized state (so yes, you can minimize the addon, change addon settings, then maximize it again and the settings will be applied) +- Core: Edge-case: Ensure TV background music isn't recognized as audio +- Core: Logging: Clean Plex tokens from constructed item for playback dict +- Core: SeekHandler: fix usage of player.video.duration erroring +- Core: Home: Store last BG on minimize and on home select as well; don't store the same background URL if it hasn't changed +- Core: VideoPlayer: Make sure we're the only active player, try to stop all other active players for 5 seconds +- Core: Instead of using Action(back) before running addon, navigate to Home and try to guarantee that +- Home: Don’t round robin while loading the next hub pagination chunk +- Home: Sections: Increase section select timeout from 300 to 500ms +- Home: Fix autoplay from home on episodes (ACTION_PLAYER_PLAY) +- Home: Don't fail on empty hub +- InfoScreen: Add DV stream metadata info +- Episodes: Show episode options menu on long press select/OK (CONTEXT_MENU) +- Episodes/Seasons/Shows: Calculate and show season watched percentage including in-progress episodes +- Episodes/Seasons/Shows: Show progress even for shows/seasons with only an in progress episode, no fully watched ones +- Episodes: Adjust ratings/userratings positioning and label widths; add autoscroll to show and episode titles +- Episodes: Wait for full episode data to be loaded before enabling playback +- Episodes: Add directors/writers to episode view +- Episodes: BGM: Fix show() accessor failing +- Episodes: Don't play background music when current volume is zero +- Movies/TV/Episodes: Support ratings from non-legacy agents (also when primary provider is IMDB) +- Movies: Scroll long titles as well +- Movies/Preplay: Move userRating/stars in line with other ratings +- Movies/Preplay: Show writers besides directors as well +- PlayerSettings: Add Kodi Resolution Settings for Kodi >= 18 (only when you have a whitelist configured in Kodi/Video) +- PlayerSettings: Add Kodi Colour Management for Kodi >= 20 (when applicable) +- SeekDialog: Ignore immediate OK/SELECT on auto-skipping marker with countdown during the first second of the marker shown +- SeekDialog: Make sure we only send the correct timeline request on certain actions +- SeekDialog: Make sure we send a timeline request on certain actions +- SeekDialog: Remove now unnecessary negative offset on manual marker skip on final credits marker, as we’ve got much more robust PostPlay handling now +- SeekDialog: Avoid showing the final credits marker twice (after it’s been manually skipped already) +- SeekDialog: Hide once-manually-skipped markers and move them to the OSD as well +- SeekDialog: Don't react to SELECT/ENTER on counting down autoskip marker if marker is only visible in OSD +- SeekDialog: Bingemode: Don’t autoskip the credits of the last available episode of a TV show +- Player/SeekDialog: Simplify and harden PostPlay behaviour +- Libraries: Clean up code, improve performance (memory usage, CPU load) and optimize viewing experience by properly chunking the view’s requests based on the view position (thanks @bowlingbeeg) +- Addon Settings: Add setting for Library view chunk size +- Settings: Add separate playback setting to skip Post Play in TV shows (separate from binge mode) +- Settings: Clarify and reorder playback settings + [- 0.7.4 -] - Add: Show video codec rendering type (SDR/HDR, ...) in "choose version" dialog - Add: Add imperfect (but better-than-none) representation of DTS profiles and EAC3 JOC to preplay and stream screens diff --git a/script.plexmod/lib/_included_packages/plexnet/audioobject.py b/script.plexmod/lib/_included_packages/plexnet/audioobject.py index 453e5e604..a980dea6f 100644 --- a/script.plexmod/lib/_included_packages/plexnet/audioobject.py +++ b/script.plexmod/lib/_included_packages/plexnet/audioobject.py @@ -30,7 +30,7 @@ def build(self, directPlay=None): self.metadata = obj - util.LOG("Constructed audio item for playback: {0}".format(obj)) + util.LOG("Constructed audio item for playback: {0}".format(util.cleanObjTokens(obj))) return self.metadata diff --git a/script.plexmod/lib/_included_packages/plexnet/media.py b/script.plexmod/lib/_included_packages/plexnet/media.py index 1192e02a3..802da02c9 100644 --- a/script.plexmod/lib/_included_packages/plexnet/media.py +++ b/script.plexmod/lib/_included_packages/plexnet/media.py @@ -57,15 +57,18 @@ def delete(self): self.deleted = req.wasOK() return self.deleted - def exists(self): - if self.deleted or self.deletedAt: + def exists(self, force_full_check=False): + if (self.deleted or self.deletedAt) and not force_full_check: return False - data = self.server.query('/library/metadata/{0}'.format(self.ratingKey)) + # force_full_check is imperfect, as it doesn't check for existence of its mediaparts + try: + data = self.server.query('/library/metadata/{0}'.format(self.ratingKey)) + except exceptions.BadRequest: + # item does not exist anymore + util.DEBUG_LOG("Item {} doesn't exist.".format(self.ratingKey)) + return False return data is not None and data.attrib.get('size') != '0' - # req = plexrequest.PlexRequest(self.server, '/library/metadata/{0}'.format(self.ratingKey), method='HEAD') - # req.getToStringWithTimeout(10) - # return not req.wasNotFound() def relatedHubs(self, data, _itemCls, hubIdentifiers=None, _filter=None): hubs = data.find("Related") diff --git a/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py index 6d043f24c..454516be7 100644 --- a/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py +++ b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py @@ -131,20 +131,20 @@ def __init__(self): for timelineType in self.TIMELINE_TYPES: self.timelines[timelineType] = TimelineData(timelineType) - def updatePlaybackState(self, timelineType, playerObject, state, time, playQueue=None, duration=0): + def updatePlaybackState(self, timelineType, playerObject, state, t, playQueue=None, duration=0, force=False): timeline = self.timelines[timelineType] timeline.state = state timeline.item = playerObject.item timeline.choice = playerObject.choice timeline.playQueue = playQueue - timeline.attrs["time"] = str(time) + timeline.attrs["time"] = str(t) timeline.duration = duration # self.sendTimelineToAll() - self.sendTimelineToServer(timelineType, timeline, time) + self.sendTimelineToServer(timelineType, timeline, t, force=force) - def sendTimelineToServer(self, timelineType, timeline, time): + def sendTimelineToServer(self, timelineType, timeline, t, force=False): if not hasattr(timeline.item, 'getServer') or not timeline.item.getServer(): return @@ -152,7 +152,7 @@ def sendTimelineToServer(self, timelineType, timeline, time): # Only send timeline if it's the first, item changes, playstate changes or timer pops itemsEqual = timeline.item and serverTimeline.item and timeline.item.ratingKey == serverTimeline.item.ratingKey - if itemsEqual and timeline.state == serverTimeline.state and not serverTimeline.isExpired(): + if itemsEqual and timeline.state == serverTimeline.state and not serverTimeline.isExpired() and not force: return serverTimeline.reset() @@ -168,11 +168,11 @@ def sendTimelineToServer(self, timelineType, timeline, time): # It's possible with timers and in player seeking for the time to be greater than the # duration, which causes a 400, so in that case we'll set the time to the duration. duration = timeline.item.duration.asInt() or timeline.duration - if time > duration: - time = duration + if t > duration: + t = duration params = util.AttributeDict() - params["time"] = time + params["time"] = t params["duration"] = duration params["state"] = timeline.state params["guid"] = timeline.item.guid diff --git a/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py index d59a4e974..c9d0c4fc4 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py @@ -154,7 +154,7 @@ def folder(self, start=None, size=None, subDir=False): def items(self, path, start, size, filter_, sort, unwatched, type_, tag_fallback): - args = {"includeCollections" : "1"} + args = {} if size is not None: args['X-Plex-Container-Start'] = start @@ -162,6 +162,8 @@ def items(self, path, start, size, filter_, sort, unwatched, type_, tag_fallback if filter_: args[filter_[0]] = filter_[1] + else: + args['includeCollections'] = 1 if sort: args['sort'] = '{0}:{1}'.format(*sort) @@ -183,10 +185,12 @@ def jumpList(self, filter_=None, sort=None, unwatched=False, type_=None): else: path = '/library/sections/{0}/firstCharacter'.format(self.key) - args = {"includeCollections" : "1"} + args = {} if filter_: args[filter_[0]] = filter_[1] + else: + args['includeCollections'] = 1 if sort: args['sort'] = '{0}:{1}'.format(*sort) @@ -444,7 +448,7 @@ def __repr__(self): title = self.title.replace(' ', '.')[0:20] return '<{0}:{1}:{2}>'.format(self.__class__.__name__, self.key, title) - def exists(self): + def exists(self, *args, **kwargs): try: self.server.query('/playlists/{0}'.format(self.ratingKey)) return True @@ -506,6 +510,10 @@ def buildComposite(self, **kwargs): class BaseHub(plexobjects.PlexObject): + def __init__(self, *args, **kwargs): + super(BaseHub, self).__init__(*args, **kwargs) + self._identifier = None + def reset(self): self.set('offset', 0) self.set('size', len(self.items)) @@ -516,6 +524,11 @@ def reset(self): (self.items[0].container.offset.asInt() + self.items[0].container.size.asInt() < totalSize) and '1' or '' ) + def getCleanHubIdentifier(self): + if not self._identifier: + self._identifier = re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) + return self._identifier + class Hub(BaseHub): TYPE = "Hub" @@ -541,13 +554,10 @@ def init(self, data): def __repr__(self): return '<{0}:{1}>'.format(self.__class__.__name__, self.hubIdentifier) - def getCleanHubIdentifier(self): - return re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) - def reload(self, **kwargs): """ Reload the data for this object from PlexServer XML. """ try: - data = self.server.query(self.key, params=kwargs) + data = self.server.query(self.key, **kwargs) except Exception as e: import traceback traceback.print_exc() @@ -594,9 +604,6 @@ def init(self, data): util.DEBUG_LOG('AudioPlaylistHub: Bad request: {0}'.format(self)) self.items = [] - def getCleanHubIdentifier(self): - return re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) - def extend(self, start=None, size=None): path = '/playlists/all?playlistType={0}'.format(self.type) diff --git a/script.plexmod/lib/_included_packages/plexnet/plexobjects.py b/script.plexmod/lib/_included_packages/plexnet/plexobjects.py index 6d5c01f2d..cba919e90 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexobjects.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexobjects.py @@ -43,6 +43,12 @@ def __new__(cls, value, parent=None): def __call__(self, default): return not self.NA and self or PlexValue(default, self.parent) + def __copy__(self): + return self.__deepcopy__() + + def __deepcopy__(self, memodict=None): + return self.__class__(self) + def asBool(self): return self == '1' @@ -187,7 +193,7 @@ def __getattr__(self, attr): return a - def exists(self): + def exists(self, *args, **kwargs): # Used for media items - for others we just return True return True diff --git a/script.plexmod/lib/_included_packages/plexnet/plexplayer.py b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py index 89482c430..fdafd9e4c 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexplayer.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py @@ -171,7 +171,7 @@ def _build(self, directPlay=None, directStream=True, currentPartIndex=None): self.metadata = obj - util.LOG("Constructed video item for playback: {0}".format(dict(obj))) + util.LOG("Constructed video item for playback: {0}".format(util.cleanObjTokens(dict(obj)))) return self.metadata @@ -853,7 +853,7 @@ def build(self, directPlay=None): self.metadata = obj - util.LOG("Constructed audio item for playback: {0}".format(dict(obj))) + util.LOG("Constructed audio item for playback: {0}".format(util.cleanObjTokens(dict(obj)))) return self.metadata @@ -927,7 +927,7 @@ def build(self, item=None): obj.url = server.buildUrl(path, True) obj.enableBlur = server.supportsPhotoTranscoding - util.DEBUG_LOG("Constructed photo item for playback: {0}".format(dict(obj))) + util.DEBUG_LOG("Constructed photo item for playback: {0}".format(util.cleanObjTokens(dict(obj)))) self.metadata = obj diff --git a/script.plexmod/lib/_included_packages/plexnet/plexserver.py b/script.plexmod/lib/_included_packages/plexnet/plexserver.py index 7c6e7a8c7..7857260a8 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexserver.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexserver.py @@ -10,7 +10,6 @@ from . import util from . import exceptions from . import compat -from . import verlib from xml.etree import ElementTree from . import signalsmixin diff --git a/script.plexmod/lib/_included_packages/plexnet/plexstream.py b/script.plexmod/lib/_included_packages/plexnet/plexstream.py index 33e034480..495620be6 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexstream.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexstream.py @@ -144,9 +144,17 @@ def videoCodecRendering(self): render = "sdr" if self.DOVIProfile == "8" and self.DOVIBLCompatID == "1": - render = "dv/hdr10" + render = "dv p8.1/hdr" + elif self.DOVIProfile == "8" and self.DOVIBLCompatID == "2": + render = "dv p8.2/sdr" + elif self.DOVIProfile == "8" and self.DOVIBLCompatID == "4": + render = "dv p8.4/hlg" + elif self.DOVIProfile == "7": + render = "dv p7" + elif self.DOVIProfile == "5": + render = "dv p5" elif self.DOVIProfile: - render = "dv" + render = "dv p{}".format(self.DOVIProfile) elif self.colorTrc == "smpte2084": render = "hdr" elif self.colorTrc == "arib-std-b67": diff --git a/script.plexmod/lib/_included_packages/plexnet/util.py b/script.plexmod/lib/_included_packages/plexnet/util.py index a979fbb0f..27340f12b 100644 --- a/script.plexmod/lib/_included_packages/plexnet/util.py +++ b/script.plexmod/lib/_included_packages/plexnet/util.py @@ -1,6 +1,9 @@ # coding=utf-8 from __future__ import absolute_import + +from copy import copy + from . import simpleobjects import re import sys @@ -156,6 +159,26 @@ def cleanToken(url): return re.sub(r'X-Plex-Token=[^&]+', 'X-Plex-Token=****', url) +def cleanObjTokens(dorig, flistkeys=("streamUrls",), fstrkeys=("url", "token")): + d = {} + dcopy = copy(dorig) + + # filter lists + for k in flistkeys: + if k not in d: + continue + d[k] = list(map(lambda x: cleanToken(x), d[k][:])) + + # filter strings + for k in fstrkeys: + if k not in d: + continue + d[k] = "****" if k == "token" else cleanToken(d[k]) + + dcopy.update(d) + return dcopy + + def now(local=False): if local: return time.time() diff --git a/script.plexmod/lib/_included_packages/plexnet/video.py b/script.plexmod/lib/_included_packages/plexnet/video.py index 00a12ebdd..8fe100a50 100644 --- a/script.plexmod/lib/_included_packages/plexnet/video.py +++ b/script.plexmod/lib/_included_packages/plexnet/video.py @@ -549,6 +549,8 @@ def _setData(self, data): self.roles = plexobjects.PlexItemList(data, media.Role, media.Role.TYPE, server=self.server, container=self.container) #self.related = plexobjects.PlexItemList(data.find('Related'), plexlibrary.Hub, plexlibrary.Hub.TYPE, server=self.server, container=self) self.extras = PlexVideoItemList(data.find('Extras'), initpath=self.initpath, server=self.server, container=self) + self.onDeck = PlexVideoItemList(data.find('OnDeck'), initpath=self.initpath, server=self.server, + container=self) @property def unViewedLeafCount(self): diff --git a/script.plexmod/lib/main.py b/script.plexmod/lib/main.py index 313dd1ff8..12244a5fc 100644 --- a/script.plexmod/lib/main.py +++ b/script.plexmod/lib/main.py @@ -9,11 +9,12 @@ import atexit import threading import six +import sys +sys.modules['_asyncio'] = None from . import plex from plexnet import plexapp -from plexnet import threadutils from .windows import background, userselect, home, windowutils from . import player from . import backgroundthread @@ -61,6 +62,8 @@ def signout(): def main(): global BACKGROUND + util.ensureHome() + try: with util.Cron(0.1): BACKGROUND = background.BackgroundWindow.create(function=_main) diff --git a/script.plexmod/lib/playback_utils.py b/script.plexmod/lib/playback_utils.py index 57c0edb25..86b20a4b3 100644 --- a/script.plexmod/lib/playback_utils.py +++ b/script.plexmod/lib/playback_utils.py @@ -17,7 +17,8 @@ "b": "binge_mode", "i": "auto_skip_intro", "c": "auto_skip_credits", - "e": "show_intro_skip_early" + "e": "show_intro_skip_early", + "p": "skip_post_play_tv", } # I know dicts are ordered in py3, but we want to be compatible with py2. @@ -25,7 +26,8 @@ ("binge_mode", 33618), ("auto_skip_intro", 32522), ("auto_skip_credits", 32526), - ("show_intro_skip_early", 33505) + ("show_intro_skip_early", 33505), + ("skip_post_play_tv", 32973), )) ATTR_MAP_REV = dict((v, k) for k, v in ATTR_MAP.items()) diff --git a/script.plexmod/lib/player.py b/script.plexmod/lib/player.py index 38d84299b..e21a85ed2 100644 --- a/script.plexmod/lib/player.py +++ b/script.plexmod/lib/player.py @@ -26,6 +26,7 @@ def __init__(self, player, session_id=None): self.player = player self.media = None self.baseOffset = 0 + self._lastDuration = 0 self.timelineType = None self.lastTimelineState = None self.ignoreTimelines = False @@ -108,14 +109,17 @@ def shouldSendTimeline(self, item): def currentDuration(self): if self.player.playerObject and self.player.isPlaying(): try: - return int(self.player.getTotalTime() * 1000) + self._lastDuration = int(self.player.getTotalTime() * 1000) + return self._lastDuration except RuntimeError: pass - return 0 + return self._lastDuration - def updateNowPlaying(self, force=False, refreshQueue=False, state=None, time=None): - util.DEBUG_LOG("UpdateNowPlaying: force: {0} refreshQueue: {1} state: {2}".format(force, refreshQueue, state)) + def updateNowPlaying(self, force=False, refreshQueue=False, t=None, state=None, overrideChecks=False): + util.DEBUG_LOG("UpdateNowPlaying: force: {0} refreshQueue: " + "{1} state: {2} overrideChecks: {3} time: {4}".format(force, refreshQueue, state, overrideChecks, + t)) if self.ignoreTimelines: util.DEBUG_LOG("UpdateNowPlaying: ignoring timeline as requested") return @@ -135,7 +139,7 @@ def updateNowPlaying(self, force=False, refreshQueue=False, state=None, time=Non self.lastTimelineState = state # self.timelineTimer.reset() - time = time or int(self.trueTime * 1000) + _time = t or int(self.trueTime * 1000) # self.trigger("progress", [m, item, time]) @@ -143,12 +147,16 @@ def updateNowPlaying(self, force=False, refreshQueue=False, state=None, time=Non self.playQueue.refreshOnTimeline = True plexapp.util.APP.nowplayingmanager.updatePlaybackState( - self.timelineType, self.player.playerObject, state, time, self.playQueue, duration=self.currentDuration() + self.timelineType, self.player.playerObject, state, _time, self.playQueue, duration=self.currentDuration(), + force=overrideChecks ) def getVolume(self): return util.rpc.Application.GetProperties(properties=["volume"])["volume"] + def sessionEnded(self): + self.player.sessionID = None + class SeekPlayerHandler(BasePlayerHandler): NO_SEEK = 0 @@ -172,8 +180,9 @@ def __init__(self, player, session_id=None): self.title2 = '' self.seekOnStart = 0 self.chapters = None - self.stoppedInBingeMode = False + self.stoppedManually = False self.inBingeMode = False + self.skipPostPlay = False self.prePlayWitnessed = False self.queuingNext = False self.reset() @@ -184,9 +193,10 @@ def reset(self): self.baseOffset = 0 self.seeking = self.NO_SEEK self.seekOnStart = 0 + self._lastDuration = 0 self.mode = self.MODE_RELATIVE self.ended = False - self.stoppedInBingeMode = False + self.stoppedManually = False self.prePlayWitnessed = False self.queuingNext = False @@ -195,6 +205,7 @@ def setup(self, duration, meta, offset, bif_url, title='', title2='', seeking=NO self.baseOffset = offset / 1000.0 self.seeking = seeking self.duration = duration + self._lastDuration = duration self.bifURL = bif_url self.title = title self.title2 = title2 @@ -202,8 +213,9 @@ def setup(self, duration, meta, offset, bif_url, title='', title2='', seeking=NO self.playedThreshold = plexapp.util.INTERFACE.getPlayedThresholdValue() self.ignoreTimelines = False self.queuingNext = False - self.stoppedInBingeMode = False + self.stoppedManually = False self.inBingeMode = False + self.skipPostPlay = False self.prePlayWitnessed = False self.getDialog(setup=True) self.dialog.setup(self.duration, meta, int(self.baseOffset * 1000), self.bifURL, self.title, self.title2, @@ -239,10 +251,10 @@ def shouldShowPostPlay(self): if self.playlist and self.playlist.TYPE == 'playlist': return False - if self.inBingeMode and not self.stoppedInBingeMode: + if not self.stoppedManually and self.skipPostPlay: return False - if (not util.advancedSettings.postplayAlways and self.player.video.duration.asInt() <= FIVE_MINUTES_MILLIS)\ + if (not util.advancedSettings.postplayAlways and self._lastDuration <= FIVE_MINUTES_MILLIS)\ or util.advancedSettings.postplayTimeout <= 0: return False @@ -258,9 +270,9 @@ def showPostPlay(self): self.hideOSD(delete=True) self.player.trigger('post.play', video=self.player.video, playlist=self.playlist, handler=self, - stoppedInBingeMode=self.stoppedInBingeMode) + stoppedManually=self.stoppedManually) - self.stoppedInBingeMode = False + self.stoppedManually = False return True @@ -275,10 +287,10 @@ def next(self, on_end=False): if self.showPostPlay(): return True - if not self.playlist or self.stoppedInBingeMode: + if not self.playlist or self.stoppedManually: return False - self.player.playVideoPlaylist(self.playlist, handler=self, resume=self.player.resume) + self.player.playVideoPlaylist(self.playlist, handler=self, resume=False) return True @@ -287,8 +299,7 @@ def prev(self): return False self.seeking = self.SEEK_PLAYLIST - xbmc.sleep(500) - self.player.playVideoPlaylist(self.playlist, handler=self, resume=self.player.resume) + self.player.playVideoPlaylist(self.playlist, handler=self, resume=False) return True @@ -433,16 +444,18 @@ def onPlayBackResumed(self): def onPlayBackStopped(self): util.DEBUG_LOG('SeekHandler: onPlayBackStopped - ' - 'Seeking={0}, QueueingNext={1}, BingeMode={2}'.format(self.seeking, self.queuingNext, - self.inBingeMode)) + 'Seeking={0}, QueueingNext={1}, BingeMode={2}, StoppedManually={3}, SkipPostPlay={4}' + .format(self.seeking, self.queuingNext, self.inBingeMode, self.stoppedManually, + self.skipPostPlay)) if self.dialog: self.dialog.onPlayBackStopped() - if self.queuingNext and self.inBingeMode: + if self.queuingNext: if self.isDirectPlay and self.playlist and self.playlist.hasNext(): self.hideOSD(delete=True) - if self.next(on_end=False): + # fixme: the on_end value is a hack here, we should rename or use a different parameter + if self.next(on_end=not self.skipPostPlay): return if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_REWIND): @@ -477,8 +490,7 @@ def onPlayBackEnded(self): util.DEBUG_LOG('SeekHandler: onPlayBackEnded - event ignored') return - if self.inBingeMode: - self.stoppedInBingeMode = False + self.stoppedManually = False if self.playlist and self.playlist.hasNext(): self.queuingNext = True @@ -548,6 +560,7 @@ def setSubtitles(self, do_sleep=True, honor_forced_subtitles_override=True): self.player.showSubtitles(False) def setAudioTrack(self): + self.player.lastPlayWasBGM = False if self.isDirectPlay: track = self.player.video.selectedAudioStream() if track: @@ -564,8 +577,6 @@ def setAudioTrack(self): except: util.ERROR() - self.player.lastPlayWasBGM = False - xbmc.sleep(100) util.DEBUG_LOG('Switching audio track - index: {0}'.format(track.typeIndex)) self.player.setAudioStream(track.typeIndex) @@ -637,6 +648,7 @@ def close(self): self.hideOSD(delete=True) def sessionEnded(self): + self.player.sessionID = None if self.ended: return self.ended = True @@ -807,13 +819,11 @@ def __init__(self, player, rating_key): self.timelineType = 'music' self.currentlyPlaying = rating_key util.setGlobalProperty('track.ID', '') - util.setGlobalProperty('theme_playing', '1') self.oldVolume = util.rpc.Application.GetProperties(properties=["volume"])["volume"] def onPlayBackStarted(self): util.DEBUG_LOG("BGM: playing theme for %s" % self.currentlyPlaying) - self.player.bgmPlaying = True def _setVolume(self, vlm): xbmc.executebuiltin("SetVolume({})".format(vlm)) @@ -876,6 +886,13 @@ def run(self): if self.isCanceled(): return + self.player.bgmPlaying = True + util.setGlobalProperty('theme_playing', '1') + ct = 0 + while ct < 10 and not util.getGlobalProperty('theme_playing') and not util.MONITOR.abortRequested(): + util.MONITOR.waitForAbort(0.1) + ct += 1 + self.player.play(self.source, windowed=True) @@ -890,6 +907,7 @@ class PlexPlayer(xbmc.Player, signalsmixin.SignalsMixin): def __init__(self, *args, **kwargs): xbmc.Player.__init__(self, *args, **kwargs) signalsmixin.SignalsMixin.__init__(self) + self.sessionID = None self.handler = AudioPlayerHandler(self) def init(self): @@ -901,6 +919,7 @@ def init(self): self.BGMTask = None self.pauseAfterPlaybackStarted = False self.video = None + self.sessionID = None self.hasOSD = False self.hasSeekOSD = False self.handler = AudioPlayerHandler(self) @@ -998,6 +1017,11 @@ def playBackgroundMusic(self, source, volume, rating_key, *args, **kwargs): # cancel any currently playing theme before starting the new one else: self.stopAndWait() + self.sessionID = "BGM{}".format(rating_key) + curVol = self.handler.getVolume() + # no current volume, don't play BGM either + if not curVol: + return if self.BGMTask and self.BGMTask.isValid(): self.BGMTask.cancel() @@ -1006,7 +1030,6 @@ def playBackgroundMusic(self, source, volume, rating_key, *args, **kwargs): self.handler = BGMPlayerHandler(self, rating_key) # store current volume if it's different from the BGM volume - curVol = self.handler.getVolume() if volume < curVol: util.setSetting('last_good_volume', curVol) @@ -1022,12 +1045,12 @@ def playVideo(self, video, resume=False, force_update=False, session_id=None, ha self.stopAndWait() self.handler = handler if handler and isinstance(handler, SeekPlayerHandler) \ - else SeekPlayerHandler(self, session_id) + else SeekPlayerHandler(self, session_id or self.sessionID) self.video = video self.resume = resume self.open() - self._playVideo(resume and video.viewOffset.asInt() or 0, force_update=force_update) + self._playVideo(resume and video.viewOffset.asInt() or 0, force_update=force_update, session_id=session_id) def getOSSPathHint(self, meta): # only hint the path one folder above for a movie, two folders above for TV @@ -1043,7 +1066,7 @@ def getOSSPathHint(self, meta): cleaned_path = "" return cleaned_path - def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None): + def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None, session_id=None): self.trigger('new.video', video=self.video) self.trigger( 'change.background', @@ -1079,6 +1102,7 @@ def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None) util.MONITOR.waitForAbort(util.advancedSettings.consecutiveVideoPbWait) self.ignoreStopEvents = False + self.sessionID = session_id or self.sessionID self.handler.setup(self.video.duration.asInt(), meta, offset, bifURL, title=self.video.grandparentTitle, title2=self.video.title, seeking=seeking, chapters=self.video.chapters) @@ -1122,18 +1146,23 @@ def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None) util.setGlobalProperty("current_path", self.getOSSPathHint(meta), base='videoinfo.{0}') util.setGlobalProperty("current_size", str(meta.size), base='videoinfo.{0}') + + imdbNum = None + if "com.plexapp.agents.imdb" in self.video.guid: + a = self.video.guid + imdbNum = a.split("?lang=")[0][a.index("com.plexapp.agents.imdb://")+len("com.plexapp.agents.imdb://"):] li.setInfo('video', { 'mediatype': vtype, 'title': self.video.title, 'originaltitle': self.video.title, 'tvshowtitle': self.video.grandparentTitle, - 'showtitle': self.video.grandparentTitle, 'episode': vtype == "episode" and self.video.index.asInt() or '', 'season': vtype == "episode" and self.video.parentIndex.asInt() or '', #'year': self.video.year.asInt(), 'plot': self.video.summary, 'path': meta.path, 'size': meta.size, + 'imdbnumber': imdbNum }) li.setArt({ 'poster': self.video.defaultThumb.asTranscodedImageURL(347, 518), @@ -1150,7 +1179,7 @@ def playVideoPlaylist(self, playlist, resume=False, handler=None, session_id=Non if handler and isinstance(handler, SeekPlayerHandler): self.handler = handler else: - self.handler = SeekPlayerHandler(self, session_id) + self.handler = SeekPlayerHandler(self, session_id or self.sessionID) self.handler.playlist = playlist if playlist.isRemote: @@ -1159,7 +1188,8 @@ def playVideoPlaylist(self, playlist, resume=False, handler=None, session_id=Non self.video.softReload(includeChapters=1) self.resume = resume self.open() - self._playVideo(resume and self.video.viewOffset.asInt() or 0, seeking=handler and handler.SEEK_PLAYLIST or 0, force_update=True) + self._playVideo(resume and self.video.viewOffset.asInt() or 0, seeking=handler and handler.SEEK_PLAYLIST or 0, + force_update=True, session_id=session_id) # def createVideoListItem(self, video, index=0): # url = 'plugin://script.plex/play?{0}'.format(base64.urlsafe_b64encode(video.serialize())) @@ -1186,15 +1216,21 @@ def playAudio(self, track, fanart=None, **kwargs): if self.bgmPlaying: self.stopAndWait() + self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) url, li = self.createTrackListItem(track, fanart) self.stopAndWait() + self.ignoreStopEvents = False + + # maybe fixme: once started, self.sessionID will never be None for Audio + self.sessionID = "AUD%s" % track.ratingKey self.play(url, li, **kwargs) def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): if self.bgmPlaying: self.stopAndWait() + self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) plist.clear() @@ -1205,12 +1241,15 @@ def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): index += 1 xbmc.executebuiltin('PlayerControl(RandomOff)') self.stopAndWait() + self.ignoreStopEvents = False + self.sessionID = "ALB%s" % album.ratingKey self.play(plist, startpos=startpos, **kwargs) def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): if self.bgmPlaying: self.stopAndWait() + self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) plist.clear() @@ -1229,6 +1268,8 @@ def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): else: xbmc.executebuiltin('PlayerControl(RandomOff)') self.stopAndWait() + self.ignoreStopEvents = False + self.sessionID = "PLS%s" % playlist.ratingKey self.play(plist, startpos=startpos, **kwargs) def createTrackListItem(self, track, fanart=None, index=0): @@ -1256,6 +1297,8 @@ def createTrackListItem(self, track, fanart=None, index=0): return (url, li) def onPrePlayStarted(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - PRE-PLAY; handler: %r' % self.handler) self.trigger('preplay.started') if not self.handler: @@ -1263,6 +1306,8 @@ def onPrePlayStarted(self): self.handler.onPrePlayStarted() def onPlayBackStarted(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - STARTED') self.trigger('playback.started') self.started = True @@ -1275,12 +1320,16 @@ def onPlayBackStarted(self): self.handler.onPlayBackStarted() def onAVChange(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - AVChange') if not self.handler: return self.handler.onAVChange() def onAVStarted(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - AVStarted: {}'.format(self.handler)) self.trigger('av.started') if not self.handler: @@ -1288,12 +1337,16 @@ def onAVStarted(self): self.handler.onAVStarted() def onPlayBackPaused(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - PAUSED') if not self.handler: return self.handler.onPlayBackPaused() def onPlayBackResumed(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - RESUMED') if not self.handler: return @@ -1301,6 +1354,8 @@ def onPlayBackResumed(self): self.handler.onPlayBackResumed() def onPlayBackStopped(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - STOPPED' + (not self.started and ': FAILED' or '')) if self.ignoreStopEvents: return @@ -1313,6 +1368,8 @@ def onPlayBackStopped(self): self.handler.onPlayBackStopped() def onPlayBackEnded(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - ENDED' + (not self.started and ': FAILED' or '')) if self.ignoreStopEvents: return @@ -1325,12 +1382,16 @@ def onPlayBackEnded(self): self.handler.onPlayBackEnded() def onPlayBackSeek(self, time, offset): + if not self.sessionID: + return util.DEBUG_LOG('Player - SEEK: %i' % offset) if not self.handler: return self.handler.onPlayBackSeek(time, offset) def onPlayBackFailed(self): + if not self.sessionID: + return util.DEBUG_LOG('Player - FAILED: {}'.format(self.handler)) if not self.handler: return @@ -1342,6 +1403,8 @@ def onPlayBackFailed(self): # xbmcgui.Dialog().ok('Failed', 'Playback failed') def onVideoWindowOpened(self): + if not self.sessionID: + return util.DEBUG_LOG('Player: Video window opened') try: self.handler.onVideoWindowOpened() @@ -1349,6 +1412,8 @@ def onVideoWindowOpened(self): util.ERROR() def onVideoWindowClosed(self): + if not self.sessionID: + return util.DEBUG_LOG('Player: Video window closed') try: self.handler.onVideoWindowClosed() @@ -1357,6 +1422,8 @@ def onVideoWindowClosed(self): util.ERROR() def onVideoOSD(self): + if not self.sessionID: + return util.DEBUG_LOG('Player: Video OSD opened') try: self.handler.onVideoOSD() @@ -1364,6 +1431,8 @@ def onVideoOSD(self): util.ERROR() def onSeekOSD(self): + if not self.sessionID: + return util.DEBUG_LOG('Player: Seek OSD opened') try: self.handler.onSeekOSD() @@ -1390,15 +1459,22 @@ def _monitor(self): if not self.isPlaying(): util.DEBUG_LOG('Player: Idling...') - while not self.isPlaying() and not util.MONITOR.abortRequested() and not self._closed: + while not util.MONITOR.abortRequested() and not self._closed and \ + (not self.isPlaying() or (self.isPlaying() and not self.sessionID)): util.MONITOR.waitForAbort(0.1) if self.isPlayingVideo(): util.DEBUG_LOG('Monitoring video...') self._videoMonitor() elif self.isPlayingAudio(): - util.DEBUG_LOG('Monitoring audio...') - self._audioMonitor() + if self.bgmPlaying: + util.DEBUG_LOG('Monitoring BGM...') + while self.isPlayingAudio() and self.bgmPlaying and not util.MONITOR.abortRequested() and \ + not self._closed: + util.MONITOR.waitForAbort(0.1) + else: + util.DEBUG_LOG('Monitoring audio...') + self._audioMonitor() elif self.isPlaying(): util.DEBUG_LOG('Monitoring pre-play...') diff --git a/script.plexmod/lib/util.py b/script.plexmod/lib/util.py index 8cf96b219..08197fd3b 100644 --- a/script.plexmod/lib/util.py +++ b/script.plexmod/lib/util.py @@ -23,6 +23,7 @@ from kodi_six import xbmcvfs from . import colors +# noinspection PyUnresolvedReferences from .exceptions import NoDataException from plexnet import signalsmixin, plexapp @@ -37,17 +38,29 @@ # buildversion looks like: XX.X[-TAG] (a+.b+.c+) (.+); there are kodi builds that don't set the build version sys_ver = xbmc.getInfoLabel('System.BuildVersion') _ver = sys_ver -if ' ' in sys_ver and '(' in sys_ver: - _ver, _build = sys_ver.split()[:2] -_splitver = _ver.split(".") -KODI_VERSION_MAJOR, KODI_VERSION_MINOR = int(_splitver[0].split("-")[0].strip()), \ - int(_splitver[1].split("-")[0].strip()) +try: + if ' ' in sys_ver and '(' in sys_ver: + _ver, _build = sys_ver.split()[:2] + + _splitver = _ver.split(".") + KODI_VERSION_MAJOR, KODI_VERSION_MINOR = int(_splitver[0].split("-")[0].strip()), \ + int(_splitver[1].split(" ")[0].split("-")[0].strip()) +except: + xbmc.log('script.plex: Couldn\'t determine Kodi version, assuming 19.4. Got: {}'.format(sys_ver)) + # assume something "old" + KODI_VERSION_MAJOR = 19 + KODI_VERSION_MINOR = 4 _bmajor, _bminor, _bpatch = (KODI_VERSION_MAJOR, KODI_VERSION_MINOR, 0) +parsedBuild = False if _build: - _bmajor, _bminor, _bpatch = _build[1:-1].split(".") -else: + try: + _bmajor, _bminor, _bpatch = _build[1:-1].split(".") + parsedBuild = True + except: + pass +if not parsedBuild: xbmc.log('script.plex: Couldn\'t determine build version, falling back to Kodi version', xbmc.LOGINFO) # calculate a comparable build number @@ -140,7 +153,7 @@ class AdvancedSettings(object): ("auto_seek", True), ("auto_seek_delay", 1), ("dynamic_timeline_seek", False), - ("fast_back", False), + ("fast_back", True), ("dynamic_backgrounds", True), ("background_art_blur_amount2", 0), ("background_art_opacity_amount2", 20), @@ -173,6 +186,8 @@ class AdvancedSettings(object): ("subtitle_use_extended_title", True), ("poster_resolution_scale_perc", 100), ("consecutive_video_pb_wait", 0.0), + ("retrieve_all_media_up_front", False), + ("library_chunk_size", 240), ) def __init__(self): @@ -271,15 +286,13 @@ def onNotification(self, sender, method, data): setGlobalProperty('stop_running', '1') return if kodigui.BaseFunctions.lastWinID > 13000: + reInitAddon() xbmc.executebuiltin('ActivateWindow({0})'.format(kodigui.BaseFunctions.lastWinID)) else: ERROR("Addon never properly started, can't reactivate") setGlobalProperty('stop_running', '1') return - getAdvancedSettings() - populateTimeFormat() - elif sender == "xbmc" and method == "System.OnSleep" and getSetting('action_on_sleep', "none") != "none": getattr(self, "action{}".format(getSetting('action_on_sleep', "none").capitalize()))() @@ -296,6 +309,10 @@ def onDPMSActivated(self): DEBUG_LOG("Monitor: OnDPMSActivated") #self.stopPlayback() + def onSettingsChanged(self): + """ unused stub, but works if needed """ + pass + MONITOR = UtilityMonitor() @@ -472,6 +489,14 @@ def getAdvancedSettings(): advancedSettings = AdvancedSettings() +def reInitAddon(): + global ADDON + # reinit the ADDON reference so we get the updated addon settings + ADDON = xbmcaddon.Addon() + getAdvancedSettings() + populateTimeFormat() + + def setSetting(key, value): with SETTINGS_LOCK: value = _processSettingForWrite(value) @@ -940,10 +965,16 @@ def getPlatform(): return key.rsplit('.', 1)[-1] -def getProgressImage(obj): - if not obj.get('viewOffset') or not obj.get('duration'): +def getProgressImage(obj, perc=None): + if not obj and not perc: return '' - pct = int((obj.viewOffset.asInt() / obj.duration.asFloat()) * 100) + + if obj: + if not obj.get('viewOffset') or not obj.get('duration'): + return '' + pct = int((obj.viewOffset.asInt() / obj.duration.asFloat()) * 100) + else: + pct = perc pct = pct - pct % 2 # Round to even number - we have even numbered progress only pct = max(pct, 2) return 'script.plex/progress/{0}.png'.format(pct) @@ -1000,6 +1031,18 @@ def getOpenSubtitlesHash(size, url): return format(hash_, "016x") +def ensureHome(): + if xbmcgui.getCurrentWindowId() != 10000: + LOG("Switching to home screen before starting addon") + xbmc.executebuiltin('ActivateWindow(home)') + ct = 0 + while xbmcgui.getCurrentWindowId() != 10000 and ct <= 50: + xbmc.Monitor().waitForAbort(0.1) + ct += 1 + if ct > 50: + DEBUG_LOG("Still active window: %s" % xbmcgui.getCurrentWindowId()) + + def garbageCollect(): gc.collect(2) diff --git a/script.plexmod/lib/windows/dropdown.py b/script.plexmod/lib/windows/dropdown.py index dc973b02b..aefb7f0e1 100644 --- a/script.plexmod/lib/windows/dropdown.py +++ b/script.plexmod/lib/windows/dropdown.py @@ -14,6 +14,8 @@ class DropdownDialog(kodigui.BaseDialog): res = '1080i' width = 1920 height = 1080 + dropWidth = 360 + borderOff = -20 GROUP_ID = 100 OPTIONS_LIST_ID = 250 @@ -41,7 +43,7 @@ def __init__(self, *args, **kwargs): @property def x(self): - return min(self.width - 360, self.pos[0]) + return min(self.width - self.dropWidth - self.borderOff, self.pos[0]) @property def y(self): @@ -188,6 +190,7 @@ def showOptions(self): class DropdownHeaderDialog(DropdownDialog): xmlFile = 'script-plex-dropdown_header.xml' + dropWidth = 660 def showDropdown( @@ -204,6 +207,7 @@ def showDropdown( header=None, select_index=None, onclose_callback=None, + dialog_props=None ): if header: @@ -222,6 +226,7 @@ def showDropdown( header=header, select_index=select_index, onclose_callback=onclose_callback, + dialog_props=dialog_props, ) else: pos = pos or (810, 400) @@ -239,6 +244,7 @@ def showDropdown( header=header, select_index=select_index, onclose_callback=onclose_callback, + dialog_props=dialog_props, ) choice = w.choice w = None diff --git a/script.plexmod/lib/windows/episodes.py b/script.plexmod/lib/windows/episodes.py index 0ed790439..d6c6ed391 100644 --- a/script.plexmod/lib/windows/episodes.py +++ b/script.plexmod/lib/windows/episodes.py @@ -27,7 +27,7 @@ from . import playbacksettings from lib.util import T -from .mixins import SeasonsMixin +from .mixins import SeasonsMixin, RatingsMixin VIDEO_RELOAD_KW = dict(includeExtras=1, includeExtrasCount=10, includeChapters=1) @@ -174,7 +174,7 @@ def getData(self, offset, amount): return self.parentWindow.show_.getRelated(offset=offset, limit=amount) -class EpisodesWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, +class EpisodesWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, RatingsMixin, playbacksettings.PlaybackSettingsMixin): xmlFile = 'script-plex-episodes.xml' path = util.ADDON.getAddonInfo('path') @@ -206,6 +206,7 @@ class EpisodesWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMix PROGRESS_IMAGE_ID = 250 PLAY_BUTTON_ID = 301 + PLAY_BUTTON_DISABLED_ID = 306 SHUFFLE_BUTTON_ID = 302 OPTIONS_BUTTON_ID = 303 INFO_BUTTON_ID = 304 @@ -229,6 +230,7 @@ def __init__(self, *args, **kwargs): self.cameFrom = kwargs.get('came_from') self.tasks = backgroundthread.Tasks() self.initialized = False + self.currentItemLoaded = False self.closing = False self._reloadVideos = [] @@ -236,7 +238,8 @@ def reset(self, episode, season=None, show=None): self.episode = episode self.season = season if season is not None else self.episode.season() try: - self.show_ = show or (self.episode or self.season).show().reload(includeExtras=1, includeExtrasCount=10) + self.show_ = show or (self.episode or self.season).show().reload(includeExtras=1, includeExtrasCount=10, + includeOnDeck=1) except IndexError: raise util.NoDataException @@ -274,7 +277,7 @@ def _onFirstInit(self): def doAutoPlay(self): # First reload the video to get all the other info self.initialEpisode.reload(checkFiles=1, **VIDEO_RELOAD_KW) - return self.playButtonClicked(force_episode=self.initialEpisode) + return self.playButtonClicked(force_episode=self.initialEpisode, from_auto_play=True) def onFirstInit(self): self._onFirstInit() @@ -320,7 +323,13 @@ def onReInit(self): def postSetup(self, from_select_episode=False): self.selectEpisode(from_select_episode=from_select_episode) self.checkForHeaderFocus(xbmcgui.ACTION_MOVE_DOWN) - self.setFocusId(self.PLAY_BUTTON_ID) + + selected = self.episodeListControl.getSelectedItem() + if selected: + self.setFocusId(self.getPlayButtonID(selected, base=not self.currentItemLoaded + and self.PLAY_BUTTON_DISABLED_ID or None) + ) + self.initialized = True @busy.dialog() @@ -391,6 +400,9 @@ def onAction(self, action): if controlID == self.EPISODE_LIST_ID: if self.checkForHeaderFocus(action): return + elif action == xbmcgui.ACTION_CONTEXT_MENU: + self.optionsButtonClicked(from_item=True) + return elif controlID == self.RELATED_LIST_ID: if self.relatedPaginator.boundaryHit: @@ -518,7 +530,7 @@ def onFocus(self, controlID): elif xbmc.getCondVisibility('ControlGroup(50).HasFocus(0) + !ControlGroup(300).HasFocus(0) + !ControlGroup(1300).HasFocus(0)'): self.setProperty('on.extras', '1') - if player.PLAYER.bgmPlaying and player.PLAYER.handler.currentlyPlaying != self.season.show().ratingKey: + if player.PLAYER.bgmPlaying and player.PLAYER.handler.currentlyPlaying != self.show_.ratingKey: player.PLAYER.stopAndWait() def openItem(self, control=None, item=None, came_from=None): @@ -655,7 +667,7 @@ def searchButtonClicked(self): section_id = self.show_.getLibrarySectionId() self.processCommand(search.dialog(self, section_id=section_id or None)) - def playButtonClicked(self, shuffle=False, force_episode=None): + def playButtonClicked(self, shuffle=False, force_episode=None, from_auto_play=False): if shuffle: seasonOrShow = self.season or self.show_ items = seasonOrShow.all() @@ -666,7 +678,7 @@ def playButtonClicked(self, shuffle=False, force_episode=None): return True else: - return self.episodeListClicked(force_episode=force_episode) + return self.episodeListClicked(force_episode=force_episode, from_auto_play=from_auto_play) def shuffleButtonClicked(self): self.playButtonClicked(shuffle=True) @@ -708,7 +720,10 @@ def infoButtonClicked(self): video=episode ) - def episodeListClicked(self, force_episode=None): + def episodeListClicked(self, force_episode=None, from_auto_play=False): + if not self.currentItemLoaded and not from_auto_play: + return + if not force_episode: mli = self.episodeListControl.getSelectedItem() if not mli or mli.getProperty("is.boundary"): @@ -732,7 +747,8 @@ def episodeListClicked(self, force_episode=None): pos=(660, 441), close_direction='none', set_dropdown_prop=False, - header=T(32314, 'In Progress') + header=T(32314, 'In Progress'), + dialog_props=from_auto_play and self.dialogProps or None ) if not choice: @@ -802,38 +818,33 @@ def optionsButtonClicked(self, from_item=False): pos = (500, 620) bottom = False - setDropdownProp = False if from_item: viewPos = self.episodeListControl.getViewPosition() - if viewPos > 6: - pos = (1490, 312 + (viewPos * 100)) - bottom = True - else: - pos = (1490, 167 + (viewPos * 100)) - bottom = False - setDropdownProp = True + optsLen = len(list(filter(None, options))) + # dropDown handles any overlap with the right window boundary so we don't need to care here + pos = ((((viewPos + 1) * 359) - 100), 649 if optsLen < 7 else 649 - 66 * (optsLen - 6)) choice = dropdown.showDropdown(options, pos, pos_is_bottom=bottom, close_direction='left', - set_dropdown_prop=setDropdownProp) + set_dropdown_prop=False) if not choice: return if choice['key'] == 'play_next': xbmc.executebuiltin('PlayerControl(Next)') elif choice['key'] == 'mark_watched': - mli.dataSource.markWatched() + mli.dataSource.markWatched(**VIDEO_RELOAD_KW) self.updateItems(mli) util.MONITOR.watchStatusChanged() elif choice['key'] == 'mark_unwatched': - mli.dataSource.markUnwatched() + mli.dataSource.markUnwatched(**VIDEO_RELOAD_KW) self.updateItems(mli) util.MONITOR.watchStatusChanged() elif choice['key'] == 'mark_season_watched': - self.season.markWatched() + self.season.markWatched(**VIDEO_RELOAD_KW) self.updateItems() util.MONITOR.watchStatusChanged() elif choice['key'] == 'mark_season_unwatched': - self.season.markUnwatched() + self.season.markUnwatched(**VIDEO_RELOAD_KW) self.updateItems() util.MONITOR.watchStatusChanged() elif choice['key'] == 'to_show': @@ -939,6 +950,7 @@ def checkForHeaderFocus(self, action): def updateProperties(self): showTitle = self.show_ and self.show_.title or '' + self.setBoolProperty('current_item.loaded', False) self.updateBackgroundFrom(self.show_ or self.season.show()) self.setProperty('season.thumb', (self.season or self.show_).thumb.asTranscodedImageURL(*self.POSTER_DIM)) self.setProperty('show.title', showTitle) @@ -992,28 +1004,7 @@ def setItemInfo(self, video, mli): mli.setProperty('content.rating', video.contentRating.split('/', 1)[-1]) mli.setProperty('genre', self.genre) - if video.get('userRating'): - stars = str(int(round((video.userRating.asFloat() / 10) * 5))) - mli.setProperty('rating.stars', stars) - # elif video.rating: - # stars = str(int(round((video.rating.asFloat() / 10) * 5))) - # mli.setProperty('rating.stars', stars) - - if video.get('ratingImage'): - rating = video.rating - audienceRating = video.audienceRating - if video.ratingImage.startswith('rottentomatoes:'): - rating = '{0}%'.format(int(rating.asFloat() * 10)) - if audienceRating: - audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) - - mli.setProperty('rating', rating) - mli.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(video.ratingImage.replace('://', '/'))) - if video.get('audienceRatingImage'): - mli.setProperty('rating2', audienceRating) - mli.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(video.audienceRatingImage.replace('://', '/'))) - else: - mli.setProperty('rating', video.rating) + self.populateRatings(video, mli) def setPostReloadItemInfo(self, video, mli): self.setItemAudioAndSubtitleInfo(video, mli) @@ -1026,6 +1017,15 @@ def setPostReloadItemInfo(self, video, mli): mli.setBoolProperty('unavailable', not video.available()) mli.setBoolProperty('media.multiple', len(list(filter(lambda x: x.isAccessible(), video.media()))) > 1) + directors = u' / '.join([d.tag for d in video.directors()][:2]) + directorsLabel = len(video.directors) > 1 and T(32401, u'DIRECTORS').upper() or T(32383, + u'DIRECTOR').upper() + mli.setProperty('directors', directors and u'{0} {1}'.format(directorsLabel, directors) or '') + writers = u' / '.join([r.tag for r in video.writers()][:2]) + writersLabel = len(video.writers) > 1 and T(32403, u'WRITERS').upper() or T(32402, u'WRITER').upper() + mli.setProperty('writers', + writers and u'{0}{1} {2}'.format(directors and ' ' or '', writersLabel, writers) or '') + def setItemAudioAndSubtitleInfo(self, video, mli): sas = video.selectedAudioStream() @@ -1101,6 +1101,9 @@ def reloadItems(self, items, with_progress=False): backgroundthread.BGThreader.addTasks(tasks) + def getPlayButtonID(self, mli, base=None): + return (base and base or self.PLAY_BUTTON_ID) + (mli.getProperty('media.multiple') and 1000 or 0) + def reloadItemCallback(self, task, episode, with_progress=False): self.tasks.remove(task) del task @@ -1126,6 +1129,22 @@ def reloadItemCallback(self, task, episode, with_progress=False): if mli == selected: self.lastItem = mli self.setProgress(mli) + + if not self.currentItemLoaded and ( + mli == selected or (self.episode and self.episode == mli.dataSource)): + self.currentItemLoaded = True + self.setBoolProperty('current_item.loaded', True) + if not self.lastFocusID or self.lastFocusID in ( + self.PLAY_BUTTON_DISABLED_ID, self.PLAY_BUTTON_DISABLED_ID + 1000): + # wait for visibility of the button + tries = 0 + PBID = self.getPlayButtonID(mli) + while not xbmc.getCondVisibility('Control.IsVisible({})'.format(PBID)) \ + and not util.MONITOR.abortRequested() and tries < 5: + util.MONITOR.waitForAbort(0.1) + tries += 1 + if xbmc.getCondVisibility('Control.IsVisible({})'.format(PBID)): + self.setFocusId(PBID) return def fillExtras(self, has_prev=False): diff --git a/script.plexmod/lib/windows/home.py b/script.plexmod/lib/windows/home.py index 5bbc84152..a5612ba73 100644 --- a/script.plexmod/lib/windows/home.py +++ b/script.plexmod/lib/windows/home.py @@ -349,6 +349,7 @@ def __init__(self, *args, **kwargs): self.sectionChangeTimeout = 0 self.lastFocusID = None self.lastNonOptionsFocusID = None + self._lastSelectedItem = None self.sectionHubs = {} self.updateHubs = {} self.changingServer = False @@ -512,6 +513,7 @@ def shutdown(self): def storeLastBG(self): if util.advancedSettings.dynamicBackgrounds: + oldbg = util.getSetting("last_bg_url", "") # store BG url of first hub, first item, as this is most likely to be the one we're focusing on the # next start try: @@ -524,6 +526,11 @@ def storeLastBG(self): if not ds.art: continue + if oldbg: + url = plexnet.compat.quote_plus(ds.art) + if url in oldbg: + return + bg = util.backgroundFromArt(ds.art, width=self.width, height=self.height) if bg: util.DEBUG_LOG('Storing BG for {0}, "{1}"'.format(self.hubControls[index].dataSource, @@ -578,7 +585,8 @@ def onAction(self, action): self.setFocusId(self.SERVER_BUTTON_ID) elif 399 < controlID < 500: if action.getId() in MOVE_SET: - self.checkHubItem(controlID) + self.checkHubItem(controlID, actionID=action.getId()) + return elif action.getId() == xbmcgui.ACTION_PLAYER_PLAY: self.hubItemClicked(controlID, auto_play=True) return @@ -598,6 +606,11 @@ def onAction(self, action): self.setFocusId(self.SERVER_BUTTON_ID) return + if controlID == self.SECTION_LIST_ID and self.sectionList.control.getSelectedPosition() > 0: + self.sectionList.setSelectedItemByPos(0) + self.showHubs(HomeSection) + return + if util.advancedSettings.fastBack and not optionsFocused and offSections \ and self.lastFocusID not in (self.USER_BUTTON_ID, self.SERVER_BUTTON_ID, self.SEARCH_BUTTON_ID, self.SECTION_LIST_ID): @@ -605,7 +618,7 @@ def onAction(self, action): self.setFocusId(self.SECTION_LIST_ID) return - if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): + if action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): if not optionsFocused and offSections \ and (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): self.lastNonOptionsFocusID = self.lastFocusID @@ -623,6 +636,7 @@ def onAction(self, action): if ex.button in (2, None): return elif ex.button == 1: + self.storeLastBG() xbmc.executebuiltin('ActivateWindow(10000)') return elif ex.button == 0: @@ -752,8 +766,17 @@ def hubItemClicked(self, hubControlID, auto_play=False): if mli.dataSource is None: return + carryProps = None + if auto_play and self.hubControls: + # carry over some props to the new window as we might end up showing a resume dialog not rendering the + # underlying window. the new window class will invalidate the old one temporarily, though, as it seems + # and the properties vanish, resulting in all text2lines enabled hubs to lose their title2 labels + carryProps = dict( + ('hub.text2lines.4{0:02d}'.format(i), '1') for i, hubCtrl in enumerate(self.hubControls) if + hubCtrl.dataSource and self.HUBMAP[hubCtrl.dataSource.getCleanHubIdentifier()].get("text2lines")) + try: - command = opener.open(mli.dataSource, auto_play=auto_play) + command = opener.open(mli.dataSource, auto_play=auto_play, dialog_props=carryProps) if command == "NODATA": raise util.NoDataException except util.NoDataException: @@ -765,7 +788,9 @@ def hubItemClicked(self, hubControlID, auto_play=False): if not mli: return - if not mli.dataSource.exists(): + # MediaItem.exists checks for the deleted and deletedAt flags. We still want to show the media if it's still + # valid, but has deleted files. Do a more thorough check for existence in this case + if not mli.dataSource.exists() and not mli.dataSource.exists(force_full_check=True): try: control.removeItem(mli.pos()) except (ValueError, TypeError): @@ -808,19 +833,33 @@ def checkSectionItem(self, force=False, action=None): self.sectionList.selectItem(self.bottomItem) item = self.sectionList[self.bottomItem] + if item.getProperty('is.home'): + self.storeLastBG() + if item.dataSource != self.lastSection: self.lastSection = item.dataSource self.sectionChanged(force) - def checkHubItem(self, controlID): + def checkHubItem(self, controlID, actionID=None): control = self.hubControls[controlID - 400] mli = control.getSelectedItem() is_valid_mli = mli and mli.getProperty('is.end') != '1' + is_last_item = is_valid_mli and control.isLastItem(mli) if util.advancedSettings.dynamicBackgrounds and is_valid_mli: self.updateBackgroundFrom(mli.dataSource) if not mli or not mli.getProperty('is.end') or mli.getProperty('is.updating') == '1': + mlipos = control.getManagedItemPosition(mli) + + # in order to not round robin when the next chunk is loading, implement our own cheap round robining + # by storing the last selected item of the current control. if we've seen it twice, we need to wrap around + if mli and not mli.getProperty('is.end') and is_last_item and actionID == xbmcgui.ACTION_MOVE_RIGHT: + if (controlID, mlipos) == self._lastSelectedItem: + control.selectItem(0) + self._lastSelectedItem = None + else: + self._lastSelectedItem = (controlID, mlipos) return mli.setBoolProperty('is.updating', True) @@ -855,7 +894,7 @@ def cleanTasks(self): self.tasks = [t for t in self.tasks if t.isValid()] def sectionChanged(self, force=False): - self.sectionChangeTimeout = time.time() + 0.3 + self.sectionChangeTimeout = time.time() + 0.5 if not self.sectionChangeThread or not self.sectionChangeThread.is_alive() or force: self.sectionChangeThread = threading.Thread(target=self._sectionChanged, name="sectionchanged") self.sectionChangeThread.start() @@ -894,6 +933,10 @@ def updateHubCallback(self, hub, items=None): continue hubs = self.sectionHubs.get(section.key, ()) + if not hubs: + util.LOG("Hubs for {} not found/no data".format(section.key)) + continue + for idx, ihub in enumerate(hubs): if ihub == hub: if self.lastSection == section: diff --git a/script.plexmod/lib/windows/info.py b/script.plexmod/lib/windows/info.py index 4c3374cd2..7c610b9ca 100644 --- a/script.plexmod/lib/windows/info.py +++ b/script.plexmod/lib/windows/info.py @@ -91,10 +91,22 @@ def getVideoInfo(self): streamtype = stream.streamType.asInt() # video if streamtype == 1: - addMedia.append("Video: {}x{}, {} {}/{}bit/{}/{}@{} kBit, {} fps\n".format( + dovi = "" + if stream.DOVIPresent: + dovi = "Level: {}, Profile: {}, Version: {}, " \ + "BL: {}{}, EL: {}, RPU: {}".format(stream.DOVILevel, + stream.DOVIProfile, + stream.DOVIVersion, + stream.DOVIBLPresent, + stream.DOVIBLPresent and + " (compat ID: {})".format(stream.DOVIBLCompatID) + or "", + stream.DOVIELPresent, + stream.DOVIRPUPresent) + addMedia.append("Video: {}x{}, {} {}/{}bit/{}/{}@{} kBit, {} fps{}\n".format( stream.width, stream.height, stream.videoCodecRendering, stream.codec.upper(), stream.bitDepth, stream.chromaSubsampling, stream.colorPrimaries, stream.bitrate, - stream.frameRate)) + stream.frameRate, dovi and "\nDoVi: {}\n".format(dovi) or "")) # audio elif streamtype == 2: addMedia.append("Audio: {}{}, {}/{}ch@{} kBit, {} Hz\n".format( diff --git a/script.plexmod/lib/windows/kodigui.py b/script.plexmod/lib/windows/kodigui.py index 19c55804c..ff50b6121 100644 --- a/script.plexmod/lib/windows/kodigui.py +++ b/script.plexmod/lib/windows/kodigui.py @@ -106,6 +106,11 @@ def __init__(self, *args, **kwargs): self._winID = None self.started = False self.finishedInit = False + self.dialogProps = kwargs.get("dialog_props", None) + + carryProps = kwargs.get("window_props", None) + if carryProps: + self.setProperties(list(carryProps.keys()), list(carryProps.values())) def onInit(self): global LAST_BG_URL @@ -216,6 +221,10 @@ def __init__(self, *args, **kwargs): self._winID = '' self.started = False + carryProps = kwargs.get("dialog_props", None) + if carryProps: + self.setProperties(list(carryProps.keys()), list(carryProps.values())) + def onInit(self): self._winID = xbmcgui.getCurrentWindowDialogId() BaseFunctions.lastDialogID = self._winID @@ -304,7 +313,7 @@ def __nonzero__(self): __bool__ = __nonzero__ - def exists(self): + def exists(self, *args, **kwargs): return False @@ -445,6 +454,15 @@ def setProperty(self, key, value): self.listItem.setProperty(key, value) return self + def setProperties(self, prop_list, val_list_or_val): + if isinstance(val_list_or_val, list) or isinstance(val_list_or_val, tuple): + val_list = val_list_or_val + else: + val_list = [val_list_or_val] * len(prop_list) + + for prop, val in zip(prop_list, val_list): + self.setProperty(prop, val) + def setBoolProperty(self, key, boolean): return self.setProperty(key, boolean and '1' or '') diff --git a/script.plexmod/lib/windows/library.py b/script.plexmod/lib/windows/library.py index adad2b296..605686317 100644 --- a/script.plexmod/lib/windows/library.py +++ b/script.plexmod/lib/windows/library.py @@ -29,9 +29,6 @@ import six from six.moves import range -CHUNK_SIZE = 200 -# CHUNK_SIZE = 30 - KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' MOVE_SET = frozenset( @@ -301,136 +298,17 @@ def setSetting(self, setting, value): self._saveSettings() -class ChunkedWrapList(kodigui.ManagedControlList): - LIST_MAX = CHUNK_SIZE * 3 - - def __getitem__(self, idx): - # if isinstance(idx, slice): - # return self.items[idx] - # else: - idx = idx % self.LIST_MAX - return self.items[idx] - # return self.getListItem(idx) - - -class ChunkModeWrapped(object): - ALL_MAX = CHUNK_SIZE * 2 - - def __init__(self): - self.reset() - - def reset(self): - self.midStart = 0 - self.itemCount = 0 - self.keys = {} - - def addKeyRange(self, key, krange): - self.keys[key] = krange - - def getKey(self, pos): - for k, krange in self.keys.items(): - if krange[0] <= pos <= krange[1]: - return k - - def isAtBeginning(self): - return self.midStart == 0 - - def posIsForward(self, pos): - if self.itemCount <= self.ALL_MAX: - return False - return pos >= self.midStart + CHUNK_SIZE - - def posIsBackward(self, pos): - if self.itemCount <= self.ALL_MAX: - return False - return pos < self.midStart - - def posIsValid(self, pos): - return self.midStart - CHUNK_SIZE <= pos < self.midStart + (CHUNK_SIZE * 2) - - def shift(self, mod): - if mod < 0 and self.midStart == 0: - return None - elif mod > 0 and self.midStart + CHUNK_SIZE >= self.itemCount: - return None - - offset = CHUNK_SIZE * mod - self.midStart += offset - start = self.midStart + offset - - return start - - def shiftToKey(self, key, keyStart=None): - if keyStart is None: - if key not in self.keys: - util.DEBUG_LOG('CHUNK MODE: NO ITEMS FOR KEY') - return - - keyStart = self.keys[key][0] - self.midStart = keyStart - keyStart % CHUNK_SIZE - return keyStart, max(self.midStart - CHUNK_SIZE, 0) - - def addObjects(self, pos, objects): - if not self.posIsValid(pos): - return - - if pos == self.midStart - CHUNK_SIZE: - self.objects = objects + self.objects[CHUNK_SIZE:] - elif pos == self.midStart: - self.objects = self.objects[:CHUNK_SIZE] + objects + self.objects[CHUNK_SIZE * 2:] - elif pos == self.midStart + CHUNK_SIZE: - self.objects = self.objects[:CHUNK_SIZE * 2] + objects - - -class CustomScrollBar(object): - def __init__(self, window, bar_group_id, bar_image_id, bar_image_focus_id, button_id, min_bar_height=20): - self._barGroup = window.getControl(bar_group_id) - self._barImage = window.getControl(bar_image_id) - self._barImageFocus = window.getControl(bar_image_focus_id) - self._button = window.getControl(button_id) - self.height = self._button.getHeight() - self.x, self.y = self._barGroup.getPosition() - self._minBarHeight = min_bar_height - self._barHeight = min_bar_height - self.reset() - - def reset(self): - self.size = 0 - self.count = 0 - self.pos = 0 - - def setSizeAndCount(self, size, count): - self.size = size - self.count = count - self._barHeight = min(self.height, max(self._minBarHeight, int(self.height * (count / float(size))))) - self._moveHeight = self.height - self._barHeight - self._barImage.setHeight(self._barHeight) - self._barImageFocus.setHeight(self._barHeight) - self.setPosition(0) - - def setPosition(self, pos): - self.pos = pos - offset = int((pos / float(max(self.size, 2) - 1)) * self._moveHeight) - self._barGroup.setPosition(self.x, self.y + offset) - - def getPosFromY(self, y): - y -= int(self._barHeight / 2) + 150 - y = min(max(y, 0), self._moveHeight) - return int((self.size - 1) * (y / float(self._moveHeight))) - - def onMouseDrag(self, window, action): - y = window.mouseYTrans(action.getAmount2()) - y -= int(self._barHeight / 2) + 150 - y = min(max(y, 0), self._moveHeight) - self._barGroup.setPosition(self.x, self.y) - - class LibraryWindow(kodigui.MultiWindow, windowutils.UtilMixin): bgXML = 'script-plex-blank.xml' path = util.ADDON.getAddonInfo('path') theme = 'Main' res = '1080i' + # Needs to be an even multiple of 6(posters) and 10(small posters) and 12(list) + # so that we fill an entire row + CHUNK_SIZE = 240 + CHUNK_OVERCOMMIT = 6 + def __init__(self, *args, **kwargs): kodigui.MultiWindow.__init__(self, *args, **kwargs) windowutils.UtilMixin.__init__(self) @@ -467,29 +345,22 @@ def reset(self): self.sort = self.librarySettings.getSetting('sort', 'titleSort') self.sortDesc = self.librarySettings.getSetting('sort.desc', False) - self.chunkMode = None - #if ITEM_TYPE in ('episode', 'album'): - # self.chunkMode = ChunkModeWrapped() + self.alreadyFetchedChunkList = set() + self.finalChunkPosition = 0 + + self.CHUNK_SIZE = util.advancedSettings.libraryChunkSize key = self.section.key if not key.isdigit(): key = self.section.getLibrarySectionId() viewtype = util.getSetting('viewtype.{0}.{1}'.format(self.section.server.uuid, key)) - if self.chunkMode: - if self.section.TYPE in ('artist', 'photo', 'photodirectory'): - self.setWindows(VIEWS_SQUARE_CHUNKED.get('all')) - self.setDefault(VIEWS_SQUARE_CHUNKED.get(viewtype)) - else: - self.setWindows(VIEWS_POSTER_CHUNKED.get('all')) - self.setDefault(VIEWS_POSTER_CHUNKED.get(viewtype)) + if self.section.TYPE in ('artist', 'photo', 'photodirectory'): + self.setWindows(VIEWS_SQUARE.get('all')) + self.setDefault(VIEWS_SQUARE.get(viewtype)) else: - if self.section.TYPE in ('artist', 'photo', 'photodirectory'): - self.setWindows(VIEWS_SQUARE.get('all')) - self.setDefault(VIEWS_SQUARE.get(viewtype)) - else: - self.setWindows(VIEWS_POSTER.get('all')) - self.setDefault(VIEWS_POSTER.get(viewtype)) + self.setWindows(VIEWS_POSTER.get('all')) + self.setDefault(VIEWS_POSTER.get(viewtype)) @busy.dialog() def doClose(self): @@ -497,10 +368,6 @@ def doClose(self): kodigui.MultiWindow.doClose(self) def onFirstInit(self): - self.scrollBar = None - #if ITEM_TYPE in ('episode', 'album'): - # self.scrollBar = CustomScrollBar(self, 950, 952, 953, 951) - if self.showPanelControl and not self.refill: self.showPanelControl.newControl(self) self.keyListControl.newControl(self) @@ -508,10 +375,7 @@ def onFirstInit(self): self.setFocusId(self.VIEWTYPE_BUTTON_ID) self.setBoolProperty("initialized", True) else: - if self.chunkMode: - self.showPanelControl = ChunkedWrapList(self, self.POSTERS_PANEL_ID, 5) - else: - self.showPanelControl = kodigui.ManagedControlList(self, self.POSTERS_PANEL_ID, 5) + self.showPanelControl = kodigui.ManagedControlList(self, self.POSTERS_PANEL_ID, 5) hideFilterOptions = self.section.TYPE == 'photodirectory' or self.section.TYPE == 'collection' @@ -544,24 +408,17 @@ def onAction(self, action): self.setBoolProperty('dragging', self.dragging) if action.getId() in MOVE_SET: + mli = self.showPanelControl.getSelectedItem() + if mli: + self.requestChunk(mli.pos()) + if util.advancedSettings.dynamicBackgrounds: - mli = self.showPanelControl.getSelectedItem() if mli and mli.dataSource: self.updateBackgroundFrom(mli.dataSource) controlID = self.getFocusId() if controlID == self.POSTERS_PANEL_ID or controlID == self.SCROLLBAR_ID: self.updateKey() - self.checkChunkedNav(action) - elif controlID == self.CUSTOM_SCOLLBAR_BUTTON_ID: - if action == xbmcgui.ACTION_MOVE_UP: - self.shiftSelection(-12) - elif action == xbmcgui.ACTION_MOVE_DOWN: - self.shiftSelection(12) - # elif action == xbmcgui.KEY_MOUSE_DRAG_START: - # self.onMouseDragStart(action) - # elif action == xbmcgui.KEY_MOUSE_DRAG_END: - # self.onMouseDragEnd(action) elif action == xbmcgui.ACTION_MOUSE_DRAG: self.onMouseDrag(action) elif action == xbmcgui.ACTION_CONTEXT_MENU: @@ -633,83 +490,6 @@ def onItemChanged(self, mli): self.showPhotoItemProperties(mli.dataSource) - def onMouseDragStart(self, action): - if not self.scrollBar: - return - - controlID = self.getFocusId() - if controlID != self.CUSTOM_SCOLLBAR_BUTTON_ID: - return - - self.dragging = True - self.setBoolProperty('dragging', self.dragging) - - def onMouseDragEnd(self, action): - if not self.scrollBar: - return - - if not self.dragging: - return - - self.dragging = False - self.setBoolProperty('dragging', self.dragging) - - y = self.mouseYTrans(action.getAmount2()) - - pos = self.scrollBar.getPosFromY(y) - self.shiftSelection(pos=pos) - - def onMouseDrag(self, action): - if not self.scrollBar: - return - - if not self.dragging: - controlID = self.getFocusId() - if controlID != self.CUSTOM_SCOLLBAR_BUTTON_ID: - return - - self.onMouseDragStart(action) - if not self.dragging: - return - - # self.scrollBar.onMouseDrag(self, action) - - y = self.mouseYTrans(action.getAmount2()) - - pos = self.scrollBar.getPosFromY(y) - if self.chunkMode.posIsForward(pos) or self.chunkMode.posIsBackward(pos): - self.shiftSelection(pos=pos) - else: - self.showPanelControl.selectItem(pos) - self.checkChunkedNav() - - def shiftSelection(self, offset=0, pos=None): - if pos is not None: - self.scrollBar.setPosition(pos) - return self.delayedChunkedPosJump(pos) - else: - mli = self.showPanelControl.getSelectedItem() - - try: - idx = int(mli.getProperty('index')) - except ValueError: - return - - target = idx + offset - if target >= self.chunkMode.itemCount: - pos = self.chunkMode.itemCount - 1 - elif target < 0: - pos = 0 - else: - pos = self.showPanelControl.getSelectedPosition() - pos += offset - - if pos < 0 or pos >= self.showPanelControl.size(): - pos = pos % self.showPanelControl.size() - - self.showPanelControl.selectItem(pos) - self.checkChunkedNav(idx=pos) - def updateKey(self, mli=None): mli = mli or self.showPanelControl.getSelectedItem() if not mli: @@ -723,78 +503,6 @@ def updateKey(self, mli=None): self.selectKey(mli) - def checkChunkedNav(self, action=None, idx=None): - if not self.chunkMode: - return - - # if action == xbmcgui.ACTION_PAGE_DOWN: - # idx = self.showPanelControl.getSelectedPosition() - 5 - # if idx < 0: - # idx += self.showPanelControl.size() - # mli = self.showPanelControl.getListItem(idx) - # self.showPanelControl.selectItem(idx) - # elif action == xbmcgui.ACTION_PAGE_UP: - # idx = self.showPanelControl.getSelectedPosition() + 5 - # if idx >= self.showPanelControl.size(): - # idx %= self.showPanelControl.size() - # mli = self.showPanelControl.getListItem(idx) - # self.showPanelControl.selectItem(idx) - # else: - mli = self.showPanelControl.getSelectedItem() - - try: - if idx is not None: - pos = int(self.showPanelControl[idx].getProperty('index')) - else: - pos = int(mli.getProperty('index')) - - if pos >= self.chunkMode.itemCount: - raise ValueError - except ValueError: - if self.chunkMode.isAtBeginning() and action not in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN): - idx = 0 - else: - idx = ((self.chunkMode.itemCount - 1) % self.showPanelControl.LIST_MAX) - - self.showPanelControl.selectItem(idx) - mli = self.showPanelControl[idx] - self.updateKey(mli) - if self.scrollBar: - try: - pos = int(mli.getProperty('index')) - self.scrollBar.setPosition(pos) - except ValueError: - pass - - if idx == 0 and action == xbmcgui.ACTION_MOVE_UP: - self.setFocusId(600) - - return - - if self.scrollBar: - self.scrollBar.setPosition(pos) - - if self.chunkMode.posIsForward(pos): - self.shiftChunks() - elif self.chunkMode.posIsBackward(pos): - self.shiftChunks(-1) - - def shiftChunks(self, mod=1): - start = self.chunkMode.shift(mod) - if start is None: - return - - if start < 0: - self.chunkCallback([None] * CHUNK_SIZE, -CHUNK_SIZE) - else: - self.chunkCallback([False] * CHUNK_SIZE, start) - task = ChunkRequestTask().setup( - self.section, start, CHUNK_SIZE, self.chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, subDir=self.subDir - ) - - self.tasks.add(task) - backgroundthread.BGThreader.addTasksToFront([task]) - def selectKey(self, mli=None): if not mli: mli = self.showPanelControl.getSelectedItem() @@ -809,78 +517,30 @@ def selectKey(self, mli=None): def searchButtonClicked(self): self.processCommand(search.dialog(self, section_id=self.section.key)) - def delayedChunkedPosJump(self, pos): - if not self.cleared: - self.chunkCallback(None, None, clear=True) - self.dcpjTimeout = time.time() + 0.5 - self.dcpjPos = pos - if not self.dcpjThread or not self.dcpjThread.is_alive(): - self.dcpjThread = threading.Thread(target=self._chunkedPosJump) - self.dcpjThread.start() - - def _chunkedPosJump(self): - while not util.MONITOR.waitForAbort(0.1): - if time.time() >= self.dcpjTimeout: - break - else: - return - - keyStart_start = self.chunkMode.shiftToKey(None, keyStart=self.dcpjPos) - if not keyStart_start: - return - - keyStart, start = keyStart_start - pos = keyStart % self.showPanelControl.LIST_MAX - self.chunkedPosJump(pos, start) - self.showPanelControl.selectItem(pos) - - def chunkedPosJump(self, pos, start=None): - if start is None: - start = max(pos - CHUNK_SIZE, 0) - - mul = 3 - if not start: - mul = 2 - - tasks = [] - for x in range(mul): - task = ChunkRequestTask().setup( - self.section, - start + (CHUNK_SIZE * x), - CHUNK_SIZE, - self.chunkCallback, - filter_=self.getFilterOpts(), - sort=self.getSortOpts(), - unwatched=self.filterUnwatched, - subDir=self.subDir - ) - - self.tasks.add(task) - tasks.append(task) - - mid = tasks.pop(1) - backgroundthread.BGThreader.addTasksToFront([mid] + tasks) - def keyClicked(self): li = self.keyListControl.getSelectedItem() if not li: return - if self.chunkMode: - keyStart_start = self.chunkMode.shiftToKey(li.dataSource) - if not keyStart_start: - return - keyStart, start = keyStart_start - - pos = keyStart % self.showPanelControl.LIST_MAX - self.chunkedPosJump(pos, start) - else: - mli = self.firstOfKeyItems.get(li.dataSource) - if not mli: - return - pos = mli.pos() - + mli = self.firstOfKeyItems.get(li.dataSource) + if not mli: + return + pos = mli.pos() + + # This code is a little goofy but what it's trying to do is move the selected item from the + # jumplist up to the top of the panel and then it requests the chunk for the current position + # and the chunk for the current position + CHUNK_OVERCOMMIT. The reason we need to potentially + # request a different chunk is if the items on the panel are in two different chunks this code + # will request both chunks so that we don't have blank items. The requestChunk will only request + # chunks that haven't already been fetched so if the current position and current position + # plus the CHUNK_OVERCOMMIT are in the same chunk then the second requestChunk call doesn't + # do anything. + chunkOC = getattr(self._current, "CHUNK_OVERCOMMIT", self.CHUNK_OVERCOMMIT) + self.showPanelControl.selectItem(pos+chunkOC) self.showPanelControl.selectItem(pos) + self.requestChunk(pos) + self.requestChunk(pos+chunkOC) + self.setFocusId(self.POSTERS_PANEL_ID) util.setGlobalProperty('key', li.dataSource) @@ -916,16 +576,6 @@ def optionsButtonClicked(self): if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): options.append({'key': 'play_next', 'display': T(32325, 'Play Next')}) - # if self.section.TYPE not in ('artist', 'photo', 'photodirectory'): - # options.append({'key': 'mark_watched', 'display': 'Mark All Watched'}) - # options.append({'key': 'mark_unwatched', 'display': 'Mark All Unwatched'}) - - # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': - # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) - - # if False: - # options.append({'key': 'add_to_playlist', 'display': 'Add To Playlist'}) - if self.section.TYPE == 'photodirectory': if options: options.append(dropdown.SEPARATOR) @@ -1061,13 +711,10 @@ def sortButtonClicked(self): choice = result['type'] - forceRefresh = False if choice == self.sort: self.sortDesc = not self.sortDesc else: self.sortDesc = defSortByOption.get(choice, False) - if choice == 'titleSort': - forceRefresh = True self.sort = choice @@ -1077,7 +724,7 @@ def sortButtonClicked(self): util.setGlobalProperty('sort', choice) self.setProperty('sort.display', result['title']) - self.sortShowPanel(choice, forceRefresh) + self.sortShowPanel(choice, True) def viewTypeButtonClicked(self): for task in self.tasks: @@ -1095,7 +742,7 @@ def viewTypeButtonClicked(self): util.setSetting('viewtype.{0}.{1}'.format(self.section.server.uuid, key), win.VIEWTYPE) def sortShowPanel(self, choice, force_refresh=False): - if force_refresh or self.chunkMode or self.showPanelControl.size() == 0: + if force_refresh or self.showPanelControl.size() == 0: self.fillShows() return @@ -1170,7 +817,7 @@ def filter1ButtonClicked(self): options = [] - if self.section.TYPE in ('movie', 'show'): + if self.section.TYPE in ('movie', 'show') and not ITEM_TYPE == 'collection': options.append({'type': 'unwatched', 'display': T(32368, 'UNPLAYED').upper(), 'indicator': self.filterUnwatched and check or ''}) if self.filter: @@ -1202,8 +849,11 @@ def filter1ButtonClicked(self): } if self.section.TYPE == 'movie': - for k in ('year', 'decade', 'genre', 'contentRating', 'collection', 'director', 'actor', 'country', 'studio', 'resolution', 'labels'): - options.append(optionsMap[k]) + if ITEM_TYPE == 'collection': + options.append(optionsMap['contentRating']) + else: + for k in ('year', 'decade', 'genre', 'contentRating', 'collection', 'director', 'actor', 'country', 'studio', 'resolution', 'labels'): + options.append(optionsMap[k]) elif self.section.TYPE == 'show': if ITEM_TYPE == 'episode': for k in ('year', 'collection', 'resolution'): @@ -1380,9 +1030,6 @@ def setBackground(self, items, position, randomize=True): self.backgroundSet = True def fill(self): - if self.chunkMode: - self.chunkMode.reset() - self.backgroundSet = False if self.section.TYPE in ('photo', 'photodirectory'): @@ -1415,6 +1062,8 @@ def fillShows(self): self.keyItems = {} self.firstOfKeyItems = {} totalSize = 0 + self.alreadyFetchedChunkList = set() + self.finalChunkPosition = 0 type_ = None if ITEM_TYPE == 'episode': @@ -1444,12 +1093,11 @@ def fillShows(self): else: self.setBoolProperty('no.content', True) else: - if not self.chunkMode: - for x in range(totalSize): - mli = kodigui.ManagedListItem('') - mli.setProperty('thumb.fallback', fallback) - mli.setProperty('index', str(x)) - items.append(mli) + for x in range(totalSize): + mli = kodigui.ManagedListItem('') + mli.setProperty('thumb.fallback', fallback) + mli.setProperty('index', str(x)) + items.append(mli) else: jumpList = self.section.jumpList(filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, type_=type_) @@ -1475,33 +1123,18 @@ def fillShows(self): jitems.append(mli) totalSize += ji.size.asInt() - if self.chunkMode: - self.chunkMode.addKeyRange(ji.key, (idx, (idx + ji.size.asInt()) - 1)) - idx += ji.size.asInt() - else: - for x in range(ji.size.asInt()): - mli = kodigui.ManagedListItem('') - mli.setProperty('key', ji.key) - mli.setProperty('thumb.fallback', fallback) - mli.setProperty('index', str(idx)) - items.append(mli) - if not x: # i.e. first item - self.firstOfKeyItems[ji.key] = mli - idx += 1 + for x in range(ji.size.asInt()): + mli = kodigui.ManagedListItem('') + mli.setProperty('key', ji.key) + mli.setProperty('thumb.fallback', fallback) + mli.setProperty('index', str(idx)) + items.append(mli) + if not x: # i.e. first item + self.firstOfKeyItems[ji.key] = mli + idx += 1 util.setGlobalProperty('key', jumpList[0].key) - if self.scrollBar: - self.scrollBar.setSizeAndCount(totalSize, 12) - - if self.chunkMode: - self.chunkMode.itemCount = totalSize - items = [ - kodigui.ManagedListItem('', properties={'index': str(i)}) for i in range(CHUNK_SIZE * 2) - ] + [ - kodigui.ManagedListItem('') for i in range(CHUNK_SIZE) - ] - self.showPanelControl.reset() self.keyListControl.reset() @@ -1512,16 +1145,20 @@ def fillShows(self): self.setFocusId(self.POSTERS_PANEL_ID) tasks = [] - ct = 0 - for start in range(0, totalSize, CHUNK_SIZE): + for startChunkPosition in range(0, totalSize, self.CHUNK_SIZE): tasks.append( ChunkRequestTask().setup( - self.section, start, CHUNK_SIZE, self.chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, subDir=self.subDir + self.section, startChunkPosition, self.CHUNK_SIZE, self._chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), unwatched=self.filterUnwatched, subDir=self.subDir ) ) - ct += 1 - if self.chunkMode and ct > 1: + # If we're retrieving media as we navigate then we just want to request the first + # chunk of media and stop. We'll fetch the rest as the user navigates to those items + if not util.advancedSettings.retrieveAllMediaUpFront: + # Calculate the end chunk's starting position based on the totalSize of items + self.finalChunkPosition = (totalSize // self.CHUNK_SIZE) * self.CHUNK_SIZE + # Keep track of the chunks we've already fetched by storing the chunk's starting position + self.alreadyFetchedChunkList.add(startChunkPosition) break self.tasks.add(tasks) @@ -1643,33 +1280,11 @@ def fillPhotos(self): if keys: util.setGlobalProperty('key', keys[0]) - def chunkCallback(self, items, start, clear=False): - if clear: - with self.lock: - items = [kodigui.ManagedListItem('') for i in range(CHUNK_SIZE * 3)] - - self.showPanelControl.reset() - self.showPanelControl.addItems(items) - - self.cleared = True - return - - if self.cleared: - self.cleared = False - busy.widthDialog(self._chunkCallback, self, items, start) - else: - self._chunkCallback(items, start) - def _chunkCallback(self, items, start): - if self.chunkMode and not self.chunkMode.posIsValid(start): - return - if not self.showPanelControl or not items: return with self.lock: - if self.chunkMode and not self.chunkMode.posIsValid(start): - return pos = start self.setBackground(items, pos, randomize=not util.advancedSettings.dynamicBackgrounds) @@ -1680,9 +1295,6 @@ def _chunkCallback(self, items, start): if (self.section.TYPE in ('movie', 'show') and items[0].TYPE != 'collection') or (self.section.TYPE == 'collection' and items[0].TYPE in ('movie', 'show', 'episode')): # NOTE: A collection with Seasons doesn't have the leafCount/viewedLeafCount until you actually go into the season so we can't update the unwatched count here showUnwatched = True - if self.chunkMode and len(items) < CHUNK_SIZE: - items += [None] * (CHUNK_SIZE - len(items)) - if ITEM_TYPE == 'episode': for offset, obj in enumerate(items): if not self.showPanelControl: @@ -1704,8 +1316,6 @@ def _chunkCallback(self, items, start): mli.setProperty('summary', obj.summary) - # # mli.setProperty('key', self.chunkMode.getKey(pos)) - mli.setLabel2(util.durationToText(obj.fixedDuration())) mli.setProperty('art', obj.defaultArt.asTranscodedImageURL(*artDim)) if not obj.isWatched: @@ -1734,13 +1344,7 @@ def _chunkCallback(self, items, start): mli.setProperty('summary', obj.summary) - # # mli.setProperty('key', self.chunkMode.getKey(pos)) - mli.setLabel2(obj.year) - - if self.chunkMode: - mli.setProperty('key', self.chunkMode.getKey(pos)) - else: mli.clear() if obj is False: @@ -1768,11 +1372,6 @@ def _chunkCallback(self, items, start): mli.dataSource = obj mli.setProperty('summary', obj.get('summary')) - #if self.chunkMode: - # mli.setProperty('key', self.chunkMode.getKey(pos)) - #else: - # mli.setProperty('key', obj.key) - if showUnwatched and obj.TYPE != 'collection': if not obj.isDirectory(): mli.setLabel2(util.durationToText(obj.fixedDuration())) @@ -1793,6 +1392,28 @@ def _chunkCallback(self, items, start): pos += 1 + def requestChunk(self, start): + if util.advancedSettings.retrieveAllMediaUpFront: + return + + # Calculate the correct starting chunk position for the item they passed in + startChunkPosition = (start // self.CHUNK_SIZE) * self.CHUNK_SIZE + # If we calculated a chunk position that's beyond the end chunk then just return + if startChunkPosition > self.finalChunkPosition: + return + + # Check if the chunk has already been requested, if not then go fetch the data + if startChunkPosition not in self.alreadyFetchedChunkList: + util.DEBUG_LOG('Position {0} so requesting chunk {1}'.format(start, startChunkPosition)) + # Keep track of the chunks we've already fetched by storing the chunk's starting position + self.alreadyFetchedChunkList.add(startChunkPosition) + task = ChunkRequestTask().setup(self.section, startChunkPosition, self.CHUNK_SIZE, + self._chunkCallback, filter_=self.getFilterOpts(), sort=self.getSortOpts(), + unwatched=self.filterUnwatched, subDir=self.subDir) + + self.tasks.add(task) + backgroundthread.BGThreader.addTasksToFront([task]) + class PostersWindow(kodigui.ControlledWindow): xmlFile = 'script-plex-posters.xml' @@ -1825,31 +1446,21 @@ class PostersWindow(kodigui.ControlledWindow): VIEWTYPE = 'panel' MULTI_WINDOW_ID = 0 - CUSTOM_SCOLLBAR_BUTTON_ID = 951 + CHUNK_OVERCOMMIT = 6 class PostersSmallWindow(PostersWindow): xmlFile = 'script-plex-posters-small.xml' VIEWTYPE = 'panel2' MULTI_WINDOW_ID = 1 - - -class PostersChunkedWindow(PostersWindow): - xmlFile = 'script-plex-listview-16x9-chunked.xml' - VIEWTYPE = 'list' - MULTI_WINDOW_ID = 0 + CHUNK_OVERCOMMIT = 30 class ListView16x9Window(PostersWindow): xmlFile = 'script-plex-listview-16x9.xml' VIEWTYPE = 'list' MULTI_WINDOW_ID = 2 - - -class ListView16x9ChunkedWindow(PostersWindow): - xmlFile = 'script-plex-listview-16x9-chunked.xml' - VIEWTYPE = 'list' - MULTI_WINDOW_ID = 1 + CHUNK_OVERCOMMIT = 12 class SquaresWindow(PostersWindow): @@ -1858,24 +1469,12 @@ class SquaresWindow(PostersWindow): MULTI_WINDOW_ID = 0 -class SquaresChunkedWindow(PostersWindow): - xmlFile = 'script-plex-listview-square-chunked.xml' - VIEWTYPE = 'list' - MULTI_WINDOW_ID = 0 - - class ListViewSquareWindow(PostersWindow): xmlFile = 'script-plex-listview-square.xml' VIEWTYPE = 'list' MULTI_WINDOW_ID = 1 -class ListViewSquareChunkedWindow(PostersWindow): - xmlFile = 'script-plex-listview-square-chunked.xml' - VIEWTYPE = 'list' - MULTI_WINDOW_ID = 1 - - VIEWS_POSTER = { 'panel': PostersWindow, 'panel2': PostersSmallWindow, @@ -1883,20 +1482,8 @@ class ListViewSquareChunkedWindow(PostersWindow): 'all': (PostersWindow, PostersSmallWindow, ListView16x9Window) } -VIEWS_POSTER_CHUNKED = { - 'panel': PostersChunkedWindow, - 'list': ListView16x9ChunkedWindow, - 'all': (PostersChunkedWindow, ListView16x9ChunkedWindow) -} - VIEWS_SQUARE = { 'panel': SquaresWindow, 'list': ListViewSquareWindow, 'all': (SquaresWindow, ListViewSquareWindow) } - -VIEWS_SQUARE_CHUNKED = { - 'panel': SquaresChunkedWindow, - 'list': ListViewSquareChunkedWindow, - 'all': (SquaresChunkedWindow, ListViewSquareChunkedWindow) -} \ No newline at end of file diff --git a/script.plexmod/lib/windows/mixins.py b/script.plexmod/lib/windows/mixins.py index 1e3586847..f35ac80fb 100644 --- a/script.plexmod/lib/windows/mixins.py +++ b/script.plexmod/lib/windows/mixins.py @@ -1,11 +1,16 @@ # coding=utf-8 +import math + from lib import util from . import kodigui +from . import optionsdialog +from . import busy +from lib.util import T -class SeasonsMixin(): +class SeasonsMixin: SEASONS_CONTROL_ATTR = "subItemListControl" THUMB_DIMS = { @@ -31,8 +36,20 @@ def _createListItem(self, mediaItem, obj): ) return mli - def fillSeasons(self, mediaItem, update=False, seasonsFilter=None, selectSeason=None): - seasons = mediaItem.seasons() + def getSeasonProgress(self, show, season): + """ + calculates the season progress based on how many episodes are watched and, optionally, if there's an episode + in progress, take that into account as well + """ + watchedPerc = season.viewedLeafCount.asInt() / season.leafCount.asInt() * 100 + for v in show.onDeck: + if v.parentRatingKey == season.ratingKey and v.viewOffset: + vPerc = int((v.viewOffset.asInt() / v.duration.asFloat()) * 100) + watchedPerc += vPerc / season.leafCount.asFloat() + return watchedPerc > 0 and math.ceil(watchedPerc) or 0 + + def fillSeasons(self, show, update=False, seasonsFilter=None, selectSeason=None): + seasons = show.seasons() if not seasons or (seasonsFilter and not seasonsFilter(seasons)): return False @@ -42,11 +59,13 @@ def fillSeasons(self, mediaItem, update=False, seasonsFilter=None, selectSeason= if selectSeason and season == selectSeason: continue - mli = self._createListItem(mediaItem, season) + mli = self._createListItem(show, season) if mli: mli.setProperty('index', str(idx)) mli.setProperty('thumb.fallback', 'script.plex/thumb_fallbacks/show.png') mli.setProperty('unwatched.count', not season.isWatched and str(season.unViewedLeafCount) or '') + if not season.isWatched: + mli.setProperty('progress', util.getProgressImage(None, self.getSeasonProgress(show, season))) items.append(mli) idx += 1 @@ -59,3 +78,62 @@ def fillSeasons(self, mediaItem, update=False, seasonsFilter=None, selectSeason= return True + +class DeleteMediaMixin: + def delete(self, item=None): + button = optionsdialog.show( + T(32326, 'Really delete?'), + T(32327, 'Are you sure you really want to delete this media?'), + T(32328, 'Yes'), + T(32329, 'No') + ) + + if button != 0: + return + + if not self._delete(item=item or self.mediaItem): + util.messageDialog(T(32330, 'Message'), T(32331, 'There was a problem while attempting to delete the media.')) + return + return True + + @busy.dialog() + def _delete(self, item): + success = item.delete() + util.LOG('Media DELETE: {0} - {1}'.format(self.mediaItem, success and 'SUCCESS' or 'FAILED')) + if success: + self.doClose() + return success + + +class RatingsMixin: + def populateRatings(self, video, ref): + def sanitize(src): + return src.replace("themoviedb", "tmdb").replace('://', '/') + + setProperty = getattr(ref, "setProperty") + getattr(ref, "setProperties")(('rating.stars', 'rating', 'rating.image', 'rating2', 'rating2.image'), '') + + if video.userRating: + stars = str(int(round((video.userRating.asFloat() / 10) * 5))) + setProperty('rating.stars', stars) + + audienceRating = video.audienceRating + + if video.rating or audienceRating: + if video.rating: + rating = video.rating + if video.ratingImage.startswith('rottentomatoes:'): + rating = '{0}%'.format(int(rating.asFloat() * 10)) + + setProperty('rating', rating) + if video.ratingImage: + setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(sanitize(video.ratingImage))) + if audienceRating: + if video.audienceRatingImage.startswith('rottentomatoes:'): + audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) + setProperty('rating2', audienceRating) + if video.audienceRatingImage: + setProperty('rating2.image', + 'script.plex/ratings/{0}.png'.format(sanitize(video.audienceRatingImage))) + else: + setProperty('rating', video.rating) diff --git a/script.plexmod/lib/windows/playersettings.py b/script.plexmod/lib/windows/playersettings.py index 3abbd4427..51fbb5c1b 100644 --- a/script.plexmod/lib/windows/playersettings.py +++ b/script.plexmod/lib/windows/playersettings.py @@ -111,6 +111,10 @@ def showSettings(self, init=False): ] if util.KODI_VERSION_MAJOR >= 18: options.append(('kodi_subtitle', T(32492, 'Kodi Subtitle Settings'), '')) + if xbmc.getCondVisibility('Player.HasResolutions'): + options.append(('kodi_resolutions', T(32968, 'Kodi Resolution Settings'), '')) + if util.KODI_VERSION_MAJOR >= 20 and xbmc.getCondVisibility('System.HasCMS'): + options.append(('kodi_colours', T(32967, 'Kodi Colour Settings'), '')) if self.viaOSD: if self.parent.getProperty("show.PPI"): @@ -184,6 +188,10 @@ def editSetting(self): xbmc.executebuiltin('ActivateWindow(OSDAudioSettings)') elif result == 'kodi_subtitle': xbmc.executebuiltin('ActivateWindow(OSDSubtitleSettings)') + elif result == 'kodi_colours': + xbmc.executebuiltin('ActivateWindow(osdcmssettings)') + elif result == 'kodi_resolutions': + xbmc.executebuiltin("Action(PlayerResolutionSelect)") elif result == "stream_info": if self.parent: if self.parent.getProperty("show.PPI"): diff --git a/script.plexmod/lib/windows/preplay.py b/script.plexmod/lib/windows/preplay.py index e4fbdf53f..e72bae109 100644 --- a/script.plexmod/lib/windows/preplay.py +++ b/script.plexmod/lib/windows/preplay.py @@ -15,6 +15,7 @@ from . import optionsdialog from . import preplayutils from . import pagination +from .mixins import RatingsMixin from plexnet import plexplayer, media @@ -32,7 +33,7 @@ def getData(self, offset, amount): return self.parentWindow.video.getRelated(offset=offset, limit=amount) -class PrePlayWindow(kodigui.ControlledWindow, windowutils.UtilMixin): +class PrePlayWindow(kodigui.ControlledWindow, windowutils.UtilMixin, RatingsMixin): xmlFile = 'script-plex-pre_play.xml' path = util.ADDON.getAddonInfo('path') theme = 'Main' @@ -95,7 +96,7 @@ def onFirstInit(self): def doAutoPlay(self): # First reload the video to get all the other info self.video.reload(checkFiles=1, **VIDEO_RELOAD_KW) - return self.playVideo() + return self.playVideo(from_auto_play=True) @busy.dialog() def onReInit(self): @@ -452,7 +453,7 @@ def getRoleItemDDPosition(self): x = ((focus + 1) * 304) - 100 return x, y - def playVideo(self): + def playVideo(self, from_auto_play=False): if not self.video.available(): util.messageDialog(T(32312, 'Unavailable'), T(32313, 'This item is currently unavailable.')) return @@ -467,7 +468,8 @@ def playVideo(self): pos=(660, 441), close_direction='none', set_dropdown_prop=False, - header=T(32314, 'In Progress') + header=T(32314, 'In Progress'), + dialog_props=from_auto_play and self.dialogProps or None ) if not choice: @@ -527,20 +529,21 @@ def setInfo(self, skip_bg=False): self.setProperty('summary', self.video.summary.strip().replace('\t', ' ')) self.setProperty('unwatched', not self.video.isWatched and '1' or '') - directors = u' / '.join([d.tag for d in self.video.directors()][:5]) + directors = u' / '.join([d.tag for d in self.video.directors()][:3]) directorsLabel = len(self.video.directors) > 1 and T(32401, u'DIRECTORS').upper() or T(32383, u'DIRECTOR').upper() self.setProperty('directors', directors and u'{0} {1}'.format(directorsLabel, directors) or '') + writers = u' / '.join([r.tag for r in self.video.writers()][:3]) + writersLabel = len(self.video.writers) > 1 and T(32403, u'WRITERS').upper() or T(32402, u'WRITER').upper() + self.setProperty('writers', + writers and u'{0}{1} {2}'.format(directors and ' ' or '', writersLabel, writers) or '') + # fixme: can this ever happen? if self.video.type == 'episode': self.setProperty('content.rating', '') self.setProperty('thumb', self.video.defaultThumb.asTranscodedImageURL(*self.THUMB_POSTER_DIM)) self.setProperty('preview', self.video.thumb.asTranscodedImageURL(*self.PREVIEW_DIM)) self.setProperty('info', u'{0} {1} {2} {3}'.format(T(32303, 'Season'), self.video.parentIndex, T(32304, 'Episode'), self.video.index)) self.setProperty('date', util.cleanLeadingZeros(self.video.originallyAvailableAt.asDatetime('%B %d, %Y'))) - - writers = u' / '.join([w.tag for w in self.video.writers()][:5]) - writersLabel = len(self.video.writers) > 1 and T(32403, u'WRITERS').upper() or T(32402, u'WRITER').upper() - self.setProperty('writers', writers and u'{0} {1}'.format(writersLabel, writers) or '') self.setProperty('related.header', T(32306, 'Related Shows')) elif self.video.type == 'movie': self.setProperty('title', self.video.defaultTitle) @@ -553,7 +556,7 @@ def setInfo(self, skip_bg=False): cast = u' / '.join([r.tag for r in self.video.roles()][:5]) castLabel = 'CAST' - self.setProperty('writers', cast and u'{0} {1}'.format(castLabel, cast) or '') + self.setProperty('cast', cast and u'{0} {1}'.format(castLabel, cast) or '') self.setProperty('related.header', T(32404, 'Related Movies')) self.setProperty('video.res', self.video.resolutionString()) @@ -563,29 +566,7 @@ def setInfo(self, skip_bg=False): self.setProperty('audio.channels', self.video.audioChannelsString(metadata.apiTranslate)) self.setBoolProperty('media.multiple', len(list(filter(lambda x: x.isAccessible(), self.video.media()))) > 1) - self.setProperties(('rating.stars', 'rating', 'rating.image', 'rating2', 'rating2.image'), '') - if self.video.userRating: - stars = str(int(round((self.video.userRating.asFloat() / 10) * 5))) - self.setProperty('rating.stars', stars) - # elif self.video.rating: - # stars = str(int(round((self.video.rating.asFloat() / 10) * 5))) - # self.setProperty('rating.stars', stars) - - if self.video.ratingImage: - rating = self.video.rating - audienceRating = self.video.audienceRating - if self.video.ratingImage.startswith('rottentomatoes:'): - rating = '{0}%'.format(int(rating.asFloat() * 10)) - if audienceRating: - audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) - - self.setProperty('rating', rating) - self.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(self.video.ratingImage.replace('://', '/'))) - if self.video.audienceRatingImage: - self.setProperty('rating2', audienceRating) - self.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(self.video.audienceRatingImage.replace('://', '/'))) - else: - self.setProperty('rating', self.video.rating) + self.populateRatings(self.video, self) self.setAudioAndSubtitleInfo() diff --git a/script.plexmod/lib/windows/seekdialog.py b/script.plexmod/lib/windows/seekdialog.py index e0a4c6813..ac6d36597 100644 --- a/script.plexmod/lib/windows/seekdialog.py +++ b/script.plexmod/lib/windows/seekdialog.py @@ -49,6 +49,8 @@ "autoSkipName": T(32800, 'Skipping intro'), "overrideStartOff": None, "countdown": None, + "countdown_initial": None, + "skipped": False, # attrs "markerAutoSkip": "autoSkipIntro", @@ -62,6 +64,8 @@ "autoSkipName": T(32801, 'Skipping credits'), "overrideStartOff": None, "countdown": None, + "countdown_initial": None, + "skipped": False, "markerAutoSkip": "autoSkipCredits", "markerAutoSkipped": False, @@ -70,7 +74,7 @@ }) ]) -FINAL_MARKER_NEGOFF = 5000 +FINAL_MARKER_NEGOFF = 3000 MARKER_SHOW_NEGOFF = 3000 MARKER_OFF = 500 MARKER_CHAPTER_OVERLAP_THRES = 30000 # 30 seconds @@ -206,6 +210,7 @@ def __init__(self, *args, **kwargs): self.autoSkipIntro = False self.autoSkipCredits = False self.showIntroSkipEarly = False + self.skipPostPlay = False self.skipIntroButtonTimeout = util.advancedSettings.skipIntroButtonTimeout self.skipCreditsButtonTimeout = util.advancedSettings.skipCreditsButtonTimeout @@ -420,6 +425,7 @@ def setup(self, duration, meta, offset=0, bif_url=None, title='', title2='', cha self.autoSkipCredits = self.bingeMode or pbs.auto_skip_credits self.showIntroSkipEarly = self.bingeMode or pbs.show_intro_skip_early + self.handler.skipPostPlay = self.skipPostPlay = (self.bingeMode or pbs.skip_post_play_tv) # in transcoded scenarios, when seeking, keep previous marker states, as the video restarts if not keepMarkerDef: @@ -508,13 +514,10 @@ def onAction(self, action): final = getattr(marker, "final", False) markerOff = 0 - if marker.type == "credits" and final: - # offset final marker seek so we can trigger postPlay - markerOff = FINAL_MARKER_NEGOFF - util.DEBUG_LOG('MarkerSkip: Skipping marker {}'.format(markerDef["marker"])) self.setProperty('show.markerSkip', '') self.setProperty('show.markerSkip_OSDOnly', '') + markerDef["skipped"] = True self.doSeek(math.ceil(float(marker.endTimeOffset)) - markerOff) self.hideOSD(skipMarkerFocus=True) @@ -635,12 +638,12 @@ def onAction(self, action): # Alt-right builtin.PlayerControl('tempoup') elif action == xbmcgui.ACTION_NEXT_ITEM: - self.handler.ignoreTimelines = True + self.sendTimeline(state=self.player.STATE_STOPPED) self._ignoreTick = True self.killTimeKeeper() self.handler.next() elif action == xbmcgui.ACTION_PREV_ITEM: - self.handler.ignoreTimelines = True + self.sendTimeline(state=self.player.STATE_STOPPED) self._ignoreTick = True self.killTimeKeeper() self.handler.prev() @@ -652,21 +655,25 @@ def onAction(self, action): return # immediate marker timer actions - if self.countingDownMarker and \ - (self.getProperty('show.markerSkip') or self.getProperty('show.markerSkip_OSDOnly')): - + if self.countingDownMarker: if controlID != self.BIG_SEEK_LIST_ID and \ (util.advancedSettings.skipMarkerTimerCancel or util.advancedSettings.skipMarkerTimerImmediate): if util.advancedSettings.skipMarkerTimerCancel and \ action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): self.displayMarkers(cancelTimer=True) + return + # skip the first second of a marker shown with countdown to avoid unexpected OK/SELECT + # behaviour elif util.advancedSettings.skipMarkerTimerImmediate \ - and action == xbmcgui.ACTION_SELECT_ITEM: + and action == xbmcgui.ACTION_SELECT_ITEM and \ + self._currentMarker["countdown"] is not None and \ + self._currentMarker["countdown_initial"] is not None and \ + self._currentMarker["countdown"] < self._currentMarker["countdown_initial"]: self.displayMarkers(immediate=True) self.hideOSD(skipMarkerFocus=True) - return + return if action in cancelActions: if self.waitingForBuffer: @@ -692,6 +699,7 @@ def onAction(self, action): if self.osdVisible(): self.hideOSD() else: + self.sendTimeline(state=self.player.STATE_STOPPED) self.stop() return except: @@ -784,12 +792,12 @@ def onClick(self, controlID): elif controlID == self.SHUFFLE_BUTTON_ID: self.shuffleButtonClicked() elif controlID == self.PREV_BUTTON_ID: - self.handler.ignoreTimelines = True + self.sendTimeline(state=self.player.STATE_STOPPED) self._ignoreTick = True self.handler.prev() elif controlID == self.NEXT_BUTTON_ID: if not self.handler.queuingNext: - self.handler.ignoreTimelines = True + self.sendTimeline(state=self.player.STATE_STOPPED) self.handler.queuingNext = True self._ignoreTick = True self._ignoreInput = True @@ -813,8 +821,7 @@ def stop(self): self._ignoreTick = True self.doClose() # self.handler.onSeekAborted() - if self.bingeMode: - self.handler.stoppedInBingeMode = True + self.handler.stoppedManually = True self.handler.player.stop() def doClose(self, delete=False): @@ -1572,7 +1579,7 @@ def getCurrentMarkerDef(self, offset=None): @property def duration(self): try: - return self._duration or int(self.handler.player.getTotalTime() * 1000) + return self._duration or self.handler.currentDuration() except RuntimeError: # Not playing return 1 @@ -1860,7 +1867,8 @@ def onTimeKeeperCallback(self, tick=True): def countingDownMarker(self): return self._currentMarker and \ self._currentMarker["countdown"] is not None and \ - self._currentMarker["countdown"] > 0 + self._currentMarker["countdown"] > 0 and \ + self.getProperty('show.markerSkip') @countingDownMarker.setter def countingDownMarker(self, val): @@ -1868,6 +1876,11 @@ def countingDownMarker(self, val): self._currentMarker["countdown"] = None self.setProperty('marker.countdown', '') + def sendTimeline(self, state=None, t=None, force=True, ensureFinalTimelineEvent=True): + self.handler.updateNowPlaying(force=force, state=state, t=t, overrideChecks=True) + if ensureFinalTimelineEvent: + self.handler.ignoreTimelines = True + def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=False, setSkipped=False, offset=None): # intro/credits marker display logic @@ -1889,6 +1902,12 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F int(markerDef["marker"].startTimeOffset) markerAutoSkip = getattr(self, markerDef["markerAutoSkip"]) + + # don't skip the credits marker on the last available episode + if markerDef["marker_type"] == "credits" and self.bingeMode and self.handler.playlist and \ + not self.handler.playlist.hasNext(): + markerAutoSkip = False + markerAutoSkipped = markerDef["markerAutoSkipped"] sTOffWThres = startTimeOff + util.advancedSettings.autoSkipOffset * 1000 @@ -1937,15 +1956,14 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F return False # tell plex we've arrived at the end of the video, playing back - self.handler.updateNowPlaying(True, state=self.player.STATE_STOPPED, time=self.duration - 1000) + self.sendTimeline(state=self.player.STATE_STOPPED, t=self.duration - 1000) # go to next video immediately if on bingeMode - if self.handler.playlist and self.handler.playlist.hasNext() and self.bingeMode: + if self.handler.playlist and self.handler.playlist.hasNext(): if not self.handler.queuingNext: # skip final marker util.DEBUG_LOG("MarkerAutoSkip: {} final marker, going to next video".format( immediate and "Immediately skipping" or "Skipping")) - self.handler.ignoreTimelines = True self.handler.queuingNext = True self._ignoreTick = True self._ignoreInput = True @@ -1974,23 +1992,27 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F else: self.setProperty('show.markerSkip_OSDOnly', '') - # no marker auto skip or not yet auto skipped, normal display - if not markerAutoSkip or (markerAutoSkip and not markerAutoSkipped): + # no marker auto skip and not yet skipped or not yet auto skipped, normal display + if (markerAutoSkip and not markerAutoSkipped) or (not markerAutoSkip and not markerDef["skipped"]): self.setProperty('show.markerSkip', '1') - # marker auto skip and already skipped - hide in OSD - elif markerAutoSkip and markerAutoSkipped: + # marker auto skip and already skipped, or no autoskip and manually skipped - hide in OSD + else: self.setProperty('show.markerSkip_OSDOnly', '1') # set marker name, count down if markerAutoSkip and not markerAutoSkipped: + isNew = False if markerDef["countdown"] is None: # reset countdown on new marker if not self._currentMarker or self._currentMarker != markerDef or markerDef["countdown"] is None: # fixme: round might not be right here, but who cares markerDef["countdown"] = int(max(round((sTOffWThres - self.trueOffset()) / 1000.0) + 1, 1)) + isNew = True if self.player.playState == self.player.STATE_PLAYING and not self.osdVisible(): markerDef["countdown"] -= 1 + if isNew: + markerDef["countdown_initial"] = markerDef["countdown"] self.setProperty('marker.countdown', '1') @@ -2015,60 +2037,65 @@ def tick(self, offset=None, waitForBuffer=False): """ Called ~1/s; can be wildly inaccurate. """ - if waitForBuffer: - cont = self.waitForBuffer() - if not cont: - return - if not self.initialized or self._ignoreTick: + # we might be called with an offset for seekOnStart even before we're initialized (onFirstInit) + # in that case, skip all functionality and just seekOnStart + if (not offset and not self.initialized) or self._ignoreTick: return - # invisibly sync low-drift timer to current playback every X seconds, as Player.getTime() can be wildly off - if self.ldTimer and not self.osdVisible() and self.timeKeeper and self.timeKeeper.ticks >= 60: - self.syncTimeKeeper() + if self.initialized: + if waitForBuffer: + cont = self.waitForBuffer() + if not cont: + return - cancelTick = False - # don't auto skip while we're initializing and waiting for the handler to seek on start - if offset is None and not self.handler.seekOnStart: - cancelTick = self.displayMarkers() + # invisibly sync low-drift timer to current playback every X seconds, as Player.getTime() can be wildly off + if self.ldTimer and not self.osdVisible() and self.timeKeeper and self.timeKeeper.ticks >= 60: + self.syncTimeKeeper() - if cancelTick: - return + cancelTick = False + # don't auto skip while we're initializing and waiting for the handler to seek on start + if offset is None and not self.handler.seekOnStart: + cancelTick = self.displayMarkers() + + if cancelTick: + return - if xbmc.getCondVisibility('Window.IsActive(busydialog) + !Player.Caching'): - util.DEBUG_LOG('SeekDialog: Possible stuck busy dialog - closing') - xbmc.executebuiltin('Dialog.Close(busydialog,1)') + if xbmc.getCondVisibility('Window.IsActive(busydialog) + !Player.Caching'): + util.DEBUG_LOG('SeekDialog: Possible stuck busy dialog - closing') + xbmc.executebuiltin('Dialog.Close(busydialog,1)') - if not self.hasDialog and not self.playlistDialogVisible and self.osdVisible(): - if time.time() > self.timeout and not self._osdHideFast: - if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): - self.hideOSD() + if not self.hasDialog and not self.playlistDialogVisible and self.osdVisible(): + if time.time() > self.timeout and not self._osdHideFast: + if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): + self.hideOSD() - # try insta-hiding the OSDs when playback was requested - elif self._osdHideFast: - xbmc.executebuiltin('Dialog.Close(videoosd,true)') - xbmc.executebuiltin('Dialog.Close(seekbar,true)') - if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): - self.hideOSD() + # try insta-hiding the OSDs when playback was requested + elif self._osdHideFast: + xbmc.executebuiltin('Dialog.Close(videoosd,true)') + xbmc.executebuiltin('Dialog.Close(seekbar,true)') + if not xbmc.getCondVisibility('Window.IsActive(videoosd) | Player.Rewinding | Player.Forwarding'): + self.hideOSD() - self._osdHideFast = False + self._osdHideFast = False - try: - self.offset = offset or int(self.handler.player.getTime() * 1000) - except RuntimeError: # Playback has stopped - self.resetSeeking() - return + if offset or self.initialized: + try: + self.offset = offset or int(self.handler.player.getTime() * 1000) + except RuntimeError: # Playback has stopped + self.resetSeeking() + return - if offset or (self.autoSeekTimeout and time.time() >= self.autoSeekTimeout and - self.offset != self.selectedOffset): - self.resetAutoSeekTimer(None) - #off = offset is not None and offset or None - #self.doSeek(off) - self.doSeek() - return True + if offset or (self.autoSeekTimeout and time.time() >= self.autoSeekTimeout and + self.offset != self.selectedOffset): + self.resetAutoSeekTimer(None) + #off = offset is not None and offset or None + #self.doSeek(off) + self.doSeek() + return True - if self.isDirectPlay or not self.ldTimer: - self.updateCurrent(update_position_control=not self._seeking and not self._applyingSeek) + if self.isDirectPlay or not self.ldTimer: + self.updateCurrent(update_position_control=not self._seeking and not self._applyingSeek) @property def playlistDialogVisible(self): @@ -2106,7 +2133,8 @@ def hideOSD(self, skipMarkerFocus=False, closing=False): return self.setFocusId(self.NO_OSD_BUTTON_ID) - if not skipMarkerFocus and self.getCurrentMarkerDef() and not self.getProperty('show.markerSkip_OSDOnly'): + if not skipMarkerFocus and not self.getProperty('show.markerSkip_OSDOnly') \ + and self.getProperty('show.markerSkip'): self.setFocusId(self.SKIP_MARKER_BUTTON_ID) self.resetSeeking() diff --git a/script.plexmod/lib/windows/settings.py b/script.plexmod/lib/windows/settings.py index e3c545e37..978d75b42 100644 --- a/script.plexmod/lib/windows/settings.py +++ b/script.plexmod/lib/windows/settings.py @@ -378,6 +378,23 @@ class Settings(object): ), 'player_user': ( T(32631, 'Playback (user-specific)'), ( + BoolUserSetting( + 'show_chapters', T(33601, 'Show video chapters'), True + ).description( + T(33602, 'If available, show video chapters from the video-file instead of the ' + 'timeline-big-seek-steps.') + ), + BoolUserSetting( + 'virtual_chapters', T(33603, 'Use virtual chapters'), True + ).description( + T(33604, 'When the above is enabled and no video chapters are available, simulate them by using the' + ' markers identified by the Plex Server (Intro, Credits).') + ), + BoolUserSetting( + 'auto_skip_in_transcode', T(32948, 'Allow auto-skip when transcoding'), True + ).description( + T(32949, 'When transcoding/DirectStreaming, allow auto-skip functionality.') + ), BoolUserSetting( 'post_play_auto', T(32039, 'Post Play Auto Play'), True ).description( @@ -391,40 +408,32 @@ class Settings(object): 'binge_mode', T(33618, 'TV binge-viewing mode'), False ).description( T(33619, 'Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t ' - 'skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show.') + 'skip the intro of the first episode of a season and doesn\'t skip the final credits of a ' + 'show.\n\nCan be disabled/enabled per TV show.' + '\n\nOverrides any playback setting below.') ), BoolUserSetting( 'auto_skip_intro', T(32522, 'Automatically Skip Intro'), False ).description( - T(32523, 'Automatically skip intros if available. Doesn\'t override enabled binge mode.') + T(32523, 'Automatically skip intros if available. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.') ), BoolUserSetting( 'auto_skip_credits', T(32526, 'Auto Skip Credits'), False ).description( - T(32527, 'Automatically skip credits if available. Doesn\'t override enabled binge mode.') + T(32527, 'Automatically skip credits if available. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.') ), BoolUserSetting( 'show_intro_skip_early', T(33505, 'Show intro skip button early'), False ).description( T(33506, 'Show the intro skip button from the start of a video with an intro marker. The auto-skipp' - 'ing setting applies. Doesn\'t override enabled binge mode.') - ), - BoolUserSetting( - 'auto_skip_in_transcode', T(32948, 'Allow auto-skip when transcoding'), True - ).description( - T(32949, 'When transcoding/DirectStreaming, allow auto-skip functionality.') + 'ing setting applies. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.') ), BoolUserSetting( - 'show_chapters', T(33601, 'Show video chapters'), True + 'skip_post_play_tv', T(32973, 'Episodes: Skip Post Play screen'), False ).description( - T(33602, 'If available, show video chapters from the video-file instead of the ' - 'timeline-big-seek-steps.') - ), - BoolUserSetting( - 'virtual_chapters', T(33603, 'Use virtual chapters'), True - ).description( - T(33604, 'When the above is enabled and no video chapters are available, simulate them by using the' - ' markers identified by the Plex Server (Intro, Credits).') + T(32974, 'When finishing an episode, don\'t show Post Play but go to the next one immediately.' + '\nCan be disabled/enabled per TV show. Doesn\'t override enabled binge mode. ' + 'Overrides the Post Play setting.') ), ) ), diff --git a/script.plexmod/lib/windows/subitems.py b/script.plexmod/lib/windows/subitems.py index 774e9ad87..915ec15f1 100644 --- a/script.plexmod/lib/windows/subitems.py +++ b/script.plexmod/lib/windows/subitems.py @@ -5,13 +5,11 @@ from kodi_six import xbmcgui from . import kodigui -from lib import colors from lib import util from lib import metadata from lib import player from plexnet import playlist -from plexnet.util import INTERFACE from . import busy from . import episodes @@ -27,7 +25,7 @@ from . import playbacksettings from lib.util import T -from .mixins import SeasonsMixin +from .mixins import SeasonsMixin, DeleteMediaMixin, RatingsMixin class RelatedPaginator(pagination.BaseRelatedPaginator): @@ -35,7 +33,8 @@ def getData(self, offset, amount): return self.parentWindow.mediaItem.getRelated(offset=offset, limit=amount) -class ShowWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, playbacksettings.PlaybackSettingsMixin): +class ShowWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, DeleteMediaMixin, RatingsMixin, + playbacksettings.PlaybackSettingsMixin): xmlFile = 'script-plex-seasons.xml' path = util.ADDON.getAddonInfo('path') theme = 'Main' @@ -68,6 +67,8 @@ class ShowWindow(kodigui.ControlledWindow, windowutils.UtilMixin, SeasonsMixin, def __init__(self, *args, **kwargs): kodigui.ControlledWindow.__init__(self, *args, **kwargs) + SeasonsMixin.__init__(*args, **kwargs) + DeleteMediaMixin.__init__(*args, **kwargs) self.mediaItem = kwargs.get('media_item') self.parentList = kwargs.get('parent_list') self.cameFrom = kwargs.get('came_from') @@ -106,7 +107,7 @@ def onInit(self): self.mediaItem.ratingKey) def setup(self): - self.mediaItem.reload(includeExtras=1, includeExtrasCount=10) + self.mediaItem.reload(includeExtras=1, includeExtrasCount=10, includeOnDeck=1) self.relatedPaginator = RelatedPaginator(self.relatedListControl, leaf_count=int(self.mediaItem.relatedCount), parent_window=self) @@ -142,27 +143,7 @@ def updateProperties(self): genres = self.mediaItem.genres() self.setProperty('info', genres and (u' / '.join([g.tag for g in genres][:3])) or '') - self.setProperties(('rating.stars', 'rating', 'rating.image', 'rating2', 'rating2.image'), '') - - if self.mediaItem.userRating: - stars = str(int(round((self.mediaItem.userRating.asFloat() / 10) * 5))) - self.setProperty('rating.stars', stars) - - if self.mediaItem.ratingImage: - rating = self.mediaItem.rating - audienceRating = self.mediaItem.audienceRating - if self.mediaItem.ratingImage.startswith('rottentomatoes:'): - rating = '{0}%'.format(int(rating.asFloat() * 10)) - if audienceRating: - audienceRating = '{0}%'.format(int(audienceRating.asFloat() * 10)) - - self.setProperty('rating', rating) - self.setProperty('rating.image', 'script.plex/ratings/{0}.png'.format(self.mediaItem.ratingImage.replace('://', '/'))) - if self.mediaItem.audienceRatingImage: - self.setProperty('rating2', audienceRating) - self.setProperty('rating2.image', 'script.plex/ratings/{0}.png'.format(self.mediaItem.audienceRatingImage.replace('://', '/'))) - else: - self.setProperty('rating', self.mediaItem.rating) + self.populateRatings(self.mediaItem, self) sas = self.mediaItem.selectedAudioStream() self.setProperty('audio', sas and sas.getTitle() or 'None') @@ -173,7 +154,14 @@ def updateProperties(self): leafcount = self.mediaItem.leafCount.asFloat() if leafcount: - width = (int((self.mediaItem.viewedLeafCount.asInt() / leafcount) * self.width)) or 1 + wBase = self.mediaItem.viewedLeafCount.asInt() / leafcount + for v in self.mediaItem.onDeck: + if v.viewOffset: + wBase += v.viewOffset.asInt() / v.duration.asFloat() / leafcount + + # if we have _any_ progress, display it as the smallest step + wBase = 0 < wBase < 0.01 and 0.01 or wBase + width = (int(wBase * self.width)) or 1 self.progressImageControl.setWidth(width) def onAction(self, action): @@ -184,7 +172,10 @@ def onAction(self, action): self.setFocusId(self.lastFocusID) if action == xbmcgui.ACTION_CONTEXT_MENU: - if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): + if controlID == self.SUB_ITEM_LIST_ID: + self.optionsButtonClicked(from_item=True) + return + elif not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): self.lastNonOptionsFocusID = self.lastFocusID self.setFocusId(self.OPTIONS_GROUP_ID) return @@ -380,7 +371,8 @@ def subItemListClicked(self): return if update: - mli.setProperty('unwatched.count', not mli.dataSource.isWatched and str(mli.dataSource.unViewedLeafCount) or '') + if mli and mli.dataSource: + mli.setProperty('unwatched.count', not mli.dataSource.isWatched and str(mli.dataSource.unViewedLeafCount) or '') self.mediaItem.reload(includeRelated=1, includeRelatedCount=10, includeExtras=1, includeExtrasCount=10) self.updateProperties() @@ -422,23 +414,38 @@ def playButtonClicked(self, shuffle=False): def shuffleButtonClicked(self): self.playButtonClicked(shuffle=True) - def optionsButtonClicked(self): + def optionsButtonClicked(self, from_item=None): options = [] - oldBingeModeValue = None if xbmc.getCondVisibility('Player.HasAudio + MusicPlayer.HasNext'): options.append({'key': 'play_next', 'display': 'Play Next'}) - if self.mediaItem.type != 'artist': - if self.mediaItem.isWatched: + item = self.mediaItem + if from_item: + sel = self.subItemListControl.getSelectedItem() + if sel.dataSource: + item = sel.dataSource + + if not item: + return + + if item.type != 'artist': + if item.isWatched: options.append({'key': 'mark_unwatched', 'display': T(32318, 'Mark Unplayed')}) else: options.append({'key': 'mark_watched', 'display': T(32319, 'Mark Played')}) - if self.mediaItem.type == "show": + if item.type == "show": if options: options.append(dropdown.SEPARATOR) options.append({'key': 'playback_settings', 'display': T(32925, 'Playback Settings')}) + if item.server.allowsMediaDeletion: + options.append(dropdown.SEPARATOR) + options.append({'key': 'delete', 'display': T(32322, 'Delete')}) + elif item.type == "season": + if item.server.allowsMediaDeletion: + options.append(dropdown.SEPARATOR) + options.append({'key': 'delete', 'display': T(32975, 'Delete Season')}) # if xbmc.getCondVisibility('Player.HasAudio') and self.section.TYPE == 'artist': # options.append({'key': 'add_to_queue', 'display': 'Add To Queue'}) @@ -450,6 +457,11 @@ def optionsButtonClicked(self): options.append({'key': 'to_section', 'display': u'Go to {0}'.format(self.mediaItem.getLibrarySectionTitle())}) pos = (880, 618) + if from_item: + viewPos = self.subItemListControl.getViewPosition() + optsLen = len(list(filter(None, options))) + # dropDown handles any overlap with the right window boundary so we don't need to care here + pos = ((((viewPos + 1) * 218) - 100), 460 if optsLen < 7 else 460 - 66 * (optsLen - 6)) choice = dropdown.showDropdown(options, pos, close_direction='left') if not choice: @@ -458,12 +470,12 @@ def optionsButtonClicked(self): if choice['key'] == 'play_next': xbmc.executebuiltin('PlayerControl(Next)') elif choice['key'] == 'mark_watched': - self.mediaItem.markWatched() + item.markWatched() self.updateItems() self.updateProperties() util.MONITOR.watchStatusChanged() elif choice['key'] == 'mark_unwatched': - self.mediaItem.markUnwatched() + item.markUnwatched() self.updateItems() self.updateProperties() util.MONITOR.watchStatusChanged() @@ -471,6 +483,11 @@ def optionsButtonClicked(self): self.goHome(self.mediaItem.getLibrarySectionId()) elif choice['key'] == 'playback_settings': self.playbackSettings(self.mediaItem, pos, False) + elif choice['key'] == 'delete': + if self.delete(item): + # cheap way of requesting a home hub refresh because of major deletion + util.MONITOR.watchStatusChanged() + self.goHome() def roleClicked(self): mli = self.rolesListControl.getSelectedItem() diff --git a/script.plexmod/lib/windows/videoplayer.py b/script.plexmod/lib/windows/videoplayer.py index 0e36426cd..a67e5018d 100644 --- a/script.plexmod/lib/windows/videoplayer.py +++ b/script.plexmod/lib/windows/videoplayer.py @@ -17,6 +17,7 @@ from lib import util from lib import player from lib import colors +from lib import kodijsonrpc from lib.util import T @@ -298,6 +299,27 @@ def sessionEnded(self, session_id=None, **kwargs): def play(self, resume=False, handler=None): self.hidePostPlay() + def anyOtherVPlayer(): + return any(list(filter(lambda x: x['playerid'] > 0, kodijsonrpc.rpc.Player.GetActivePlayers()))) + + if player.PLAYER.isPlayingVideo(): + activePlayers = anyOtherVPlayer() + if activePlayers: + util.DEBUG_LOG("Stopping other active players: {}".format(activePlayers)) + xbmc.executebuiltin('PlayerControl(Stop)') + ct = 0 + while player.PLAYER.isPlayingVideo() or anyOtherVPlayer(): + if ct >= 50: + util.showNotification("Other player active", header=util.T(32448, 'Playback Failed!')) + break + util.MONITOR.waitForAbort(0.1) + ct += 1 + + if ct >= 50: + self.doClose() + return + util.MONITOR.waitForAbort(0.5) + self.setBackground() if self.playQueue: player.PLAYER.playVideoPlaylist(self.playQueue, resume=self.resume, session_id=id(self), handler=handler) @@ -341,7 +363,7 @@ def hidePostPlay(self): self.rolesListControl.reset() @busy.dialog() - def postPlay(self, video=None, playlist=None, handler=None, stoppedInBingeMode=False, **kwargs): + def postPlay(self, video=None, playlist=None, handler=None, stoppedManually=False, **kwargs): util.DEBUG_LOG('VideoPlayer: Starting post-play') self.showPostPlay() self.prev = video @@ -373,7 +395,7 @@ def postPlay(self, video=None, playlist=None, handler=None, stoppedInBingeMode=F hasPrev = self.fillRelated() self.fillRoles(hasPrev) - if not stoppedInBingeMode: + if not stoppedManually: self.startTimer() if self.next: diff --git a/script.plexmod/plugin.py b/script.plexmod/plugin.py index 6e2eec66e..2c16c2b89 100644 --- a/script.plexmod/plugin.py +++ b/script.plexmod/plugin.py @@ -66,7 +66,7 @@ def main(): if path == 'play': play(data) else: # This is a hack since it's both a plugin and a script. My Addons and Shortcuts otherwise can't launch the add-on - xbmc.executebuiltin('Action(back)') # This sometimes works to back out of the plugin directory display + util.ensureHome() xbmc.executebuiltin('RunScript(script.plexmod)') except: util.ERROR() diff --git a/script.plexmod/resources/language/resource.language.de_de/strings.po b/script.plexmod/resources/language/resource.language.de_de/strings.po index c08dab049..a0366bb24 100644 --- a/script.plexmod/resources/language/resource.language.de_de/strings.po +++ b/script.plexmod/resources/language/resource.language.de_de/strings.po @@ -1369,8 +1369,8 @@ msgstr "Intro automatisch überspringen" #: msgctxt "#32523" -msgid "Automatically skip intros if available." -msgstr "Automatisches Überspringen von Intros, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht." +msgid "Automatically skip intros if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Automatisches Überspringen von Intros, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht.\nKann pro Serie aktiviert/deaktiviert werden." #: msgctxt "#32524" @@ -1389,8 +1389,8 @@ msgstr "Automatisches Überspringen von Abspann" #: msgctxt "#32527" -msgid "Automatically skip credits if available." -msgstr "Automatisches Überspringen von Abspann, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht." +msgid "Automatically skip credits if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Automatisches Überspringen von Abspann, falls vorhanden. Überschreibt aktivierten Binge-Modus nicht.\nKann pro Serie aktiviert/deaktiviert werden." #: msgctxt "#32528" @@ -1518,8 +1518,8 @@ msgid "Show intro skip button early" msgstr "Intro überspringen früher anzeigen" msgctxt "#33506" -msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies." -msgstr "Zeige den Intro-Überspringen-Knopf von Anfang an. Die Automatische-Intro-Überspringen-Einstellung wird angewandt. Überschreibt aktivierten Binge-Modus nicht." +msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Zeige den Intro-Überspringen-Knopf von Anfang an. Die Automatische-Intro-Überspringen-Einstellung wird angewandt. Überschreibt aktivierten Binge-Modus nicht.\nKann pro Serie aktiviert/deaktiviert werden." msgctxt "#33507" msgid "Enabled" @@ -1614,8 +1614,8 @@ msgid "TV binge-viewing mode" msgstr "TV Binge-Viewing-Modus" msgctxt "#33619" -msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show." -msgstr "Überspringt automatisch Intros und Abspänne von Episoden und versucht Recaps zu vermeiden. Überspringt das Intro der ersten Episode einer Staffel nicht." +msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season and doesn't skip the final credits of a show.\n\nCan be disabled/enabled per TV show.\nOverrides any setting below." +msgstr "Überspringt automatisch Intros und Abspänne von Episoden und versucht Recaps zu vermeiden. Überspringt das Intro der ersten Episode einer Staffel und den Abspann der letzten Episode einer Serie nicht.\n\nKann pro Serie aktiviert/deaktiviert werden.\nÜberschreibt jegliche Einstellung unterhalb dieser." msgctxt "#33620" msgid "Plex requests timeout (seconds)" @@ -1972,3 +1972,35 @@ msgstr "Kodi beenden anstatt PM4K" msgctxt "#32966" msgid "When showing the addon exit confirmation, use \"Quit Kodi\" as default option. Can be dynamically switched using CONTEXT_MENU (often longpress SELECT)" msgstr "Wenn die Addon-Beendigungsbestätigung angezeigt wird, \"Kodi beenden\" als Standardoption verwenden. Kann dynamisch mit CONTEXT_MENU (oft lange gedrücktes SELECT) getauscht werden." + +msgctxt "#32967" +msgid "Kodi Colour Management" +msgstr "Kodi Farbeinstellungen" + +msgctxt "#32968" +msgid "Kodi Resolution Settings" +msgstr "Kodi Auflösungseinstellungen" + +msgctxt "#32969" +msgid "Always request all library media items at once" +msgstr "Immer die vollständige Bibliothek laden" + +msgctxt "#32970" +msgid "Retrieve all media in library up front instead of fetching it in chunks as the user navigates through the library" +msgstr "Alle Medieninhalte einer Bibliothek anfordern, anstatt diese gestückelt nachzuladen" + +msgctxt "#32971" +msgid "Library item-request chunk size" +msgstr "Bibliothek Anfrage-Blockgröße" + +msgctxt "#32972" +msgid "Request this amount of media items per chunk request in library view (+6-30 depending on view mode; less can be less straining for the UI at first, but puts more strain on the server)" +msgstr "Diese Anzahl an Medieninhalten pro gestückelter Anfrage in der Bibliothekenansicht laden (+6-30 abhängig von der Ansicht; weniger kann vorerst geringere UI-Last bedeuten, aber mehr Last beim Server erzeugen)" + +msgctxt "#32973" +msgid "Episodes: Skip Post Play screen" +msgstr "Direkt zur nächsten Episode springen" + +msgctxt "#32974" +msgid "When finishing an episode, don't show Post Play but go to the next one immediately.\nCan be disabled/enabled per TV show. Doesn't override enabled binge mode. Overrides the Post Play setting." +msgstr "Beim Beenden einer Episode nicht in die Post Play Ansicht gehen und stattdessen sofort zur nächsten Episode springen.\nCan be disabled/enabled per TV show. Überschreibt aktivierten Binge-Modus nicht. Überschreibt die \"Automatisch nächsten Titel abspielen\"-Einstellung" diff --git a/script.plexmod/resources/language/resource.language.en_gb/strings.po b/script.plexmod/resources/language/resource.language.en_gb/strings.po index 02aa83b2c..528dfb536 100644 --- a/script.plexmod/resources/language/resource.language.en_gb/strings.po +++ b/script.plexmod/resources/language/resource.language.en_gb/strings.po @@ -1103,7 +1103,7 @@ msgid "Automatically Skip Intro" msgstr "" msgctxt "#32523" -msgid "Automatically skip intros if available. Doesn't override enabled binge mode." +msgid "Automatically skip intros if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." msgstr "" msgctxt "#32524" @@ -1119,7 +1119,7 @@ msgid "Automatically Skip Credits" msgstr "" msgctxt "#32527" -msgid "Automatically skip credits if available. Doesn't override enabled binge mode." +msgid "Automatically skip credits if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." msgstr "" msgctxt "#32528" @@ -1219,7 +1219,7 @@ msgid "Show intro skip button early" msgstr "" msgctxt "#33506" -msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies. Doesn\'t override enabled binge mode." +msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show." msgstr "" msgctxt "#33507" @@ -1315,7 +1315,7 @@ msgid "TV binge-viewing mode" msgstr "" msgctxt "#33619" -msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season.\n\nCan be disabled/enabled per TV show." +msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season and doesn't skip the final credits of a show.\n\nCan be disabled/enabled per TV show.\nOverrides any setting below." msgstr "" msgctxt "#33620" @@ -1673,3 +1673,39 @@ msgstr "" msgctxt "#32966" msgid "When exiting the addon, use \"Quit Kodi\" as default option. Can be dynamically switched using CONTEXT_MENU (often longpress SELECT)" msgstr "" + +msgctxt "#32967" +msgid "Kodi Colour Management" +msgstr "" + +msgctxt "#32968" +msgid "Kodi Resolution Settings" +msgstr "" + +msgctxt "#32969" +msgid "Always request all library media items at once" +msgstr "" + +msgctxt "#32970" +msgid "Retrieve all media in library up front instead of fetching it in chunks as the user navigates through the library" +msgstr "" + +msgctxt "#32971" +msgid "Library item-request chunk size" +msgstr "" + +msgctxt "#32972" +msgid "Request this amount of media items per chunk request in library view (+6-30 depending on view mode; less can be less straining for the UI at first, but puts more strain on the server)" +msgstr "" + +msgctxt "#32973" +msgid "Episodes: Skip Post Play screen" +msgstr "" + +msgctxt "#32974" +msgid "When finishing an episode, don't show Post Play but go to the next one immediately.\nCan be disabled/enabled per TV show. Doesn't override enabled binge mode. Overrides the Post Play setting." +msgstr "" + +msgctxt "#32975" +msgid "Delete Season" +msgstr "" diff --git a/script.plexmod/resources/language/resource.language.es_es/strings.po b/script.plexmod/resources/language/resource.language.es_es/strings.po index b8c6adca8..ae77eb6a4 100644 --- a/script.plexmod/resources/language/resource.language.es_es/strings.po +++ b/script.plexmod/resources/language/resource.language.es_es/strings.po @@ -4,15 +4,15 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" "POT-Creation-Date: 2013-12-12 22:56+0000\n" -"PO-Revision-Date: 2020-05-16 10:52+0200\n" +"PO-Revision-Date: 2024-01-28 13:15+0100\n" +"Last-Translator: DeciBelioS\n" "Language-Team: LANGUAGE\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"Last-Translator: \n" -"X-Generator: Poedit 2.0.2\n" +"X-Generator: Poedit 3.4.2\n" msgctxt "#32000" msgid "Main" @@ -84,7 +84,7 @@ msgstr "Calidad Remoto" msgctxt "#32022" msgid "Online Quality" -msgstr "Calidad Online" +msgstr "Calidad Internet" msgctxt "#32023" msgid "Transcode Format" @@ -96,11 +96,11 @@ msgstr "Log Depuración" msgctxt "#32025" msgid "Allow Direct Play" -msgstr "Permitir Reproducción en Directo" +msgstr "Permitir reproducción directa" msgctxt "#32026" msgid "Allow Direct Stream" -msgstr "Permitir Stream en Directo" +msgstr "Permitir transmisión directa" msgctxt "#32027" msgid "Force" @@ -119,8 +119,8 @@ msgid "Auto" msgstr "Auto" msgctxt "#32031" -msgid "Burn Subtitles (Direct Play Only)" -msgstr "Subtítulos (Solo Reproducción Directo)" +msgid "Burn-in Subtitles" +msgstr "Subtítulos quemados" msgctxt "#32032" msgid "Allow Insecure Connections" @@ -152,7 +152,7 @@ msgstr "Accesso automatico" msgctxt "#32039" msgid "Post Play Auto Play" -msgstr "Post Play Auto Play" +msgstr "Reproducción automática posterior" msgctxt "#32040" msgid "Enable Subtitle Downloading" @@ -226,6 +226,46 @@ msgctxt "#32057" msgid "Current Server Version" msgstr "Versión del Servidor" +msgctxt "#32058" +msgid "Never exceed original audio codec" +msgstr "No superar nunca el códec de audio original" + +msgctxt "#32059" +msgid "When transcoding audio, never exceed the original audio bitrate or channel count on the same codec." +msgstr "Cuando transcodifiques audio, nunca superes la tasa de bits o el número de canales del audio original en el mismo códec." + +msgctxt "#32060" +msgid "Use Kodi audio channels" +msgstr "Utilizar los canales de audio de Kodi" + +msgctxt "#32061" +msgid "When transcoding audio, target the audio channels set in Kodi." +msgstr "Al transcodificar audio, apunte a los canales de audio configurados en Kodi." + +msgctxt "#32062" +msgid "Transcode audio to AC3" +msgstr "Transcodificar audio a AC3" + +msgctxt "#32063" +msgid "Transcode audio to AC3 in certain conditions (useful for passthrough)." +msgstr "Transcodifica el audio a AC3 en determinadas condiciones (útil para passthrough)." + +msgctxt "#32064" +msgid "Treat DTS like AC3" +msgstr "Tratar DTS como AC3" + +msgctxt "#32065" +msgid "When any of the force AC3 settings are enabled, treat DTS the same as AC3 (useful for Optical passthrough)" +msgstr "Cuando cualquiera de los ajustes de forzar AC3 está activado, trata DTS igual que AC3 (útil para el passthrough)" + +msgctxt "#32066" +msgid "Force audio to AC3" +msgstr "Forzar audio a AC3" + +msgctxt "#32067" +msgid "Only force multichannel audio to AC3" +msgstr "Forzar sólo el audio multicanal a AC3" + msgctxt "#32100" msgid "Skip user selection and pin entry on startup." msgstr "Omitir selección de usuario y PIN al iniciar." @@ -348,7 +388,7 @@ msgstr "No disponible" msgctxt "#32313" msgid "This item is currently unavailable." -msgstr "Este elemento no está disponible actualmente" +msgstr "Este elemento no está disponible actualmente." msgctxt "#32314" msgid "In Progress" @@ -432,7 +472,7 @@ msgstr "Listas de reproducción" msgctxt "#32334" msgid "Confirm Exit" -msgstr "Salir realmente" +msgstr "Confirmar salida" msgctxt "#32335" msgid "Are you ready to exit Plex?" @@ -487,8 +527,8 @@ msgid "Artists" msgstr "Artistas" msgctxt "#32348" -msgid "movies" -msgstr "películas" +msgid "Movies" +msgstr "Películas" msgctxt "#32349" msgid "photos" @@ -949,3 +989,723 @@ msgstr "Artista" msgctxt "#32463" msgid "By Artist" msgstr "Por Artista" + +msgctxt "#32464" +msgid "Player" +msgstr "Reproductor" + +msgctxt "#32465" +msgid "Use skip step settings from Kodi" +msgstr "Utilizar la configuración de salto de paso de Kodi" + +msgctxt "#32466" +msgid "Automatically seek selected position after a delay" +msgstr "Búsqueda automática de la posición seleccionada tras un retardo" + +msgctxt "#32467" +msgid "User Interface" +msgstr "Interfaz de usuario" + +msgctxt "#32468" +msgid "Show dynamic background art" +msgstr "Mostrar arte de fondo dinámico" + +msgctxt "#32469" +msgid "Background art blur amount" +msgstr "Cantidad de desenfoque del arte de fondo" + +msgctxt "#32470" +msgid "Background art opacity" +msgstr "Opacidad del arte de fondo" + +msgctxt "#32471" +msgid "Use Plex/Kodi steps for timeline" +msgstr "Utiliza los pasos de Plex/Kodi para la línea de tiempo" + +msgctxt "#32480" +msgid "Theme music" +msgstr "Tema musical" + +msgctxt "#32481" +msgid "Off" +msgstr "Apagado" + +msgctxt "#32482" +msgid "%(percentage)s %%" +msgstr "%(percentage)s %%" + +msgctxt "#32483" +msgid "Hide Stream Info" +msgstr "Ocultar mediainfo" + +msgctxt "#32484" +msgid "Show Stream Info" +msgstr "Mostrar mediainfo" + +msgctxt "#32485" +msgid "Go back instantly with the previous menu action in scrolled views" +msgstr "Retroceder instantáneamente con la acción del menú anterior en vistas desplazadas" + +msgctxt "#32487" +msgid "Seek Delay" +msgstr "Retraso de búsqueda" + +msgctxt "#32488" +msgid "Screensaver" +msgstr "Salvapantallas" + +msgctxt "#32489" +msgid "Quiz Mode" +msgstr "Modo concurso" + +msgctxt "#32490" +msgid "Collections" +msgstr "Colecciones" + +msgctxt "#32491" +msgid "Folders" +msgstr "Carpetas" + +msgctxt "#32492" +msgid "Kodi Subtitle Settings" +msgstr "Configuración de subtítulos de Kodi" + +msgctxt "#32493" +msgid "When a media file has a forced/foreign subtitle for a subtitle-enabled language, the Plex Media Server preselects it. This behaviour is usually not necessary and not configurable. This setting fixes that by ignoring the PMSs decision and selecting the same language without a forced flag if possible." +msgstr "Cuando un archivo multimedia tiene un subtítulo forzado/extranjero para un idioma habilitado para subtítulos, el Plex Media Server lo preselecciona. Este comportamiento no suele ser necesario y no es configurable. Este ajuste lo soluciona ignorando la decisión del PMS y seleccionando el mismo idioma sin bandera forzada si es posible." + +msgctxt "#32495" +msgid "Skip intro" +msgstr "Saltar introducción" + +msgctxt "#32496" +msgid "Skip credits" +msgstr "Saltar créditos" + +msgctxt "#32500" +msgid "Always show post-play screen (even for short videos)" +msgstr "Mostrar siempre la pantalla posterior a la reproducción (incluso para vídeos cortos)" + +msgctxt "#32501" +msgid "Time-to-wait between videos on post-play" +msgstr "Tiempo de espera entre vídeos en post-play" + +msgctxt "#32505" +msgid "Visit media in video playlist instead of playing it" +msgstr "Visitar medios en la lista de reproducción de vídeo en lugar de reproducirlos" + +msgctxt "#32521" +msgid "Skip Intro Button Timeout" +msgstr "Tiempo de espera del botón de intro" + +msgctxt "#32522" +msgid "Automatically Skip Intro" +msgstr "Saltar automáticamente la introducción" + +msgctxt "#32523" +msgid "Automatically skip intros if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Salta automáticamente las intros si están disponibles. No anula el modo atracón activado.\nPuede desactivarse/activarse por programa de TV." + +msgctxt "#32524" +msgid "Set how long the skip intro button shows for." +msgstr "Establece el tiempo que se mostrará el botón de salto de introducción." + +msgctxt "#32525" +msgid "Skip Credits Button Timeout" +msgstr "Salto del tiempo de espera del botón de créditos" + +msgctxt "#32526" +msgid "Automatically Skip Credits" +msgstr "Saltar créditos automáticamente" + +msgctxt "#32527" +msgid "Automatically skip credits if available. Doesn't override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Salta automáticamente los créditos si están disponibles. No anula el modo atracón activado.\nPuede desactivarse/activarse por programa de TV." + +msgctxt "#32528" +msgid "Set how long the skip credits button shows for." +msgstr "Establece el tiempo que se mostrará el botón de saltar créditos." + +msgctxt "#32540" +msgid "Show when the current video will end in player" +msgstr "Mostrar cuándo terminará el vídeo actual en el reproductor" + +msgctxt "#32541" +msgid "Shows time left and at which time the media will end." +msgstr "Muestra el tiempo restante y a qué hora terminará el medio." + +msgctxt "#32542" +msgid "Show \"Ends at\" label for the end-time as well" +msgstr "Mostrar la etiqueta \"Finaliza en\" también para la hora de finalización" + +msgctxt "#32543" +msgid "Ends at" +msgstr "Termina en" + +msgctxt "#32601" +msgid "Allow AV1" +msgstr "Permitir AV1" + +msgctxt "#32602" +msgid "Enable this if your hardware can handle AV1. Disable it to force transcoding." +msgstr "Actívelo si su hardware puede manejar AV1. Desactívalo para forzar la transcodificación." + +msgctxt "#33101" +msgid "By Audience Rating" +msgstr "Por índice de audiencia" + +msgctxt "#33102" +msgid "Audience Rating" +msgstr "Clasificación del público" + +msgctxt "#33103" +msgid "By my Rating" +msgstr "Según mi valoración" + +msgctxt "#33104" +msgid "My Rating" +msgstr "Mi valoración" + +msgctxt "#33105" +msgid "By Content Rating" +msgstr "Por clasificación de contenidos" + +msgctxt "#33106" +msgid "Content Rating" +msgstr "Clasificación del contenido" + +msgctxt "#33107" +msgid "By Critic Rating" +msgstr "Por valoración crítica" + +msgctxt "#33108" +msgid "Critic Rating" +msgstr "Valoración de la crítica" + +msgctxt "#33200" +msgid "Background Color" +msgstr "Color de fondo" + +msgctxt "#33201" +msgid "Specify solid Background Color instead of using media images" +msgstr "Especifique un color de fondo sólido en lugar de utilizar imágenes multimedia" + +msgctxt "#33400" +msgid "Use old compatibility profile" +msgstr "Utilizar el antiguo perfil de compatibilidad" + +msgctxt "#33401" +msgid "Uses the Chrome client profile instead of the custom one. Might fix rare issues with 3D playback." +msgstr "Utiliza el perfil de cliente de Chrome en lugar del personalizado. Podría solucionar problemas poco frecuentes con la reproducción 3D." + +msgctxt "#33501" +msgid "Video played threshold" +msgstr "Umbral de reproducción de vídeo" + +msgctxt "#33502" +msgid "Set this to the same value as your Plex server (Settings>Library>Video played threshold) to avoid certain pitfalls, Default: 90 %" +msgstr "Ajústelo al mismo valor que su servidor Plex (Configuración>Biblioteca>Umbral de vídeo reproducido) para evitar ciertas trampas, Predeterminado: 90%" + +msgctxt "#33503" +msgid "Use alternative hubs refresh" +msgstr "Utilizar centros alternativos refrescar" + +msgctxt "#33504" +msgid "Refreshes all hubs for all libraries after an item's watch-state has changed, instead of only those likely affected. Use this if you find a hub that doesn't update properly." +msgstr "Actualiza todos los concentradores de todas las bibliotecas después de que el estado de vigilancia de un elemento haya cambiado, en lugar de sólo los probablemente afectados. Utilícelo si encuentra un concentrador que no se actualiza correctamente." + +msgctxt "#33505" +msgid "Show intro skip button early" +msgstr "Mostrar el botón de salto de introducción antes de tiempo" + +msgctxt "#33506" +msgid "Show the intro skip button from the start of a video with an intro marker. The auto-skipping setting applies. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show." +msgstr "Mostrar el botón de salto de introducción desde el inicio de un vídeo con un marcador de introducción. Se aplica la configuración de salto automático. No anula el modo atracón activado.\nPuede desactivarse/activarse por programa de TV." + +msgctxt "#33507" +msgid "Enabled" +msgstr "Activado" + +msgctxt "#33508" +msgid "Disabled" +msgstr "Desactivado" + +msgctxt "#33509" +msgid "Early intro skip threshold (default: < 60s/1m)" +msgstr "Umbral de salto de introducción temprana (por defecto: < 60s/1m)" + +msgctxt "#33510" +msgid "When showing the intro skip button early, only do so if the intro occurs within the first X seconds." +msgstr "Cuando muestre el botón de salto de introducción antes de tiempo, hágalo sólo si la introducción se produce en los primeros X segundos." + +msgctxt "#33600" +msgid "System" +msgstr "Sistema" + +msgctxt "#33601" +msgid "Show video chapters" +msgstr "Mostrar capítulos de vídeo" + +msgctxt "#33602" +msgid "If available, show video chapters from the video-file instead of the timeline-big-seek-steps." +msgstr "Si está disponible, mostrar capítulos de vídeo del archivo de vídeo en lugar de la línea de tiempo-grandes-pasos-de-búsqueda." + +msgctxt "#33603" +msgid "Use virtual chapters" +msgstr "Utilizar capítulos virtuales" + +msgctxt "#33604" +msgid "When the above is enabled and no video chapters are available, simulate them by using the markers identified by the Plex Server (Intro, Credits)." +msgstr "Cuando lo anterior esté activado y no haya capítulos de vídeo disponibles, simúlelos utilizando los marcadores identificados por el Servidor Plex (Intro, Créditos)." + +msgctxt "#33605" +msgid "Video Chapters" +msgstr "Capítulos de vídeo" + +msgctxt "#33606" +msgid "Virtual Chapters" +msgstr "Capítulos virtuales" + +msgctxt "#33607" +msgid "Chapter {}" +msgstr "Capítulo {}" + +msgctxt "#33608" +msgid "Intro" +msgstr "Introducción" + +msgctxt "#33609" +msgid "Credits" +msgstr "Créditos" + +msgctxt "#33610" +msgid "Main" +msgstr "Principal" + +msgctxt "#33611" +msgid "Chapters" +msgstr "Capítulos" + +msgctxt "#33612" +msgid "Markers" +msgstr "Marcadores" + +msgctxt "#33613" +msgid "Kodi Buffer Size (MB)" +msgstr "Tamaño del búfer de Kodi (MB)" + +msgctxt "#33614" +msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~100 MB, Recommended max: {} MB, Default: 20 MB." +msgstr "Establece el tamaño de la Caché/Buffer de Kodi. Libre: {} MB, Recomendado: ~100 MB, Máximo recomendado: {} MB, Por defecto: 20 MB." + +msgctxt "#33615" +msgid "{time} left" +msgstr "{time} restante" + +msgctxt "#33616" +msgid "Addon Path" +msgstr "Ruta de Addon" + +msgctxt "#33617" +msgid "Userdata/Profile Path" +msgstr "Ruta de datos de usuario/perfil" + +msgctxt "#33618" +msgid "TV binge-viewing mode" +msgstr "Modo \"atracón\" de TV" + +msgctxt "#33619" +msgid "Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t skip the intro of the first episode of a season and doesn't skip the final credits of a show.\n\nCan be disabled/enabled per TV show.\nOverrides any setting below." +msgstr "Se salta automaticamente las intros de los episodios, los créditos e intenta saltarse los resúmenes de los episodios. No salta la intro del primer episodio de una temporada y no salta los créditos finales de un programa.\nPuede ser desactivado/activado por programa de TV.\nAnula cualquier configuración de abajo." + +msgctxt "#33620" +msgid "Plex requests timeout (seconds)" +msgstr "Tiempo de espera de las solicitudes de Plex (segundos)" + +msgctxt "#33621" +msgid "Set the (async and connection) timeout value of the Python requests library in seconds. Default: 5" +msgstr "Establece el valor del tiempo de espera (asíncrono y de conexión) de la biblioteca de peticiones de Python en segundos. Predeterminado: 5" + +msgctxt "#33622" +msgid "LAN reachability timeout (ms)" +msgstr "Tiempo de espera de alcanzabilidad de LAN (ms)" + +msgctxt "#33623" +msgid "When checking for LAN reachability, use this timeout. Default: 10ms" +msgstr "Cuando compruebe la accesibilidad de la LAN, utilice este tiempo de espera. Por defecto: 10ms" + +msgctxt "#33624" +msgid "Network" +msgstr "Red" + +msgctxt "#33625" +msgid "Smart LAN/local server discovery" +msgstr "Detección inteligente de LAN/servidores locales" + +msgctxt "#33626" +msgid "Checks whether servers returned from Plex.tv are actually local/in your LAN. For specific setups (e.g. Docker) Plex.tv might not properly detect a local server.\n\nNOTE: Only works on Kodi 19 or above." +msgstr "Comprueba si los servidores devueltos por Plex.tv son realmente locales en su LAN. Para configuraciones específicas (por ejemplo, Docker) Plex.tv podría no detectar correctamente un servidor local.\n\nNOTA: Sólo funciona en Kodi 19 o superior." + +msgctxt "#33627" +msgid "Prefer LAN/local servers over security" +msgstr "Preferir los servidores LAN/locales a la seguridad" + +msgctxt "#33628" +msgid "Prioritizes local connections over secure ones. Needs the proper setting in \"Allow Insecure Connections\" and the Plex Server's \"Secure connections\" at \"Preferred\". Can be used to enforce manual servers." +msgstr "Prioriza las conexiones locales sobre las seguras. Necesita la configuración adecuada en \"Permitir conexiones inseguras\" y las \"Conexiones seguras\" del servidor Plex en \"Preferidas\". Puede utilizarse para reforzar servidores manuales." + +msgctxt "#33629" +msgid "Auto-skip intro/credits offset" +msgstr "Desplazamiento automático de introducción/créditos" + +msgctxt "#33630" +msgid "Intro/credits markers might be a little early in Plex. When auto skipping add (or subtract) this many seconds from the marker. This avoids cutting off content, while possibly skipping the marker a little late." +msgstr "Los marcadores de introducción/créditos pueden ser un poco tempranos en Plex. Al saltar automáticamente, añada (o reste) esta cantidad de segundos al marcador. Esto evita cortar el contenido, mientras que posiblemente salta el marcador un poco tarde." + +msgctxt "#32631" +msgid "Playback (user-specific)" +msgstr "Reproducción (específica del usuario)" + +msgctxt "#33632" +msgid "Server connectivity check timeout (seconds)" +msgstr "Tiempo de espera de comprobación de conectividad del servidor (segundos)" + +msgctxt "#33633" +msgid "Set the maximum amount of time a server connection has to answer a connectivity request. Default: 2.5" +msgstr "Establece la cantidad máxima de tiempo que una conexión de servidor tiene para responder a una solicitud de conectividad. Predeterminado: 2,5" + +msgctxt "#33634" +msgid "Combined Chapters" +msgstr "Capítulos combinados" + +msgctxt "#33635" +msgid "Final Credits" +msgstr "Créditos finales" + +msgctxt "#32700" +msgid "Action on Sleep event" +msgstr "Acción sobre el evento de suspensión" + +msgctxt "#32701" +msgid "When Kodi receives a sleep event from the system, run the following action." +msgstr "Cuando Kodi reciba un evento de suspensión del sistema, ejecuta la siguiente acción." + +msgctxt "#32702" +msgid "Nothing" +msgstr "Nada" + +msgctxt "#32703" +msgid "Stop playback" +msgstr "Detener la reproducción" + +msgctxt "#32704" +msgid "Quit Kodi" +msgstr "Salir de Kodi" + +msgctxt "#32705" +msgid "CEC Standby" +msgstr "CEC En espera" + +msgctxt "#32800" +msgid "Skipping intro" +msgstr "Saltar introducción" + +msgctxt "#32801" +msgid "Skipping credits" +msgstr "Saltar créditos" + +msgctxt "#32900" +msgid "While playing back an item and seeking on the seekbar, automatically seek to the selected position after a delay instead of having to confirm the selection." +msgstr "Mientras se reproduce un elemento y se busca en la barra de búsqueda, se busca automáticamente la posición seleccionada tras un retardo en lugar de tener que confirmar la selección." + +msgctxt "#32901" +msgid "Seek delay in seconds." +msgstr "Retraso de búsqueda en segundos." + +msgctxt "#32902" +msgid "Kodi has its own skip step settings. Try to use them if they're configured instead of the default ones." +msgstr "Kodi tiene sus propios ajustes para saltar pasos. Intenta usarlos si están configurados en lugar de los predeterminados." + +msgctxt "#32903" +msgid "Use the above for seeking on the timeline as well." +msgstr "Utilice lo anterior también para buscar en la línea de tiempo." + +msgctxt "#32904" +msgid "In seconds." +msgstr "En segundos." + +msgctxt "#32905" +msgid "Cancel post-play timer by pressing OK/SELECT" +msgstr "Cancelar el temporizador post-play pulsando OK/SELECCIONAR" + +msgctxt "#32906" +msgid "Cancel skip marker timer with BACK" +msgstr "Cancelar el temporizador de salto de marcador con VOLVER" + +msgctxt "#32907" +msgid "When auto-skipping a marker, allow cancelling the timer by pressing BACK." +msgstr "Cuando se salta automáticamente un marcador, permite cancelar el temporizador pulsando VOLVER." + +msgctxt "#32908" +msgid "Immediately skip marker with OK/SELECT" +msgstr "Saltar inmediatamente el marcador con OK/SELECCIONAR" + +msgctxt "#32909" +msgid "When auto-skipping a marker with a timer, allow skipping immediately by pressing OK/SELECT." +msgstr "Cuando se omita automáticamente un marcador con temporizador, permita la omisión inmediatamente pulsando OK/SELECCIONAR." + +msgctxt "#32912" +msgid "Show buffer-state on timeline" +msgstr "Mostrar el estado de la memoria intermedia en la línea de tiempo" + +msgctxt "#32913" +msgid "Shows the current Kodi buffer/cache state on the video player timeline." +msgstr "Muestra el estado actual del búfer/caché de Kodi en la línea de tiempo del reproductor de vídeo." + +msgctxt "#32914" +msgid "Loading" +msgstr "Cargando" + +msgctxt "#32915" +msgid "Slow connection" +msgstr "Conexión lenta" + +msgctxt "#32916" +msgid "Use with a wonky/slow connection, e.g. in a hotel room. Adjusts the UI to visually wait for item refreshes and waits for the buffer to fill when starting playback. Automatically sets readfactor=20, requires Kodi restart." +msgstr "Utilícelo con una conexión lenta o inestable, por ejemplo, en una habitación de hotel. Ajusta la interfaz de usuario para esperar visualmente a que se actualicen los elementos y espera a que se llene el búfer al iniciar la reproducción. Establece automáticamente readfactor=20, requiere reiniciar Kodi." + +msgctxt "#32917" +msgid "Couldn't fill buffer in time ({}s)" +msgstr "No se ha podido llenar el buffer a tiempo ({}s)" + +msgctxt "#32918" +msgid "Buffer wait timeout (seconds)" +msgstr "Tiempo de espera del búfer (segundos)" + +msgctxt "#32919" +msgid "When slow connection is enabled in the addon, wait this long for the buffer to fill. Default: 120 s" +msgstr "Cuando la conexión lenta está activada en el addon, espera este tiempo a que se llene el búfer. Por defecto: 120 s" + +msgctxt "#32920" +msgid "Insufficient buffer wait (seconds)" +msgstr "Espera de búfer insuficiente (segundos)" + +msgctxt "#32921" +msgid "When slow connection is enabled in the addon and the configured buffer isn't big enough for us to determine its fill state, wait this long when starting playback. Default: 10 s" +msgstr "Cuando la conexión lenta está activada en el addon y el búfer configurado no es lo suficientemente grande como para que podamos determinar su estado de llenado, espere este tiempo al iniciar la reproducción. Predeterminado: 10 s" + +msgctxt "#32922" +msgid "Kodi Cache Readfactor" +msgstr "Factor de lectura de la caché de Kodi" + +msgctxt "#32923" +msgid "Sets the Kodi cache readfactor value. Default: {0}, recommended: {1}. With \"Slow connection\" enabled this will be set to {2}, as otherwise the cache doesn't fill fast/aggressively enough." +msgstr "Establece el valor del factor de lectura de la caché de Kodi. Por defecto: {0}, recomendado: {1}. Con \"Conexión lenta\" activada, este valor será {2}, ya que de lo contrario la caché no se llena lo suficientemente rápido/agresivamente." + +msgctxt "#32924" +msgid "Minimize" +msgstr "Minimizar" + +msgctxt "#32925" +msgid "Playback Settings" +msgstr "Ajustes de reproducción" + +msgctxt "#32926" +msgid "Wrong pin entered!" +msgstr "¡Pin introducido erróneo!" + +msgctxt "#32927" +msgid "Use episode thumbnails in continue hub" +msgstr "Utilizar miniaturas de episodios en el hub de continuación" + +msgctxt "#32928" +msgid "Instead of using media artwork, use thumbnails for episodes in the continue hub on the home screen if available." +msgstr "En lugar de utilizar ilustraciones multimedia, utiliza miniaturas de los episodios en el hub de continuación de la pantalla de inicio, si está disponible." + +msgctxt "#32929" +msgid "Use legacy background fallback image" +msgstr "Utilizar la imagen de fondo anterior" + +msgctxt "#32930" +msgid "Previous Subtitle" +msgstr "Subtítulos anteriores" + +msgctxt "#32931" +msgid "Audio/Subtitles" +msgstr "Audio/Subtítulos" + +msgctxt "#32932" +msgid "Show subtitle quick-actions button" +msgstr "Botón de acciones rápidas para mostrar subtítulos" + +msgctxt "#32933" +msgid "Show FFWD/RWD buttons" +msgstr "Mostrar botones FFWD/RWD" + +msgctxt "#32934" +msgid "Show repeat button" +msgstr "Mostrar botón de repetición" + +msgctxt "#32935" +msgid "Show shuffle button" +msgstr "Botón de reproducción aleatoria" + +msgctxt "#32936" +msgid "Show playlist button" +msgstr "Botón Mostrar lista de reproducción" + +msgctxt "#32937" +msgid "Show prev/next button" +msgstr "Mostrar botón anterior/siguiente" + +msgctxt "#32938" +msgid "Only for Episodes" +msgstr "Sólo para episodios" + +msgctxt "#32939" +msgid "Only applies to video player UI" +msgstr "Sólo se aplica a la interfaz de usuario del reproductor de vídeo" + +msgctxt "#32940" +msgid "Player UI" +msgstr "Interfaz del reproductor" + +msgctxt "#32941" +msgid "Forced subtitles fix" +msgstr "Corrección de subtítulos forzados" + +msgctxt "#32942" +msgid "Other seasons" +msgstr "Otras temporadas" + +msgctxt "#32943" +msgid "Crossfade dynamic background art" +msgstr "Arte de fondo dinámico con fundido cruzado" + +msgctxt "#32944" +msgid "Burn-in SSA subtitles (DirectStream)" +msgstr "Subtítulos SSA quemados (DirectStream)" + +msgctxt "#32945" +msgid "When Direct Streaming instruct the Plex Server to burn in SSA/ASS subtitles (thus transcoding the video stream). If disabled it will not touch the video stream, but will convert the subtitle to unstyled text." +msgstr "Cuando se hace Direct Streaming, ordena al Servidor Plex que grabe los subtítulos SSA/ASS (transcodificando así el flujo de vídeo). Si se desactiva no tocará el flujo de vídeo, pero convertirá los subtítulos en texto sin estilo." + +msgctxt "#32946" +msgid "Stop video playback on idle after" +msgstr "Detener la reproducción de vídeo en reposo después de" + +msgctxt "#32947" +msgid "Stop video playback on screensaver" +msgstr "Detener la reproducción de vídeo en el salvapantallas" + +msgctxt "#32948" +msgid "Allow auto-skip when transcoding" +msgstr "Permitir el salto automático al transcodificar" + +msgctxt "#32949" +msgid "When transcoding/DirectStreaming, allow auto-skip functionality." +msgstr "Al transcodificar/transmitir directamente, permita la función de salto automático." + +msgctxt "#32950" +msgid "Use extended title for subtitles" +msgstr "Utilizar el título ampliado para los subtítulos" + +msgctxt "#32951" +msgid "When displaying subtitles use the extendedDisplayTitle Plex exposes." +msgstr "Cuando muestre subtítulos utilice el título de pantalla extendida que Plex expone." + +msgctxt "#32952" +msgid "Dialog flicker fix" +msgstr "Corrección del parpadeo de los diálogos" + +msgctxt "#32953" +msgid "Reviews" +msgstr "Reseñas" + +msgctxt "#32954" +msgid "Needs Kodi restart. WARNING: This will overwrite advancedsettings.xml!\n\nTo customize other cache/network-related values, copy \"script.plexmod/pm4k_cache_template.xml\" to profile folder and edit it to your liking. (See About section for the file paths)" +msgstr "Necesita reiniciar Kodi. ADVERTENCIA: ¡Esto sobrescribirá advancedsettings.xml!\n\nPara personalizar otros valores relacionados con la caché/red, copia \"script.plexmod/pm4k_cache_template.xml\" a la carpeta profile y edítalo a tu gusto. (Consulta la sección Acerca de para ver las rutas de los archivos)" + +msgctxt "#32955" +msgid "Use Kodi keyboard for searching" +msgstr "Usa el teclado de Kodi para buscar" + +msgctxt "#32956" +msgid "Poster resolution scaling %" +msgstr "Escalado de la resolución del póster %" + +msgctxt "#32957" +msgid "In percent. Scales the resolution of all posters/thumbnails for better image quality. May impact PMS/PM4K performance, will increase the cache usage accordingly. Recommended: 200-300 % for for big screens if your hardware can handle it. Needs addon restart." +msgstr "En porcentaje. Escala la resolución de todos los pósters/miniaturas para una mejor calidad de imagen. Puede afectar al rendimiento de PMS/PM4K, aumentará el uso de caché en consecuencia. Recomendado: 200-300 % para pantallas grandes si tu hardware puede soportarlo. Necesita reiniciar el addon." + +msgctxt "#32958" +msgid "Calculate OpenSubtitles.com hash" +msgstr "Calcular el hash de OpenSubtitles.com" + +msgctxt "#32959" +msgid "When opening the subtitle download feature, automatically calculate the OpenSubtitles.com hash for the given file. Can improve search results, downloads 2*64 KB of the video file to calculate the hash." +msgstr "Al abrir la función de descarga de subtítulos, calcula automáticamente el hash de OpenSubtitles.com para el archivo dado. Puede mejorar los resultados de búsqueda, descarga 2*64 KB del archivo de vídeo para calcular el hash." + +msgctxt "#32960" +msgid "Similar Artists" +msgstr "Artistas similares" + +msgctxt "#32961" +msgid "Show hub bifurcation lines" +msgstr "Mostrar líneas de bifurcación del buje" + +msgctxt "#32962" +msgid "Visually separate hubs horizontally using a thin line." +msgstr "Separe visualmente los cubos horizontalmente mediante una línea fina." + +msgctxt "#32963" +msgid "Wait between videos (s)" +msgstr "Espera entre vídeos (s)" + +msgctxt "#32964" +msgid "When playing back consecutive videos (e.g. TV shows), wait this long before starting the next one in the queue. Might fix compatibility issues with certain configurations." +msgstr "Al reproducir vídeos consecutivos (por ejemplo, programas de TV), espere este tiempo antes de iniciar el siguiente de la cola. Podría solucionar problemas de compatibilidad con determinadas configuraciones." + +msgctxt "#32965" +msgid "Quit Kodi on exit by default" +msgstr "Salir de Kodi al salir por defecto" + +msgctxt "#32966" +msgid "When exiting the addon, use \"Quit Kodi\" as default option. Can be dynamically switched using CONTEXT_MENU (often longpress SELECT)" +msgstr "Al salir del addon, usa \"Salir de Kodi\" como opción por defecto. Se puede cambiar dinámicamente usando CONTEXT_MENU (a menudo pulsando prolongadamente SELECCIONAR)" + +msgctxt "#32967" +msgid "Kodi Colour Management" +msgstr "Gestión del color en Kodi" + +msgctxt "#32968" +msgid "Kodi Resolution Settings" +msgstr "Ajustes de resolución de Kodi" + +msgctxt "#32969" +msgid "Always request all library media items at once" +msgstr "Solicite siempre todos los materiales de la biblioteca a la vez" + +msgctxt "#32970" +msgid "Retrieve all media in library up front instead of fetching it in chunks as the user navigates through the library" +msgstr "Recuperar todos los medios de la biblioteca por adelantado en lugar de hacerlo por partes a medida que el usuario navega por la biblioteca" + +msgctxt "#32971" +msgid "Library item-request chunk size" +msgstr "Tamaño del fragmento de solicitud de ítem de biblioteca" + +msgctxt "#32972" +msgid "Request this amount of media items per chunk request in library view (+6-30 depending on view mode; less can be less straining for the UI at first, but puts more strain on the server)" +msgstr "Solicita esta cantidad de elementos multimedia por petición de chunk en la vista de biblioteca (+6-30 dependiendo del modo de vista; menos puede ser menos estresante para la interfaz de usuario al principio, pero pone más tensión en el servidor)" + +msgctxt "#32973" +msgid "Episodes: Skip Post Play screen" +msgstr "Episodios: Saltar pantalla de reproducción" + +msgctxt "#32974" +msgid "When finishing an episode, don't show Post Play but go to the next one immediately.\nCan be disabled/enabled per TV show. Doesn't override enabled binge mode. Overrides the Post Play setting." +msgstr "Al terminar un episodio, no muestra Post Play sino que pasa al siguiente inmediatamente.\nPuede desactivarse/activarse por programa de TV. No anula el modo atracón activado. Anula la configuración de Post Play." + +msgctxt "#32975" +msgid "Delete Season" +msgstr "Borrar temporada" diff --git a/script.plexmod/resources/settings.xml b/script.plexmod/resources/settings.xml index 51d386d7e..b994c848f 100644 --- a/script.plexmod/resources/settings.xml +++ b/script.plexmod/resources/settings.xml @@ -35,6 +35,30 @@ false + + 0 + false + + + + 0 + 240 + + + + + + + + + + false + + + false + + + @@ -198,7 +222,7 @@ 0 - false + true diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml index c3cfee2b3..38fe38352 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml @@ -95,10 +95,13 @@ - true + !String.IsEmpty(Window.Property(current_item.loaded)) + !String.IsEmpty(Window.Property(current_item.loaded)) Focus UnFocus + 304 + 305 0 0 176 @@ -108,6 +111,20 @@ script.plex/buttons/play.png + + String.IsEmpty(Window.Property(current_item.loaded)) + + 304 + 305 + 0 + 0 + 176 + 140 + font12 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + Focus UnFocus @@ -174,7 +191,11 @@ + !String.IsEmpty(Window.Property(current_item.loaded)) + !String.IsEmpty(Window.Property(current_item.loaded)) + 1304 + 1307 0 0 161 @@ -184,6 +205,22 @@ script.plex/buttons/play.png + + String.IsEmpty(Window.Property(current_item.loaded)) + + 1304 + 1307 + 0 + 0 + 161 + 125 + font12 + 304 + 305 + script.plex/buttons/play-focus.png + script.plex/buttons/play.png + + 0 @@ -266,19 +303,21 @@ 776 0 - 1360 + 880 60 left 0 horizontal true - auto + auto 60 font13 left top FFFFFFFF + true + 5 @@ -298,20 +337,77 @@ + + !String.IsEmpty(Container(400).ListItem.Property(rating.stars)) + 1726 + 6 + 134 + 22 + script.plex/stars/$INFO[Container(400).ListItem.Property(rating.stars)].png + 776 50 - 1360 + 714 60 font13 left top + true + 5 FFFFFFFF + + + !String.IsEmpty(Container(400).ListItem.Property(rating)) | !String.IsEmpty(Container(400).ListItem.Property(rating2)) + 1560 + 50 + 300 + 32 + right + 15 + horizontal + true + + !String.IsEmpty(Container(400).ListItem.Property(rating)) + 2 + 63 + 30 + $INFO[Container(400).ListItem.Property(rating.image)] + keep + + + !String.IsEmpty(Container(400).ListItem.Property(rating)) + auto + 30 + font12 + left + FFFFFFFF + + + + !String.IsEmpty(Container(400).ListItem.Property(rating2)) + 2 + 40 + 30 + $INFO[Container(400).ListItem.Property(rating2.image)] + keep + + + !String.IsEmpty(Container(400).ListItem.Property(rating2)) + auto + 30 + font12 + left + FFFFFFFF + + + + 776 - 140 + 100 1360 30 left @@ -342,10 +438,9 @@ - 776 - 188 + 148 1360 34 left @@ -360,22 +455,6 @@ FFFFFFFF - - !String.IsEmpty(Container(400).ListItem.Property(rating.stars)) - auto - 34 - font12 - left - FFFFFFFF - - - - !String.IsEmpty(Container(400).ListItem.Property(rating.stars)) - 6 - 134 - 22 - script.plex/stars/$INFO[Container(400).ListItem.Property(rating.stars)].png - !String.IsEmpty(Container(400).ListItem.Property(video.res)) 10 @@ -393,48 +472,16 @@ - - !String.IsEmpty(Container(400).ListItem.Property(rating)) - 1560 - 140 - 300 - 32 - right - 15 - horizontal - true - - 2 - 63 - 30 - $INFO[Container(400).ListItem.Property(rating.image)] - keep - - - auto - 30 - font12 - left - FFFFFFFF - - - - !String.IsEmpty(Container(400).ListItem.Property(rating2)) - 2 - 40 - 30 - $INFO[Container(400).ListItem.Property(rating2.image)] - keep - - - !String.IsEmpty(Container(400).ListItem.Property(rating2)) - auto - 30 - font12 - left - FFFFFFFF - - + + !String.IsEmpty(Container(400).ListItem.Property(directors)) | !String.IsEmpty(Container(400).ListItem.Property(writers)) + 776 + 194 + 1360 + 30 + font12 + left + 99FFFFFF + @@ -526,6 +573,7 @@ + !String.IsEmpty(Window.Property(initialized)) 0 @@ -533,7 +581,8 @@ 1920 1800 - 300 + 300 + 1300 0 @@ -557,10 +606,11 @@ 18 1920 360 - 300 + 300 + 1300 401 - false - false + noop + noop 200 horizontal 4 @@ -922,6 +972,27 @@ + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 230 + + 0 + 0 + 158 + 6 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 158 + 4 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + false 0 @@ -1003,6 +1074,27 @@ + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 230 + + 0 + 0 + 158 + 6 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 158 + 4 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + Control.HasFocus(401) 0 @@ -1365,8 +1457,8 @@ 520 403 404 - false - false + noop + noop 200 horizontal 4 diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml index 23303dfef..2817f0deb 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml @@ -378,6 +378,7 @@ 440 101 401 + noop 200 horizontal 4 @@ -720,6 +721,7 @@ 515 400 402 + noop 200 horizontal 4 @@ -1054,6 +1056,7 @@ 515 401 403 + noop 200 horizontal 4 @@ -1388,6 +1391,7 @@ 515 402 404 + noop 200 horizontal 4 @@ -1722,6 +1726,7 @@ 515 403 405 + noop 200 horizontal 4 @@ -2056,6 +2061,7 @@ 392 404 406 + noop 200 horizontal 4 @@ -2313,6 +2319,7 @@ 440 405 407 + noop 200 horizontal 4 @@ -2655,6 +2662,7 @@ 515 406 408 + noop 200 horizontal 4 @@ -2989,6 +2997,7 @@ 515 407 409 + noop 200 horizontal 4 @@ -3323,6 +3332,7 @@ 392 408 410 + noop 200 horizontal 4 @@ -3581,6 +3591,7 @@ 392 409 411 + noop 200 horizontal 4 @@ -3839,6 +3850,7 @@ 392 410 412 + noop 200 horizontal 4 @@ -4097,6 +4109,7 @@ 392 411 420 + noop 200 horizontal 4 @@ -4355,6 +4368,7 @@ 392 412 421 + noop 200 horizontal 4 @@ -4613,6 +4627,7 @@ 392 420 422 + noop 200 horizontal 4 @@ -4871,6 +4886,7 @@ 392 421 413 + noop 200 horizontal 4 @@ -5129,6 +5145,7 @@ 515 422 414 + noop 200 horizontal 4 @@ -5463,6 +5480,7 @@ 515 413 415 + noop 200 horizontal 4 @@ -5797,6 +5815,7 @@ 515 414 416 + noop 200 horizontal 4 @@ -6131,6 +6150,7 @@ 515 415 417 + noop 200 horizontal 4 @@ -6464,6 +6484,7 @@ 440 416 418 + noop 200 horizontal 4 @@ -6805,6 +6826,7 @@ 440 417 419 + noop 200 horizontal 4 @@ -7146,6 +7168,7 @@ 440 418 423 + noop 200 horizontal 4 @@ -7487,6 +7510,7 @@ 440 419 423 + noop 200 horizontal 4 diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml index 3e21800ea..6cfca6581 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-pre_play.xml @@ -249,10 +249,12 @@ 466 0 - 1360 + 1226 60 left 0 + true + 5 horizontal true @@ -297,22 +299,6 @@ FFFFFFFF - - !String.IsEmpty(Window.Property(rating.stars)) - auto - 34 - font12 - left - FFFFFFFF - - - - !String.IsEmpty(Window.Property(rating.stars)) - 6 - 134 - 22 - script.plex/stars/$INFO[Window.Property(rating.stars)].png - !String.IsEmpty(Window.Property(video.res)) 10 @@ -346,16 +332,17 @@ - !String.IsEmpty(Window.Property(rating)) - 1560 + !String.IsEmpty(Window.Property(rating)) | !String.IsEmpty(Window.Property(rating2)) + 1426 4 - 300 + 434 32 right 15 horizontal true + !String.IsEmpty(Window.Property(rating)) 2 63 30 @@ -363,6 +350,7 @@ keep + !String.IsEmpty(Window.Property(rating)) auto 30 font12 @@ -375,7 +363,7 @@ 2 40 30 - $INFO[Window.Property(rating2.image)] + $INFO[Window.Property(rating2.image)] keep @@ -387,10 +375,17 @@ FFFFFFFF + + !String.IsEmpty(Window.Property(rating.stars)) + 6 + 134 + 22 + script.plex/stars/$INFO[Window.Property(rating.stars)].png + - !String.IsEmpty(Window.Property(directors)) + !String.IsEmpty(Window.Property(directors)) | !String.IsEmpty(Window.Property(writers)) 466 130 1360 @@ -398,10 +393,10 @@ font12 left 99FFFFFF - + - !String.IsEmpty(Window.Property(writers)) + !String.IsEmpty(Window.Property(cast)) 466 165 1360 @@ -409,7 +404,7 @@ font12 left 99FFFFFF - + 466 diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml index 812758e72..114dafa31 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml @@ -196,7 +196,7 @@ - !String.IsEmpty(Window.Property(rating)) + !String.IsEmpty(Window.Property(rating)) | !String.IsEmpty(Window.Property(rating2)) 1660 70 200 @@ -206,6 +206,7 @@ horizontal true + !String.IsEmpty(Window.Property(rating)) 2 63 30 @@ -213,6 +214,7 @@ keep + !String.IsEmpty(Window.Property(rating)) auto 30 font12 @@ -225,7 +227,7 @@ 2 63 30 - $INFO[Window.Property(rating2.image)] + $INFO[Window.Property(rating2.image)] keep @@ -436,6 +438,27 @@ + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 230 + + 0 + 0 + 158 + 6 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 158 + 4 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + false 0 @@ -517,6 +540,27 @@ + + !String.IsEmpty(ListItem.Property(progress)) + 0 + 230 + + 0 + 0 + 158 + 6 + script.plex/white-square.png + C0000000 + + + 0 + 1 + 158 + 4 + $INFO[ListItem.Property(progress)] + FFCC7B19 + + Control.HasFocus(400) 0 diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml index 3f22f9571..5a7d0bc2d 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml @@ -9,7 +9,7 @@ 800 - [!String.IsEmpty(Window.Property(show.OSD)) | Window.IsVisible(seekbar) | !String.IsEmpty(Window.Property(button.seek))] + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + [!String.IsEmpty(Window.Property(show.OSD)) | Window.IsVisible(seekbar) | !String.IsEmpty(Window.Property(button.seek))] + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + !Window.IsVisible(osdcmssettings) Hidden String.IsEmpty(Window.Property(settings.visible)) + [Window.IsVisible(seekbar) | Window.IsVisible(videoosd) | Player.ShowInfo] @@ -363,19 +363,10 @@ 893 50 bottom - + font14 black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Audio)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Subtitles)) + Player.HasVideo + [!String.IsEmpty(Window.Property(ppi.Audio)) | !String.IsEmpty(Window.Property(ppi.Subtitles))] 893 @@ -418,7 +409,7 @@ - !String.IsEmpty(Window.Property(show.OSD)) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + !String.IsEmpty(Window.Property(show.OSD)) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(playerprocessinfo) + !Window.IsActive(selectdialog) + !Window.IsVisible(osdcmssettings) Hidden !String.IsEmpty(Window.Property(has.bif)) + [Control.HasFocus(100) | Control.HasFocus(501) | !String.IsEmpty(Window.Property(button.seek))] diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml index 646ac7135..39d399a92 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-video_settings_dialog.xml @@ -22,7 +22,7 @@ - !Window.IsVisible(sliderdialog) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsVisible(sliderdialog) + !Window.IsVisible(osdvideosettings) + !Window.IsVisible(osdaudiosettings) + !Window.IsVisible(osdsubtitlesettings) + !Window.IsVisible(subtitlesearch) + !Window.IsActive(selectdialog) + !Window.IsVisible(osdcmssettings) 460 200