From 1ac77141150015c95134b223344f2d50bc0aee5f Mon Sep 17 00:00:00 2001 From: pannal Date: Sat, 20 Apr 2024 04:25:48 +0200 Subject: [PATCH] [script.plexmod] 0.7.7 --- script.plexmod/LICENSE.txt | 7 + script.plexmod/addon.xml | 10 +- script.plexmod/changelog.txt | 45 +- script.plexmod/fanart.jpg | Bin 71213 -> 0 bytes script.plexmod/icon.png | Bin 25724 -> 0 bytes .../lib/_included_packages/plexnet/audio.py | 4 +- .../lib/_included_packages/plexnet/http.py | 8 +- .../lib/_included_packages/plexnet/media.py | 3 + .../plexnet/mediadecisionengine.py | 3 +- .../plexnet/myplexaccount.py | 55 +- .../plexnet/myplexmanager.py | 9 + .../plexnet/nowplayingmanager.py | 81 +- .../lib/_included_packages/plexnet/photo.py | 2 +- .../_included_packages/plexnet/playqueue.py | 8 + .../lib/_included_packages/plexnet/plexapp.py | 7 +- .../plexnet/plexconnection.py | 4 +- .../_included_packages/plexnet/plexlibrary.py | 6 +- .../_included_packages/plexnet/plexpart.py | 44 + .../_included_packages/plexnet/plexplayer.py | 86 +- .../_included_packages/plexnet/plexserver.py | 16 + .../plexnet/plexservermanager.py | 11 + .../lib/_included_packages/plexnet/util.py | 49 +- .../lib/_included_packages/plexnet/video.py | 32 +- .../plexnet/videosession.py | 22 +- script.plexmod/lib/backgroundthread.py | 11 +- script.plexmod/lib/main.py | 8 +- script.plexmod/lib/player.py | 133 +- script.plexmod/lib/plex.py | 16 +- script.plexmod/lib/util.py | 289 ++--- script.plexmod/lib/windows/currentplaylist.py | 8 +- script.plexmod/lib/windows/episodes.py | 178 ++- script.plexmod/lib/windows/home.py | 166 ++- script.plexmod/lib/windows/info.py | 3 + script.plexmod/lib/windows/kodigui.py | 16 +- script.plexmod/lib/windows/library.py | 21 +- script.plexmod/lib/windows/musicplayer.py | 34 +- script.plexmod/lib/windows/opener.py | 2 +- script.plexmod/lib/windows/photos.py | 24 +- script.plexmod/lib/windows/playlist.py | 2 +- script.plexmod/lib/windows/preplay.py | 2 +- script.plexmod/lib/windows/seekdialog.py | 177 +-- script.plexmod/lib/windows/settings.py | 52 +- script.plexmod/lib/windows/slidehshow.py | 2 +- script.plexmod/lib/windows/subitems.py | 4 +- script.plexmod/lib/windows/tracks.py | 2 +- script.plexmod/lib/windows/userselect.py | 8 +- script.plexmod/lib/windows/videoplayer.py | 10 +- .../resource.language.de_de/strings.po | 104 +- .../resource.language.en_gb/strings.po | 102 +- script.plexmod/resources/settings.xml | 19 +- .../skins/Main/1080i/script-plex-album.xml | 12 +- .../skins/Main/1080i/script-plex-artist.xml | 12 +- .../Main/1080i/script-plex-background.xml | 4 +- .../skins/Main/1080i/script-plex-episodes.xml | 14 +- .../skins/Main/1080i/script-plex-home.xml | 12 +- .../skins/Main/1080i/script-plex-info.xml | 12 +- .../script-plex-listview-16x9-chunked.xml | 879 ------------- .../Main/1080i/script-plex-listview-16x9.xml | 12 +- .../script-plex-listview-square-chunked.xml | 943 -------------- .../1080i/script-plex-listview-square.xml | 12 +- .../Main/1080i/script-plex-options_dialog.xml | 2 + .../skins/Main/1080i/script-plex-playlist.xml | 12 +- .../Main/1080i/script-plex-playlists.xml | 12 +- .../Main/1080i/script-plex-posters-small.xml | 12 +- .../skins/Main/1080i/script-plex-posters.xml | 12 +- .../skins/Main/1080i/script-plex-pre_play.xml | 12 +- .../skins/Main/1080i/script-plex-seasons.xml | 12 +- .../Main/1080i/script-plex-seek_dialog.xml | 1101 ----------------- .../skins/Main/1080i/script-plex-settings.xml | 4 +- .../skins/Main/1080i/script-plex-squares.xml | 12 +- .../Main/1080i/script-plex-user_select.xml | 4 +- .../Main/1080i/script-plex-video_player.xml | 10 +- .../Main/media/script.plex/home/plex.png | Bin 1579 -> 2046 bytes .../skins/Main/media/script.plex/splash.png | Bin 9464 -> 15885 bytes .../media/script.plex/user_select/plex.png | Bin 5374 -> 4767 bytes 75 files changed, 1413 insertions(+), 3609 deletions(-) delete mode 100644 script.plexmod/fanart.jpg delete mode 100644 script.plexmod/icon.png delete mode 100644 script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml delete mode 100644 script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml delete mode 100644 script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml diff --git a/script.plexmod/LICENSE.txt b/script.plexmod/LICENSE.txt index 6bcffb461..ed5f05abe 100644 --- a/script.plexmod/LICENSE.txt +++ b/script.plexmod/LICENSE.txt @@ -490,3 +490,10 @@ Library. Fontawesome https://fontawesome.com/license/free CC BY 4.0 License: https://creativecommons.org/licenses/by/4.0/# + +------------------------------------------------------------------------- +Play queue by Sébastien Robaszkiewicz from Noun Project (CC BY 3.0) +fast forward by Adiyogi from Noun Project (CC BY 3.0) +subtitle by YANDI RS from Noun Project (CC BY 3.0) + +Other unlisted icons under CC BY 3.0, https://creativecommons.org/licenses/by/3.0/ \ No newline at end of file diff --git a/script.plexmod/addon.xml b/script.plexmod/addon.xml index 2e911137b..9222be922 100644 --- a/script.plexmod/addon.xml +++ b/script.plexmod/addon.xml @@ -1,7 +1,7 @@ @@ -33,11 +33,11 @@ https://github.com/pannal/plex-for-kodi all -- Based on 0.7.6-rev2 +- Based on 0.7.7-rev3 - icon.png - fanart.jpg + icon2.png + fanart.png - \ No newline at end of file + diff --git a/script.plexmod/changelog.txt b/script.plexmod/changelog.txt index 9f552aa80..d09735b45 100644 --- a/script.plexmod/changelog.txt +++ b/script.plexmod/changelog.txt @@ -1,6 +1,46 @@ -[-0.7.6-rev2 -] -- Core: Avoid DNS rebind protection issues for plex.direct +[-0.7.7-rev2-] +- UserSelect: When not switching user (startup), close the addon on cancel actions +- Libraries/Photos: Do autoplay when Play or Shuffle buttons are pressed in photo library view +- Libraries/Photos: Fix wonky thumbnail display on photodirectories; support dynamic backgrounds for Photo items +- SeekDialog: Fix final credits marker skipping wrongly on manual marker skip +- SeekDialog: Fix several marker issues (over-jumping, invalid markers) +- SeekDialog: Show stream transport type in video session info (smb, nfs, path mapped, http(s)) +- SeekDialog: Fix empty chapters list shown when watching an episode without markers via NEXT after watching one with markers +- SeekDialog/Settings: Add option to hide all time-related information from the user when the OSD isn't open +- SeekDialog: Transcode Session Only: Don't show subtitle download option in subtitle quick actions; properly show subtitles when toggling them via subtitle quick actions +- Core: Advanced/Addon settings: Set default Plex requests timeout to 10 (was 5) +- Core/Mainloop: Make sure we were able to open the Home/UserSelect window after our BACKGROUND successfully opened, even if another modal dialog opened in the meantime - Core: Firstrun: Refresh resources after signin and/or home user switch, otherwise the first time the addon's run the user sees no servers +- Core: UI: Fall back to black background image when we have backgrounds set but they didn't load +- Core: Finally fix and handle plex.direct mappings via advancedsettings.xml +- Core: Fix issues resulting in new devices being registered with plex.tv on every plugin start +- Core: Only use mapped file path if mapped file exists or verification is off +- Core: Backgrounds: More concise fallback handling +- Core/Player: Add optional DirectPlay path mapping via addon_data/script.plexmod/path_mapping.json (path_mapping.example.json included in addon directory). Allows for arbitrary replacements of HTTP playback with SMB, NFS, local mounts etc. +- Home: Rework and simplify section/library change logic +- Home: Never update hubs while playing a video to avoid hickups in high bitrate scenarios +- Home: Properly cache user thumbnail (by removing the ?c timestamp from the URL), increasing performance +- Home: Fix round robining on hubs (going left once before the last item, then right falsely round-robined to the start, early) +- Home: Add virtual hub 'home.VIRTUAL.movies.recentlyreleased' on index 3 if we encounter 'movie.recentlyreleased' on the home hubs +- Home/Settings: Add setting to use the modern Continue Watching hub on Home instead of the separated In Progress/On Deck hubs +- Episodes: Complete rework of the watch-state handler for TV; instantly update progress while watching +- Theme: Update assets, icon, splash +- Music: Rework music player and handler; remove plugin:// path and handle tracks directly; fixed all stability issues; massively improve performance +- Player: Add button theme support, use new modern colored theme; support custom themes +- Player: Improve handling when postplay screen is not wanted and we're at the end of a show; harden progressEvent handler +- Player: Remove all /file.xxx instances instead of just .mkv and .mp4 from non mapped stream URLs +- Musicplayer: Partially fix "dangling" playing tracks in Plex Dashboard after stopping playback +- Music: Hide spinner on prev/next button clicks as well (only with Plextuary skin 4.0.0-pm4k0.9 (omega), 3.0.10-pm4k0.8 (nexus and older)) +- Settings: Add setting to toggle path mapping dynamically +- Settings: Adjust cache recommendations down from 100MB to 50MB +- AddonSettings: Add advanced setting to verify mapped files before playing them (default: on) +- Account/HomeUsers: Use new API to determine the PlexPass subscription status of the Plex Home +- Account/HomeUsers: Refresh home users once a week if we've never seen a plex home +- Home/RefreshUsers/UserSelect: Refresh subscription state + + +[-0.7.6-] +- Core: Avoid DNS rebind protection issues for plex.direct - Core: Support ipv6 plex.direct hosts when checking for locality/LAN - Core: Network: Massively speed up local connection checks - Core: Network: Skip local connection checks for plex.tv @@ -25,7 +65,6 @@ - SeekDialog: Autoscroll episode/movie title lines for too long titles - SeekDialog: Hide non-autoskipping marker into the OSD using NAV_BACK/PREVIOUS_MENU - SeekDialog: Apply positive marker endtime offset to manually skipping markers as well (unifying with the autoskip handling), to avoid re-showing the marker occasionally after seeking -- SeekDialog: Fix final credits marker skipping wrongly on manual marker skip - Library: Show current total item count in title - Settings: Add description for Direct Stream diff --git a/script.plexmod/fanart.jpg b/script.plexmod/fanart.jpg deleted file mode 100644 index 7548349f6a1dd3633c2dc7262c386ec461f361cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71213 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 diff --git a/script.plexmod/icon.png b/script.plexmod/icon.png deleted file mode 100644 index 1a6ba14690a461e957c9b27a7b50b959928feb12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25724 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 3 - if host in KNOWN_HOSTS: - ip = KNOWN_HOSTS[host] + if host in RESOLVED_PD_HOSTS: + ip = RESOLVED_PD_HOSTS[host] else: base = host.split(".", 1)[0] - ip = KNOWN_HOSTS[host] = v6 and base.replace("-", ":") or base.replace("-", ".") + ip = RESOLVED_PD_HOSTS[host] = v6 and base.replace("-", ":") or base.replace("-", ".") util.DEBUG_LOG("Dynamically resolving {} to {}".format(host, ip)) fam = v6 and socket.AF_INET6 or socket.AF_INET diff --git a/script.plexmod/lib/_included_packages/plexnet/media.py b/script.plexmod/lib/_included_packages/plexnet/media.py index 802da02c9..28b0bc96f 100644 --- a/script.plexmod/lib/_included_packages/plexnet/media.py +++ b/script.plexmod/lib/_included_packages/plexnet/media.py @@ -270,6 +270,9 @@ class Marker(MediaTag): TYPE = 'Marker' FILTER = 'Marker' + def __repr__(self): + return '<%s:%s:%s:%s>' % (self.__class__.__name__, self.id, self.type, self.final and "final" or "") + class Review(MediaTag): TYPE = 'Review' diff --git a/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py b/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py index 9dea3e086..c54c7ac02 100644 --- a/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py +++ b/script.plexmod/lib/_included_packages/plexnet/mediadecisionengine.py @@ -24,7 +24,8 @@ def __init__(self): def chooseMedia(self, item, forceUpdate=False): # If we've already evaluated this item, use our previous choice. - if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and not item.mediaChoice.media.isIndirect(): + if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and \ + not item.mediaChoice.media.isIndirect(): return item.mediaChoice # See if we're missing media/stream details for this item. diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py b/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py index d9add92d7..1d6ef2e33 100644 --- a/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py +++ b/script.plexmod/lib/_included_packages/plexnet/myplexaccount.py @@ -16,7 +16,9 @@ class HomeUser(util.AttributeDict): - pass + def __repr__(self): + return '<{0}:{1}:{2} (admin: {3})>'.format(self.__class__.__name__, self.id, + self.get('title', 'None').encode('utf8'), self.get('admin', 0)) class MyPlexAccount(object): @@ -48,6 +50,7 @@ def __init__(self): self.adminHasPlexPass = False self.lastHomeUserUpdate = None + self.revalidatePlexPass = False self.homeUsers = [] def init(self): @@ -128,6 +131,26 @@ def logState(self): util.LOG("Admin: {0}".format(self.isAdmin)) util.LOG("AdminPlexPass: {0}".format(self.adminHasPlexPass)) + def getHomeSubscription(self): + """ + This gets the state of the plex home subscription, which is easier to determine than using a combination of + isAdmin and adminHasPlexPass, especially when caching home users. + """ + try: + req = myplexrequest.MyPlexRequest("/api/v2/home") + xml = req.getToStringWithTimeout(seconds=util.LONG_TIMEOUT) + data = ElementTree.fromstring(xml) + return data.attrib.get('subscription') == '1' + except: + util.LOG("Couldn't get Plex Home info") + return + return False + + def refreshSubscription(self): + ret = self.getHomeSubscription() + if isinstance(ret, bool): + self.isPlexPass = ret + def onAccountResponse(self, request, response, context): oldId = self.ID @@ -141,9 +164,11 @@ def onAccountResponse(self, request, response, context): self.title = data.attrib.get('title') self.username = data.attrib.get('username') self.email = data.attrib.get('email') - self.thumb = data.attrib.get('thumb') + self.thumb = data.attrib.get('thumb').split("?")[0] self.authToken = data.attrib.get('authenticationToken') - self.isPlexPass = (data.find('subscription') is not None and data.find('subscription').attrib.get('active') == '1') + self.isPlexPass = self.isPlexPass or \ + (data.find('subscription') is not None and + data.find('subscription').attrib.get('active') == '1') self.isManaged = data.attrib.get('restricted') == '1' self.isSecure = data.attrib.get('secure') == '1' self.hasQueue = bool(data.attrib.get('queueEmail')) @@ -159,12 +184,20 @@ def onAccountResponse(self, request, response, context): # Cache home users forever epoch = time.time() - if self.lastHomeUserUpdate: + # never automatically update home users if we have some. + # if we've never seen any, check once a week + if (self.lastHomeUserUpdate and self.homeUsers) or \ + (self.lastHomeUserUpdate and not self.homeUsers and epoch - self.lastHomeUserUpdate < 604800): util.DEBUG_LOG( "Skipping home user update (updated {0} seconds ago)".format(epoch - self.lastHomeUserUpdate)) else: self.updateHomeUsers(use_async=bool(self.homeUsers)) + # revalidate plex home subscription state after switching home user + if self.revalidatePlexPass and self.homeUsers: + self.refreshSubscription() + self.revalidatePlexPass = False + if self.isAdmin and self.isPlexPass: self.adminHasPlexPass = True @@ -221,8 +254,8 @@ def signOut(self, expired=False): # Booleans self.isSignedIn = False - self.isPlexPass = False - self.adminHasPlexPass = False + #self.isPlexPass = False + #self.adminHasPlexPass = False self.isManaged = False self.isSecure = False self.isExpired = expired @@ -260,7 +293,7 @@ def refreshAccount(self): return self.validateToken(self.authToken, False) - def updateHomeUsers(self, use_async=False): + def updateHomeUsers(self, use_async=False, refreshSubscription=False): # Ignore request and clear any home users we are not signed in if not self.isSignedIn: self.homeUsers = [] @@ -280,6 +313,11 @@ def updateHomeUsers(self, use_async=False): else: self.onHomeUsersUpdateResponse(req, None, None) + if refreshSubscription: + self.refreshSubscription() + self.logState() + self.saveState() + def onHomeUsersUpdateResponse(self, request, response, context): """ this can either be called with a given request, which will lead to a synchronous request, or as a @@ -331,7 +369,7 @@ def switchHomeUser(self, userId, pin=''): self.validateToken(self.authToken, True) return True else: - # build path and post to myplex to swith the user + # build path and post to myplex to switch the user path = '/api/home/users/{0}/switch'.format(userId) req = myplexrequest.MyPlexRequest(path) xml = req.postToStringWithTimeout({'pin': pin}, seconds=util.LONG_TIMEOUT) @@ -344,6 +382,7 @@ def switchHomeUser(self, userId, pin=''): self.isAuthenticated = True # validate the token (trigger change:user) on user change or channel startup if userId != self.ID or not locks.LOCKS.isLocked("idleLock"): + self.revalidatePlexPass = True self.validateToken(data.attrib.get('authenticationToken'), True, force_resource_refresh=plexapp.SERVERMANAGER.reachabilityNeverTested) return True diff --git a/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py b/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py index 72c2ebb9c..7856db5f4 100644 --- a/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py +++ b/script.plexmod/lib/_included_packages/plexnet/myplexmanager.py @@ -11,6 +11,11 @@ class MyPlexManager(object): + gotResources = False + + def __init__(self): + self.gotResources = False + def publish(self): util.LOG('MyPlexManager().publish() - NOT IMPLEMENTED') return # TODO: ----------------------------------------------------------------------------------------------------------------------------- IMPLEMENT? @@ -53,6 +58,7 @@ def onResourcesResponse(self, request, response, context): response.parseFakeXMLResponse(data) util.DEBUG_LOG("Using cached resources") + hosts = [] if response.container: for resource in response.container: util.DEBUG_LOG( @@ -67,13 +73,16 @@ def onResourcesResponse(self, request, response, context): for conn in resource.connections: util.DEBUG_LOG(' {0}'.format(conn)) + hosts.append(conn.address) if 'server' in resource.provides: server = plexserver.createPlexServerForResource(resource) util.DEBUG_LOG(' {0}'.format(server)) servers.append(server) + self.gotResources = True plexapp.SERVERMANAGER.updateFromConnectionType(servers, plexconnection.PlexConnection.SOURCE_MYPLEX) + util.APP.trigger("loaded:myplex_servers", hosts=hosts, source="myplex") MANAGER = MyPlexManager() diff --git a/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py index 454516be7..9ef4b288e 100644 --- a/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py +++ b/script.plexmod/lib/_included_packages/plexnet/nowplayingmanager.py @@ -25,8 +25,7 @@ def __init__(self, timelineType, *args, **kwargs): util.AttributeDict.__init__(self, *args, **kwargs) self.type = timelineType self.state = "stopped" - self.item = None - self.choice = None + self.itemData = None self.playQueue = None self.controllable = util.AttributeDict() @@ -61,51 +60,6 @@ def updateControllableStr(self): prependComma = True self.controllableStr += name - def toXmlAttributes(self, elem): - self.updateControllableStr() - elem.attrib["type"] = self.type - elem.attrib["start"] = self.state - elem.attrib["controllable"] = self.controllableStr - - if self.item: - if self.item.duration: - elem.attrib['duration'] = self.item.duration - if self.item.ratingKey: - elem.attrib['ratingKey'] = self.item.ratingKey - if self.item.key: - elem.attrib['key'] = self.item.key - if self.item.container.address: - elem.attrib['containerKey'] = self.item.container.address - - # Send the audio, video and subtitle choice if it's available - if self.choice: - for stream in ("audioStream", "videoStream", "subtitleStream"): - if self.choice.get(stream) and self.choice[stream].id: - elem.attrib[stream + "ID"] = self.choice[stream].id - - server = self.item.getServer() - if server: - elem.attrib["machineIdentifier"] = server.uuid - - if server.activeConnection: - parts = six.moves.urllib.parse.uslparse(server.activeConnection.address) - elem.attrib["protocol"] = parts.scheme - elem.attrib["address"] = parts.netloc.split(':', 1)[0] - if ':' in parts.netloc: - elem.attrib["port"] = parts.netloc.split(':', 1)[-1] - elif parts.scheme == 'https': - elem.attrib["port"] = '443' - else: - elem.attrib["port"] = '80' - - if self.playQueue: - elem.attrib["playQueueID"] = str(self.playQueue.id) - elem.attrib["playQueueItemID"] = str(self.playQueue.selectedId) - elem.attrib["playQueueVersion"] = str(self.playQueue.version) - - for key, val in self.attrs.items(): - elem.attrib[key] = val - class NowPlayingManager(object): def __init__(self): @@ -131,11 +85,10 @@ def __init__(self): for timelineType in self.TIMELINE_TYPES: self.timelines[timelineType] = TimelineData(timelineType) - def updatePlaybackState(self, timelineType, playerObject, state, t, playQueue=None, duration=0, force=False): + def updatePlaybackState(self, timelineType, itemData, state, t, playQueue=None, duration=0, force=False): timeline = self.timelines[timelineType] timeline.state = state - timeline.item = playerObject.item - timeline.choice = playerObject.choice + timeline.itemData = itemData timeline.playQueue = playQueue timeline.attrs["time"] = str(t) timeline.duration = duration @@ -145,29 +98,25 @@ def updatePlaybackState(self, timelineType, playerObject, state, t, playQueue=No self.sendTimelineToServer(timelineType, timeline, t, force=force) def sendTimelineToServer(self, timelineType, timeline, t, force=False): - if not hasattr(timeline.item, 'getServer') or not timeline.item.getServer(): + server = util.APP.serverManager.selectedServer + if not server: return serverTimeline = self.getServerTimeline(timelineType) # 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 + itemsEqual = timeline.itemData and serverTimeline.itemData \ + and timeline.itemData.ratingKey == serverTimeline.itemData.ratingKey if itemsEqual and timeline.state == serverTimeline.state and not serverTimeline.isExpired() and not force: return serverTimeline.reset() - serverTimeline.item = timeline.item + serverTimeline.itemData = timeline.itemData serverTimeline.state = timeline.state - # Ignore sending timelines for multi part media with no duration - obj = timeline.choice - if obj and obj.part and obj.part.duration.asInt() == 0 and obj.media.parts and len(obj.media.parts) > 1: - util.WARN_LOG("Timeline not supported: the current part doesn't have a valid duration") - return - # 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 + duration = timeline.itemData.duration or timeline.duration if t > duration: t = duration @@ -175,11 +124,11 @@ def sendTimelineToServer(self, timelineType, timeline, t, force=False): params["time"] = t params["duration"] = duration params["state"] = timeline.state - params["guid"] = timeline.item.guid - params["ratingKey"] = timeline.item.ratingKey - params["url"] = timeline.item.url - params["key"] = timeline.item.key - params["containerKey"] = timeline.item.container.address + params["guid"] = timeline.itemData.guid + params["ratingKey"] = timeline.itemData.ratingKey + params["url"] = timeline.itemData.url + params["key"] = timeline.itemData.key + params["containerKey"] = timeline.itemData.containerKey if timeline.playQueue: params["playQueueItemID"] = timeline.playQueue.selectedId @@ -188,7 +137,7 @@ def sendTimelineToServer(self, timelineType, timeline, t, force=False): if params[paramKey]: path = http.addUrlParam(path, paramKey + "=" + six.moves.urllib.parse.quote(str(params[paramKey]))) - request = plexrequest.PlexRequest(timeline.item.getServer(), path) + request = plexrequest.PlexRequest(server, path) context = request.createRequestContext("timelineUpdate", callback.Callable(self.onTimelineResponse)) context.playQueue = timeline.playQueue diff --git a/script.plexmod/lib/_included_packages/plexnet/photo.py b/script.plexmod/lib/_included_packages/plexnet/photo.py index d3a66ca68..b611a0238 100644 --- a/script.plexmod/lib/_included_packages/plexnet/photo.py +++ b/script.plexmod/lib/_included_packages/plexnet/photo.py @@ -46,7 +46,7 @@ def isPhotoOrDirectoryItem(self): class PhotoDirectory(media.MediaItem): TYPE = 'photodirectory' - def all(self): + def all(self, *args, **kwargs): path = self.key return plexobjects.listItems(self.server, path) diff --git a/script.plexmod/lib/_included_packages/plexnet/playqueue.py b/script.plexmod/lib/_included_packages/plexnet/playqueue.py index 63a9ba228..88a8b9b62 100644 --- a/script.plexmod/lib/_included_packages/plexnet/playqueue.py +++ b/script.plexmod/lib/_included_packages/plexnet/playqueue.py @@ -68,10 +68,18 @@ def createUsage(cls, playQueue): if obj.type: if obj.type == "audio": return obj.createAudioUsage() + elif obj.type == "photo": + return obj.createPhotoUsage() util.DEBUG_LOG("Don't know how to usage for " + str(obj.type)) return None + def createPhotoUsage(self): + if not self.usage or self.usage.playQueueId != self.playQueue.id: + self.usage = self.playQueue.usage + + return self.usage + def createAudioUsage(self): skips = self.playQueue.container.stationSkipsPerHour.asInt(-1) if skips == -1: diff --git a/script.plexmod/lib/_included_packages/plexnet/plexapp.py b/script.plexmod/lib/_included_packages/plexnet/plexapp.py index 931799be4..466230dcb 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexapp.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexapp.py @@ -1,10 +1,7 @@ from __future__ import print_function, absolute_import -import threading import platform import uuid -import sys -from . import callback from . import signalsmixin from . import simpleobjects from . import util @@ -44,6 +41,10 @@ def __init__(self): def addTimer(self, timer): self.timers.append(timer) + @property + def serverManager(self): + return SERVERMANAGER + def startRequest(self, request, context, body=None, contentType=None): context.request = request diff --git a/script.plexmod/lib/_included_packages/plexnet/plexconnection.py b/script.plexmod/lib/_included_packages/plexnet/plexconnection.py index c9e9c4166..dfdf7304b 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexconnection.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexconnection.py @@ -128,9 +128,7 @@ def checkLocal(self): if hostname.endswith("plex.direct"): util.DEBUG_LOG("Using shortcut for hostname IP detection due to plex.direct host: {}".format(hostname)) - v6 = hostname.count("-") > 3 - base = hostname.split(".", 1)[0] - ips = [v6 and base.replace("-", ":") or base.replace("-", ".")] + ips = [util.parsePlexDirectHost(hostname)] else: try: diff --git a/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py index c9d0c4fc4..df1edbe2c 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexlibrary.py @@ -34,7 +34,7 @@ def section(self, title=None): return item raise exceptions.NotFound('Invalid library section: %s' % title) - def all(self): + def all(self, *args, **kwargs): return plexobjects.listItems(self.server, '/library/all') def onDeck(self): @@ -524,9 +524,11 @@ def reset(self): (self.items[0].container.offset.asInt() + self.items[0].container.size.asInt() < totalSize) and '1' or '' ) - def getCleanHubIdentifier(self): + def getCleanHubIdentifier(self, is_home=False): if not self._identifier: self._identifier = re.sub(r'\.\d+$', '', re.sub(r'\.\d+$', '', self.hubIdentifier)) + if is_home and self._identifier == 'movie.recentlyreleased': + self._identifier = 'home.VIRTUAL.movies.recentlyreleased' return self._identifier diff --git a/script.plexmod/lib/_included_packages/plexnet/plexpart.py b/script.plexmod/lib/_included_packages/plexnet/plexpart.py index 7fd0c138c..86cc5ddd9 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexpart.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexpart.py @@ -1,9 +1,12 @@ from __future__ import absolute_import +from kodi_six import xbmcvfs from . import plexobjects from . import plexstream from . import plexrequest from . import util +from lib.util import PATH_MAP, addonSettings + class PlexPart(plexobjects.PlexObject): def reload(self): @@ -157,6 +160,47 @@ def getIndexPath(self, indexKey, interval=None): def hasStreams(self): return bool(self.streams) + def getPathMappedUrl(self, return_only_folder=False): + verify = addonSettings.verifyMappedFiles + if PATH_MAP and util.INTERFACE.getPreference("path_mapping", True): + match = ("", "") + + for map_path, pms_path in PATH_MAP.get(self.getServer().name, {}).items(): + # the longest matching path wins + if self.file.startswith(pms_path) and len(pms_path) > len(match[1]): + match = (map_path, pms_path) + + if all(match): + map_path, pms_path = match + if return_only_folder: + return map_path + + sep = "\\" in map_path and "\\" or "/" + + # replace match and normalize path separator to separator style of map_path + url = self.file.replace(pms_path, map_path, 1).replace(sep == "/" and "\\" or "/", sep) + + if (verify and xbmcvfs.exists(url)) or not verify: + util.DEBUG_LOG("File {} found in path map, mapping to {}".format(self.file, pms_path)) + return url + util.LOG("Mapped file {} doesn't exist".format(url)) + return "" + + @property + def isPathMapped(self): + return bool(self.getPathMappedUrl()) + + def getPathMappedProto(self): + url = self.getPathMappedUrl() + if url: + prot = url.split("://")[0] + if prot == url: + ret = "mnt://" + else: + ret = "{}://".format(prot) + return ret + return "" + def __str__(self): return "PlexPart {0} {1}".format(self.id("NaN"), self.key) diff --git a/script.plexmod/lib/_included_packages/plexnet/plexplayer.py b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py index 4c3b0c1c1..cfb8e84fb 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexplayer.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexplayer.py @@ -6,14 +6,34 @@ from . import plexrequest from . import mediadecisionengine from . import serverdecision -from lib.util import CACHE_SIZE, advancedSettings, KODI_VERSION_MAJOR +from lib.util import addonSettings, KODI_VERSION_MAJOR +from lib.cache import CACHE_SIZE from six.moves import range DecisionFailure = serverdecision.DecisionFailure -class PlexPlayer(object): +class BasePlayer(object): + item = None + + def setupObj(self, obj, part, server, force_request_to_server=False): + # check for path mapping + url = part.getPathMappedUrl() + + if not url: + url = server.buildUrl(part.getAbsolutePath("key")) + # Check if we should include our token or not for this request + obj.isRequestToServer = force_request_to_server or server.isRequestToServer(url) + obj.streamUrls = [server.buildUrl(part.getAbsolutePath("key"), obj.isRequestToServer)] + obj.isMapped = False + else: + obj.isRequestToServer = False + obj.streamUrls = [url] + obj.isMapped = True + + +class PlexPlayer(BasePlayer): DECISION_ENDPOINT = "/video/:/transcode/universal/decision" def __init__(self, item, seekValue=0, forceUpdate=False): @@ -271,7 +291,7 @@ def getDecisionPath(self, directPlay=False): "mediaBufferSize={}".format(str(CACHE_SIZE * 1024))) decisionPath = http.addUrlParam(decisionPath, "hasMDE=1") - if not advancedSettings.oldprofile: + if not addonSettings.oldprofile: decisionPath = http.addUrlParam(decisionPath, 'X-Plex-Client-Profile-Name=Generic') else: decisionPath = http.addUrlParam(decisionPath, 'X-Plex-Client-Profile-Name=Chrome') @@ -303,7 +323,7 @@ def buildTranscodeHls(self, obj): builder.addParam("protocol", "hls") # TODO: This should be Generic, but will need to re-evaluate the augmentations with that change - if not advancedSettings.oldprofile: + if not addonSettings.oldprofile: builder.addParam("X-Plex-Client-Profile-Name", "Generic") else: builder.addParam("X-Plex-Client-Profile-Name", "Chrome") @@ -318,7 +338,7 @@ def buildTranscodeHls(self, obj): # Augment the server's profile for things that depend on the Roku's configuration. if self.item.settings.supportsAudioStream("ac3", 6): builder.extras.append("append-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=hls&audioCodec=ac3)") - if not advancedSettings.oldprofile: + if not addonSettings.oldprofile: builder.extras.append("add-direct-play-profile(type=videoProfile&container=mkv&videoCodec=*&audioCodec=ac3)") else: builder.extras.append( @@ -336,7 +356,7 @@ def buildTranscodeMkv(self, obj, directStream=True): builder.extras = [] builder.addParam("protocol", "http") builder.addParam("copyts", "1") - if not advancedSettings.oldprofile: + if not addonSettings.oldprofile: builder.addParam("X-Plex-Client-Profile-Name", "Generic") else: builder.addParam("X-Plex-Client-Profile-Name", "Chrome") @@ -684,10 +704,9 @@ def buildDirectPlay(self, obj, partIndex): server = self.item.getServer() - # Check if we should include our token or not for this request - obj.isRequestToServer = server.isRequestToServer(server.buildUrl(part.getAbsolutePath("key"))) - obj.streamUrls = [server.buildUrl(part.getAbsolutePath("key"), obj.isRequestToServer)] + self.setupObj(obj, part, server) obj.token = obj.isRequestToServer and server.getToken() or None + if self.media.protocol == "hls": obj.streamFormat = "hls" obj.switchingStrategy = "full-adaptation" @@ -748,7 +767,7 @@ def buildTranscode(self, server, obj, partIndex, directStream, isCurrentPart): # if server.supportsFeature("mkvTranscode") and self.item.settings.getPreference("transcode_format", 'mkv') != "hls": if server.supportsFeature("mkvTranscode"): - if not advancedSettings.oldprofile: + if not addonSettings.oldprofile: builder = self.buildTranscodeMkv(obj, directStream=directStream) else: builder = self.buildTranscodeMkvLegacy(obj, directStream=directStream) @@ -840,38 +859,34 @@ def buildTranscode(self, server, obj, partIndex, directStream, isCurrentPart): return obj -class PlexAudioPlayer(object): - def __init__(self, item): +class PlexAudioPlayer(BasePlayer): + def __init__(self, item=None): + self.item = item + self.choice = None self.containerFormats = { 'aac': "es.aac-adts" } - self.item = item - self.choice = mediadecisionengine.MediaDecisionEngine().chooseMedia(item) - if self.choice: - self.media = self.choice.media self.lyrics = None # createLyrics(item, self.media) - def build(self, directPlay=None): - directPlay = directPlay or self.choice.isDirectPlayable + def build(self, item, directPlay=None): + item = item or self.item + self.choice = choice = mediadecisionengine.MediaDecisionEngine().chooseMedia(item) + directPlay = directPlay or choice.isDirectPlayable obj = util.AttributeDict() - # TODO(schuyler): Do we want/need to add anything generic here? Title? Duration? - if directPlay: - obj = self.buildDirectPlay(obj) + obj = self.buildDirectPlay(item, choice, obj) else: - obj = self.buildTranscode(obj) - - self.metadata = obj + obj = self.buildTranscode(item, choice, obj) util.LOG("Constructed audio item for playback: {0}".format(util.cleanObjTokens(dict(obj)))) - return self.metadata + return obj - def buildTranscode(self, obj): - transcodeServer = self.item.getTranscodeServer(True, "audio") + def buildTranscode(self, item, choice, obj): + transcodeServer = item.getTranscodeServer(True, "audio") if not transcodeServer: return None @@ -882,8 +897,8 @@ def buildTranscode(self, obj): builder = http.HttpRequest(transcodeServer.buildUrl(obj.transcodeEndpoint, True)) # builder.addParam("protocol", "http") - builder.addParam("path", self.item.getAbsolutePath("key")) - builder.addParam("session", self.item.getGlobal("clientIdentifier")) + builder.addParam("path", item.getAbsolutePath("key")) + builder.addParam("session", item.getGlobal("clientIdentifier")) builder.addParam("directPlay", "0") builder.addParam("directStream", "0") @@ -891,26 +906,27 @@ def buildTranscode(self, obj): return obj - def buildDirectPlay(self, obj): - if self.choice.part: - obj.url = self.item.getServer().buildUrl(self.choice.part.getAbsolutePath("key"), True) + def buildDirectPlay(self, item, choice, obj): + if choice.part: + self.setupObj(obj, choice.part, item.getServer(), force_request_to_server=True) + obj.url = obj.streamUrls[0] # Set and override the stream format if applicable - obj.streamFormat = self.choice.media.get('container', 'mp3') + obj.streamFormat = choice.media.get('container', 'mp3') if self.containerFormats.get(obj.streamFormat): obj.streamFormat = self.containerFormats[obj.streamFormat] # If we're direct playing a FLAC, bitrate can be required, and supposedly # this is the only way to do it. plexinc/roku-client#48 # - bitrate = self.choice.media.bitrate.asInt() + bitrate = choice.media.bitrate.asInt() if bitrate > 0: obj.streams = [{'url': obj.url, 'bitrate': bitrate}] return obj # We may as well fallback to transcoding if we could not direct play - return self.buildTranscode(obj) + return self.buildTranscode(item, choice, obj) def getLyrics(self): return self.lyrics diff --git a/script.plexmod/lib/_included_packages/plexnet/plexserver.py b/script.plexmod/lib/_included_packages/plexnet/plexserver.py index 7857260a8..4c073a073 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexserver.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexserver.py @@ -150,7 +150,23 @@ def hubs(self, section=None, count=None, search_query=None): data = self.query(q, params=params) container = plexobjects.PlexContainer(data, initpath=q, server=self, address=q) + newCW = util.INTERFACE.getPreference('hubs_use_new_continue_watching', False) and not search_query \ + and not section + + if newCW: + # home, add continueWatching + cq = '/hubs/continueWatching' + cdata = self.query(cq, params=params) + ccontainer = plexobjects.PlexContainer(cdata, initpath=cq, server=self, address=cq) + hubs.append(plexlibrary.Hub(cdata[0], server=self, container=ccontainer)) + for elem in data: + hubIdent = elem.attrib.get('hubIdentifier') + # if we've added continueWatching, which combines continue and ondeck, skip those two hubs + if newCW and hubIdent and \ + (hubIdent.startswith('home.continue') or hubIdent.startswith('home.ondeck')): + continue + hubs.append(plexlibrary.Hub(elem, server=self, container=container)) return hubs diff --git a/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py b/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py index d6e968b36..d8782b8d0 100644 --- a/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py +++ b/script.plexmod/lib/_included_packages/plexnet/plexservermanager.py @@ -42,6 +42,10 @@ def __init__(self): def getSelectedServer(self): return self.selectedServer + @property + def allConnections(self): + return [c.address for s in list(self.serversByUuid.values()) for c in s.connections if s.connections] + def setSelectedServer(self, server, force=False): # Don't do anything if the server is already selected. if self.selectedServer and self.selectedServer == server: @@ -329,6 +333,7 @@ def loadState(self): util.ERROR_LOG("Failed to parse PlexServerManager JSON") return + hosts = [] for serverObj in obj['servers']: server = plexserver.createPlexServerForName(serverObj['uuid'], serverObj['name']) server.owned = bool(serverObj.get('owned')) @@ -343,6 +348,7 @@ def loadState(self): for i in range(len(serverObj.get('connections', []))): conn = serverObj['connections'][i] + hosts.append(conn['address']) isFallback = hasSecureConn and conn['address'][:5] != "https" and not util.LOCAL_OVER_SECURE sources = plexconnection.PlexConnection.SOURCE_BY_VAL[conn['sources']] connection = plexconnection.PlexConnection(sources, conn['address'], conn['isLocal'], conn['token'], isFallback) @@ -358,6 +364,7 @@ def loadState(self): self.serversByUuid[server.uuid] = server util.LOG("Loaded {0} servers from registry".format(len(obj['servers']))) + util.APP.trigger("loaded:server_connections", hosts=hosts, source="stored") self.updateReachability(False, True) def saveState(self, setPreferred=False): @@ -370,6 +377,8 @@ def saveState(self, setPreferred=False): servers = self.getServers() obj['servers'] = [] + hosts = [] + for server in servers: # Don't save secondary servers. They should be discovered through GDM or myPlex. if not server.isSecondary(): @@ -383,6 +392,7 @@ def saveState(self, setPreferred=False): for i in range(len(server.connections)): conn = server.connections[i] + hosts.append(conn.address) serverObj['connections'].append({ 'sources': conn.sources, 'address': conn.address, @@ -397,6 +407,7 @@ def saveState(self, setPreferred=False): and setPreferred: util.INTERFACE.setPreference("lastServerId.{}".format(plexapp.ACCOUNT.ID), self.selectedServer.uuid) + util.APP.trigger("loaded:server_connections", hosts=hosts, source="myplex") util.INTERFACE.setRegistry("PlexServerManager", json.dumps(obj)) def clearState(self): diff --git a/script.plexmod/lib/_included_packages/plexnet/util.py b/script.plexmod/lib/_included_packages/plexnet/util.py index d29232fb1..a796c59c6 100644 --- a/script.plexmod/lib/_included_packages/plexnet/util.py +++ b/script.plexmod/lib/_included_packages/plexnet/util.py @@ -78,7 +78,10 @@ def resetBaseHeaders(): _platform = sys.platform X_PLEX_DEVICE = _platform # Device name and model number, eg iPhone3,2, Motorola XOOM, LG5200TV -X_PLEX_IDENTIFIER = str(hex(uuid.getnode())) # UUID, serial number, or other number unique per device +X_PLEX_IDENTIFIER = ADDON.getSetting('client.ID') +if not X_PLEX_IDENTIFIER: + X_PLEX_IDENTIFIER = str(uuid.uuid4()) + ADDON.setSetting('client.ID', X_PLEX_IDENTIFIER) BASE_HEADERS = resetBaseHeaders() @@ -159,21 +162,21 @@ def cleanToken(url): return re.sub(r'X-Plex-Token=[^&]+', 'X-Plex-Token=****', url) -def cleanObjTokens(dorig, flistkeys=("streamUrls",), fstrkeys=("url", "token")): +def cleanObjTokens(dorig, flistkeys=("streamUrls", "streams",), fstrkeys=("url", "token")): d = {} dcopy = copy(dorig) # filter lists for k in flistkeys: - if k not in d: + if k not in dcopy: continue - d[k] = list(map(lambda x: cleanToken(x), d[k][:])) + d[k] = list(map(lambda x: cleanObjTokens(x) if isinstance(x, dict) else cleanToken(x), dcopy[k][:])) # filter strings for k in fstrkeys: - if k not in d: + if k not in dcopy: continue - d[k] = "****" if k == "token" else cleanToken(d[k]) + d[k] = "****" if k == "token" else cleanToken(dcopy[k]) dcopy.update(d) return dcopy @@ -198,17 +201,21 @@ def joinArgs(args, includeQuestion=True): return '{0}{1}'.format(includeQuestion and '?' or '&', '&'.join(arglist)) +def getPlexHeaders(): + return {"X-Plex-Platform": INTERFACE.getGlobal("platform"), + "X-Plex-Version": INTERFACE.getGlobal("appVersionStr"), + "X-Plex-Client-Identifier": INTERFACE.getGlobal("clientIdentifier"), + "X-Plex-Platform-Version": INTERFACE.getGlobal("platformVersion", "unknown"), + "X-Plex-Product": INTERFACE.getGlobal("product"), + "X-Plex-Provides": not INTERFACE.getPreference("remotecontrol", False) and 'player' or '', + "X-Plex-Device": INTERFACE.getGlobal("device"), + "X-Plex-Model": INTERFACE.getGlobal("model"), + "X-Plex-Device-Name": INTERFACE.getGlobal("friendlyName"), + } + + def addPlexHeaders(transferObj, token=None): - headers = {"X-Plex-Platform": INTERFACE.getGlobal("platform"), - "X-Plex-Version": INTERFACE.getGlobal("appVersionStr"), - "X-Plex-Client-Identifier": INTERFACE.getGlobal("clientIdentifier"), - "X-Plex-Platform-Version": INTERFACE.getGlobal("platformVersion", "unknown"), - "X-Plex-Product": INTERFACE.getGlobal("product"), - "X-Plex-Provides": not INTERFACE.getPreference("remotecontrol", False) and 'player' or '', - "X-Plex-Device": INTERFACE.getGlobal("device"), - "X-Plex-Model": INTERFACE.getGlobal("model"), - "X-Plex-Device-Name": INTERFACE.getGlobal("friendlyName"), - } + headers = getPlexHeaders() transferObj.session.headers.update(headers) @@ -255,6 +262,12 @@ def normalizedVersion(ver): return verlib.NormalizedVersion(verlib.suggest_normalized_version('0.0.0')) +def parsePlexDirectHost(hostname): + v6 = hostname.count("-") > 3 + base = hostname.split(".", 1)[0] + return v6 and base.replace("-", ":") or base.replace("-", ".") + + class CompatEvent(Event): def wait(self, timeout): Event.wait(self, timeout) @@ -315,9 +328,9 @@ def is_alive(self): def shouldAbort(self): return False - def join(self): + def join(self, timeout=None): if self.thread.is_alive(): - self.thread.join() + self.thread.join(timeout=timeout) def isExpired(self): return self.event.isSet() diff --git a/script.plexmod/lib/_included_packages/plexnet/video.py b/script.plexmod/lib/_included_packages/plexnet/video.py index 8fe100a50..5768a635d 100644 --- a/script.plexmod/lib/_included_packages/plexnet/video.py +++ b/script.plexmod/lib/_included_packages/plexnet/video.py @@ -331,15 +331,23 @@ def audioChannelsString(self, translate_func=util.dummyTranslate): @property def remainingTime(self): - if not self.viewOffset.asInt(): + return self._remainingTime() + + def _remainingTime(self, view_offset=None): + view_offset = view_offset if view_offset is not None else self.viewOffset.asInt() + if not view_offset: return - return (self.duration.asInt() - self.viewOffset.asInt()) // 1000 + return (self.duration.asInt() - view_offset) // 1000 @property def remainingTimeString(self): - if not self.remainingTime: + return self._remainingTimeString() + + def _remainingTimeString(self, view_offset=None): + remt = self._remainingTime(view_offset=view_offset) + if not remt: return '' - seconds = self.remainingTime + seconds = remt hours = seconds // 3600 minutes = (seconds - hours * 3600) // 60 return (hours and "{}h ".format(hours) or '') + (minutes and "{}m".format(minutes) or "0m") @@ -534,6 +542,10 @@ def actors(self): def isWatched(self): return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + @property + def isFullyWatched(self): + return self.get('viewCount').asInt() > 0 and not self.get('viewOffset').asInt() + def getStreamURL(self, **params): return self._getStreamURL(**params) @@ -580,7 +592,7 @@ def episode(self, title): path = '/library/metadata/%s/allLeaves' % self.ratingKey return plexobjects.findItem(self.server, path, title) - def all(self): + def all(self, *args, **kwargs): return self.episodes() def watched(self): @@ -622,7 +634,7 @@ def episode(self, title): path = self.key return plexobjects.findItem(self.server, path, title) - def all(self): + def all(self, *args, **kwargs): return self.episodes() def show(self): @@ -692,6 +704,10 @@ def subtitleStreams(self): def isWatched(self): return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + @property + def isFullyWatched(self): + return self.get('viewCount').asInt() > 0 and not self.get('viewOffset').asInt() + @property def playbackSettings(self): return self.show().playbackSettings @@ -742,6 +758,10 @@ def _setData(self, data): def isWatched(self): return self.get('viewCount').asInt() > 0 or self.get('viewOffset').asInt() > 0 + @property + def isFullyWatched(self): + return self.get('viewCount').asInt() > 0 and not self.get('viewOffset').asInt() + def getStreamURL(self, **params): return self._getStreamURL(**params) diff --git a/script.plexmod/lib/_included_packages/plexnet/videosession.py b/script.plexmod/lib/_included_packages/plexnet/videosession.py index 933dac0ed..e77f99333 100644 --- a/script.plexmod/lib/_included_packages/plexnet/videosession.py +++ b/script.plexmod/lib/_included_packages/plexnet/videosession.py @@ -2,6 +2,7 @@ import six from collections import OrderedDict from plexnet import plexapp +from kodi_six import xbmc class MediaDetails: @@ -289,6 +290,24 @@ def value(self, obj): return self.resolve(self.retVal, obj) +class DPAttributeMapped(DPAttribute): + def __init__(self): + pass + + def value(self, obj): + p = xbmc.Player() + if p.isPlaying(): + f = p.getPlayingFile() + prot = f.split("://")[0] + if prot == f: + ret = "path mapped" + elif prot.startswith("http"): + ret = prot + else: + ret = "mapped ({})".format(prot) + return ret + + class ComputedPPIValue: """ Holds the final computed attribute data for display @@ -318,7 +337,8 @@ class ModePPI(ComputedPPIValue): name = "Mode" dataPoints = [ DPAttributeSession("partDecision"), - DPAttributeExists("local", source="session.player", returnValue="local") + DPAttributeExists("local", source="session.player", returnValue="local"), + DPAttributeMapped() ] diff --git a/script.plexmod/lib/backgroundthread.py b/script.plexmod/lib/backgroundthread.py index 7b69cd09d..66837cbf4 100644 --- a/script.plexmod/lib/backgroundthread.py +++ b/script.plexmod/lib/backgroundthread.py @@ -42,6 +42,9 @@ def __le__(self, other): def __gt__(self, other): return self._priority > other._priority + def __bool__(self): + return self.isValid() + def start(self): BGThreader.addTask(self) @@ -146,7 +149,7 @@ def kill(self): class BackgroundThreader: - def __init__(self, name=None, worker_count=3): + def __init__(self, name=None, worker_count=5): self.name = name self._queue = MutablePriorityQueue() self._abort = False @@ -227,10 +230,10 @@ def kill(self): class ThreaderManager: - def __init__(self): + def __init__(self, worker_count=5): self.index = 0 self.abandoned = [] - self.threader = BackgroundThreader(str(self.index)) + self.threader = BackgroundThreader(str(self.index), worker_count=worker_count) def __getattr__(self, name): return getattr(self.threader, name) @@ -252,4 +255,4 @@ def kill(self): self.threader.kill() -BGThreader = ThreaderManager() +BGThreader = ThreaderManager(worker_count=util.getSetting('worker_count', 5)) diff --git a/script.plexmod/lib/main.py b/script.plexmod/lib/main.py index 88c347111..6285380f0 100644 --- a/script.plexmod/lib/main.py +++ b/script.plexmod/lib/main.py @@ -77,6 +77,7 @@ def main(): try: util.setGlobalProperty('running', '') util.setGlobalProperty('stop_running', '') + util.setGlobalProperty('ignore_spinner', '') except: pass @@ -99,7 +100,7 @@ def _main(): (len(plexapp.ACCOUNT.homeUsers) > 1 or plexapp.ACCOUNT.isProtected) ): - result = userselect.start() + result = userselect.start(BACKGROUND._winID) if not result: return elif result == 'signout': @@ -110,7 +111,8 @@ def _main(): elif result == 'cancel' and fromSwitch: util.DEBUG_LOG('Main: User selection canceled, reusing previous user') plexapp.ACCOUNT.isAuthenticated = True - + elif result == 'cancel': + return if not fromSwitch: util.DEBUG_LOG('Main: User selected') @@ -135,7 +137,7 @@ def _main(): util.DEBUG_LOG('Main: STARTING WITH SERVER: {0}'.format(selectedServer)) windowutils.HOME = home.HomeWindow.create() - if windowutils.HOME.waitForOpen(): + if windowutils.HOME.waitForOpen(base_win_id=BACKGROUND._winID): windowutils.HOME.modal() else: util.LOG("Couldn't open home window, exiting") diff --git a/script.plexmod/lib/player.py b/script.plexmod/lib/player.py index b2dd54bb5..65c443e18 100644 --- a/script.plexmod/lib/player.py +++ b/script.plexmod/lib/player.py @@ -28,6 +28,7 @@ def __init__(self, player, session_id=None): self.media = None self.baseOffset = 0 self._lastDuration = 0 + self._progressHld = {} self.timelineType = None self.lastTimelineState = None self.ignoreTimelines = False @@ -118,9 +119,6 @@ def currentDuration(self): return self._lastDuration 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 @@ -132,23 +130,45 @@ def updateNowPlaying(self, force=False, refreshQueue=False, t=None, state=None, if not self.shouldSendTimeline(item): return + util.DEBUG_LOG("UpdateNowPlaying: {0}, force: {1} refreshQueue: " + "{2} state: {3} overrideChecks: {4} time: {5}".format(item.ratingKey, + force, refreshQueue, state, overrideChecks, + t)) + state = state or self.player.playState # Avoid duplicates if state == self.lastTimelineState and not force: return + obj = item.choice + + # Ignore sending timelines for multi part media with no duration + if obj and obj.part and obj.part.duration.asInt() == 0 and obj.media.parts and len(obj.media.parts) > 1: + util.LOG("Timeline not supported: the current part doesn't have a valid duration") + return + self.lastTimelineState = state # self.timelineTimer.reset() _time = t or int(self.trueTime * 1000) + self._progressHld[str(item.ratingKey)] = _time # self.trigger("progress", [m, item, time]) if refreshQueue and self.playQueue: self.playQueue.refreshOnTimeline = True + data = plexnetUtil.AttributeDict({ + "key": str(item.key), + "ratingKey": str(item.ratingKey), + "guid": str(item.guid), + "url": str(item.url), + "duration": item.duration.asInt(), + "containerKey": str(item.container.address) + }) + plexapp.util.APP.nowplayingmanager.updatePlaybackState( - self.timelineType, self.player.playerObject, state, _time, self.playQueue, duration=self.currentDuration(), + self.timelineType, data, state, _time, self.playQueue, duration=self.currentDuration(), force=overrideChecks ) @@ -195,6 +215,7 @@ def reset(self): self.seeking = self.NO_SEEK self.seekOnStart = 0 self._lastDuration = 0 + self._progressHld = {} self.mode = self.MODE_RELATIVE self.ended = False self.stoppedManually = False @@ -207,6 +228,7 @@ def setup(self, duration, meta, offset, bif_url, title='', title2='', seeking=NO self.seeking = seeking self.duration = duration self._lastDuration = duration + self._progressHld = {} self.bifURL = bif_url self.title = title self.title2 = title2 @@ -255,8 +277,8 @@ def shouldShowPostPlay(self): if not self.stoppedManually and self.skipPostPlay: return False - if (not util.advancedSettings.postplayAlways and self._lastDuration <= FIVE_MINUTES_MILLIS)\ - or util.advancedSettings.postplayTimeout <= 0: + if (not util.addonSettings.postplayAlways and self._lastDuration <= FIVE_MINUTES_MILLIS)\ + or util.addonSettings.postplayTimeout <= 0: return False return True @@ -281,16 +303,21 @@ def getIntroOffset(self, offset=None, setSkipped=False): return self.getDialog().displayMarkers(onlyReturnIntroMD=True, offset=offset, setSkipped=setSkipped) def next(self, on_end=False): - if self.playlist and next(self.playlist): - self.seeking = self.SEEK_PLAYLIST + hasNext = False + if self.playlist: + hasNext = bool(next(self.playlist)) + if hasNext: + self.seeking = self.SEEK_PLAYLIST if on_end: if self.showPostPlay(): return True - if not self.playlist or self.stoppedManually: + if not self.playlist or self.stoppedManually or (self.playlist and not hasNext): return False + self.triggerProgressEvent() + self.player.playVideoPlaylist(self.playlist, handler=self, resume=False) return True @@ -299,6 +326,7 @@ def prev(self): if not self.playlist or not self.playlist.prev(): return False + self.triggerProgressEvent() self.seeking = self.SEEK_PLAYLIST self.player.playVideoPlaylist(self.playlist, handler=self, resume=False) @@ -443,6 +471,26 @@ def onPlayBackResumed(self): util.CRON.forceTick() # self.hideOSD() + @property + def videoPlayedFac(self): + return self.trueTime * 1000 / float(self.duration) + + @property + def videoWatched(self): + return self.videoPlayedFac >= self.playedThreshold + + def triggerProgressEvent(self): + if not self.player.video: + return + + rk = str(self.player.video.ratingKey) + if rk not in self._progressHld: + # progress already consumed + return + + self.player.trigger('video.progress', data=(rk, self._progressHld[rk] if not self.videoWatched else True)) + self._progressHld = {} + def onPlayBackStopped(self): util.DEBUG_LOG('SeekHandler: onPlayBackStopped - ' 'Seeking={0}, QueueingNext={1}, BingeMode={2}, StoppedManually={3}, SkipPostPlay={4}' @@ -461,10 +509,11 @@ def onPlayBackStopped(self): if self.seeking not in (self.SEEK_IN_PROGRESS, self.SEEK_REWIND): self.updateNowPlaying() + self.triggerProgressEvent() # show post play if possible, if an item has been watched (90% by Plex standards) if self.seeking != self.SEEK_PLAYLIST and self.duration: - playedFac = self.trueTime * 1000 / float(self.duration) + playedFac = self.videoPlayedFac util.DEBUG_LOG("Player - played-threshold: {}/{}".format(playedFac, self.playedThreshold)) if playedFac >= self.playedThreshold and self.next(on_end=True): return @@ -486,6 +535,7 @@ def onPlayBackEnded(self): return self.updateNowPlaying() + self.triggerProgressEvent() if self.queuingNext: util.DEBUG_LOG('SeekHandler: onPlayBackEnded - event ignored') @@ -578,7 +628,7 @@ def setAudioTrack(self): except: util.ERROR() - xbmc.sleep(100) + util.MONITOR.waitForAbort(0.1) util.DEBUG_LOG('Switching audio track - index: {0}'.format(track.typeIndex)) self.player.setAudioStream(track.typeIndex) @@ -628,8 +678,13 @@ def onVideoWindowClosed(self): self.hideOSD() util.DEBUG_LOG('SeekHandler: onVideoWindowClosed - Seeking={0}'.format(self.seeking)) if not self.seeking: + # send events as we might not have seen onPlayBackEnded and/or onPlayBackStopped in certain cases, + # especially when postplay isn't wanted and we're at the end of a show + #self.updateNowPlaying() + #if self._progressHld: + # self.triggerProgressEvent() if self.player.isPlaying(): - self.player.stop() + self.player.stopAndWait() if not self.playlist or not self.playlist.hasNext(): if not self.shouldShowPostPlay(): self.sessionEnded() @@ -681,7 +736,7 @@ def extractTrackInfo(self): if plexID: break - xbmc.sleep(100) + util.MONITOR.waitForAbort(0.1) if not plexID: return @@ -775,14 +830,22 @@ def stampCurrentTime(self): def onMonitorInit(self): self.extractTrackInfo() + self.ignoreTimelines = False self.updateNowPlaying(state='playing') def onPlayBackStarted(self): self.player.lastPlayWasBGM = False self.updatePlayQueue(delay=True) self.extractTrackInfo() + self.ignoreTimelines = False self.updateNowPlaying(state='playing') + def onAVStarted(self): + self.player.trigger('started.audio') + + def onAVChange(self): + self.player.trigger('changed.audio') + def onPlayBackResumed(self): self.updateNowPlaying(state='playing') @@ -792,11 +855,13 @@ def onPlayBackPaused(self): def onPlayBackStopped(self): self.updatePlayQueue() self.updateNowPlaying(state='stopped') + self.ignoreTimelines = True self.finish() def onPlayBackEnded(self): self.updatePlayQueue() self.updateNowPlaying(state='stopped') + self.ignoreTimelines = True self.finish() def onPlayBackFailed(self): @@ -1086,25 +1151,23 @@ def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None, return meta = self.playerObject.metadata - - # Kodi 19 will try to look for subtitles in the directory containing the file. '/' and `/file.mkv` both point - # to the file, and Kodi will happily try to read the whole file without recognizing it isn't a directory. - # To get around that, we omit the filename here since it is unnecessary. - url = meta.streamUrls[0].replace("file.mkv", "").replace("file.mp4", "") + url = meta.streamUrls[0] bifURL = self.playerObject.getBifUrl() util.DEBUG_LOG('Playing URL(+{1}ms): {0}{2}'.format(plexnetUtil.cleanToken(url), offset, bifURL and ' - indexed' or '')) self.ignoreStopEvents = True self.stopAndWait() # Stop before setting up the handler to prevent player events from causing havoc - if self.handler and self.handler.queuingNext and util.advancedSettings.consecutiveVideoPbWait: + if self.handler and self.handler.queuingNext and util.addonSettings.consecutiveVideoPbWait: util.DEBUG_LOG( - "Waiting for {}s until playing back next item".format(util.advancedSettings.consecutiveVideoPbWait)) - util.MONITOR.waitForAbort(util.advancedSettings.consecutiveVideoPbWait) + "Waiting for {}s until playing back next item".format(util.addonSettings.consecutiveVideoPbWait)) + util.MONITOR.waitForAbort(util.addonSettings.consecutiveVideoPbWait) self.ignoreStopEvents = False self.sessionID = session_id or self.sessionID + # fixme: this handler might be accessing a new playerObject, not the one it's expecting to access, + # especially when .next() is used self.handler.setup(self.video.duration.asInt(), meta, offset, bifURL, title=self.video.grandparentTitle, title2=self.video.title, seeking=seeking, chapters=self.video.chapters) @@ -1138,10 +1201,18 @@ def _playVideo(self, offset=0, seeking=0, force_update=False, playerObject=None, self.handler.mode = self.handler.MODE_ABSOLUTE - url = util.addURLParams(url, { - 'X-Plex-Client-Profile-Name': 'Generic', - 'X-Plex-Client-Identifier': plexapp.util.INTERFACE.getGlobal('clientIdentifier') - }) + if not meta.isMapped: + # Kodi 19 will try to look for subtitles in the directory containing the file. '/' and `/file.mkv` both + # point to the file, and Kodi will happily try to read the whole file without recognizing it isn't a + # directory. To get around that, we omit the filename here since it is unnecessary. + omit, fname = url.rsplit("/", 1) + if fname.startswith("file."): + url = "{}/{}".format(omit, "?" + fname.split("?")[1] if "?" in fname else "") + + url = util.addURLParams(url, { + 'X-Plex-Client-Profile-Name': 'Generic', + 'X-Plex-Client-Identifier': plexapp.util.INTERFACE.getGlobal('clientIdentifier') + }) li = xbmcgui.ListItem(self.video.title, path=url) vtype = self.video.type if self.video.type in ('movie', 'episode', 'musicvideo') else 'video' @@ -1219,12 +1290,14 @@ def playAudio(self, track, fanart=None, **kwargs): self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) + self.playerObject = plexplayer.PlexAudioPlayer(track) 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.trigger('starting.audio') self.play(url, li, **kwargs) def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): @@ -1233,6 +1306,7 @@ def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) + self.playerObject = plexplayer.PlexAudioPlayer() plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) plist.clear() index = 1 @@ -1244,6 +1318,7 @@ def playAlbum(self, album, startpos=-1, fanart=None, **kwargs): self.stopAndWait() self.ignoreStopEvents = False self.sessionID = "ALB%s" % album.ratingKey + self.trigger('starting.audio') self.play(plist, startpos=startpos, **kwargs) def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): @@ -1252,6 +1327,7 @@ def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): self.ignoreStopEvents = True self.handler = AudioPlayerHandler(self) + self.playerObject = plexplayer.PlexAudioPlayer() plist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) plist.clear() index = 1 @@ -1271,11 +1347,14 @@ def playAudioPlaylist(self, playlist, startpos=-1, fanart=None, **kwargs): self.stopAndWait() self.ignoreStopEvents = False self.sessionID = "PLS%s" % getattr(playlist, "ratingKey", getattr(playlist, "id", random.randint(0, 1000))) + self.trigger('starting.audio') self.play(plist, startpos=startpos, **kwargs) def createTrackListItem(self, track, fanart=None, index=0): data = base64.urlsafe_b64encode(track.serialize().encode("utf8")).decode("utf8") - url = 'plugin://script.plexmod/play?{0}'.format(data) + if not track.isFullObject(): + track = track.reload() + url = self.playerObject.build(track)['url'] li = xbmcgui.ListItem(track.title, path=url) li.setInfo('music', { 'artist': six.text_type(track.originalTitle or track.grandparentTitle), @@ -1285,6 +1364,8 @@ def createTrackListItem(self, track, fanart=None, index=0): 'tracknumber': track.get('index').asInt(), 'duration': int(track.duration.asInt() / 1000), 'playcount': index, + # fixme: this is not really necessary, as we don't go the plugin:// route anymore. + # changing the track identification style would mean a bigger rewrite, though, so let's keep it. 'comment': 'PLEX-{0}:{1}'.format(track.ratingKey, data) }) art = fanart or track.defaultArt diff --git a/script.plexmod/lib/plex.py b/script.plexmod/lib/plex.py index 9a6506dcf..c3ffecf70 100644 --- a/script.plexmod/lib/plex.py +++ b/script.plexmod/lib/plex.py @@ -76,6 +76,13 @@ def defaultUserAgent(): '%s/%s' % (p_system, p_release)]) +def getFriendlyName(): + fn = util.rpc.Settings.GetSettingValue(setting='services.devicename').get('value', 'Kodi') + if fn: + fn = fn.strip() + return fn or 'Kodi' + + class PlexInterface(plexapp.AppInterface): _regs = { None: {}, @@ -89,7 +96,7 @@ class PlexInterface(plexapp.AppInterface): 'provides': 'player', 'device': util.getPlatform() or plexapp.PLATFORM, 'model': 'Unknown', - 'friendlyName': util.rpc.Settings.GetSettingValue(setting='services.devicename').get('value') or 'Kodi', + 'friendlyName': getFriendlyName(), 'supports1080p60': True, 'vp9Support': True, 'audioChannels': '2.0', @@ -284,15 +291,16 @@ def onManualIPChange(**kwargs): plexapp.util.LOCAL_OVER_SECURE = util.getSetting('prefer_local', False) # set requests timeout -TIMEOUT = float(util.advancedSettings.requestsTimeout) -CONNCHECK_TIMEOUT = float(util.advancedSettings.connCheckTimeout) +TIMEOUT = float(util.addonSettings.requestsTimeout) +CONNCHECK_TIMEOUT = float(util.addonSettings.connCheckTimeout) plexapp.util.TIMEOUT = TIMEOUT plexapp.util.CONN_CHECK_TIMEOUT = asyncadapter.AsyncTimeout(CONNCHECK_TIMEOUT).setConnectTimeout(CONNCHECK_TIMEOUT) -plexapp.util.LAN_REACHABILITY_TIMEOUT = util.advancedSettings.localReachTimeout / 1000.0 +plexapp.util.LAN_REACHABILITY_TIMEOUT = util.addonSettings.localReachTimeout / 1000.0 pnhttp.DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(TIMEOUT).setConnectTimeout(TIMEOUT) asyncadapter.DEFAULT_TIMEOUT = pnhttp.DEFAULT_TIMEOUT plexapp.util.ACCEPT_LANGUAGE = util.ACCEPT_LANGUAGE_CODE plexapp.setUserAgent(defaultUserAgent()) +plexnet_util.BASE_HEADERS = plexnet_util.getPlexHeaders() class CallbackEvent(plexapp.util.CompatEvent): diff --git a/script.plexmod/lib/util.py b/script.plexmod/lib/util.py index 025ae4b73..6f4492640 100644 --- a/script.plexmod/lib/util.py +++ b/script.plexmod/lib/util.py @@ -142,7 +142,7 @@ def _processSetting(setting, default): return setting -class AdvancedSettings(object): +class AddonSettings(object): """ @DynamicAttrs """ @@ -188,6 +188,7 @@ class AdvancedSettings(object): ("consecutive_video_pb_wait", 0.0), ("retrieve_all_media_up_front", False), ("library_chunk_size", 240), + ("verify_mapped_files", True) ) def __init__(self): @@ -198,7 +199,7 @@ def __init__(self): getSetting(setting, default)) -advancedSettings = AdvancedSettings() +addonSettings = AddonSettings() def LOG(msg, level=xbmc.LOGINFO): @@ -209,7 +210,7 @@ def DEBUG_LOG(msg): if _SHUTDOWN: return - if not advancedSettings.debug and not xbmc.getCondVisibility('System.GetBool(debug.showloginfo)'): + if not addonSettings.debug and not xbmc.getCondVisibility('System.GetBool(debug.showloginfo)'): return LOG(msg) @@ -316,169 +317,6 @@ def onSettingsChanged(self): MONITOR = UtilityMonitor() -ADV_MSIZE_RE = re.compile(r'(\d+)') -ADV_RFACT_RE = re.compile(r'(\d+)') -ADV_CACHE_RE = re.compile(r'\s*.*', re.S | re.I) - - -class KodiCacheManager(object): - """ - A pretty cheap approach at managing the section of advancedsettings.xml - - Starting with build 20.90.821 (Kodi 21.0-BETA2) a lot of caching issues have been fixed and - readfactor behaves better. We need to adjust for that. - """ - _cleanData = None - useModernAPI = False - memorySize = 20 # in MB - readFactor = 4 - defRF = 4 - defRFSM = 20 - recRFRange = "4-10" - template = None - orig_tpl_path = os.path.join(ADDON.getAddonInfo('path'), "pm4k_cache_template.xml") - custom_tpl_path = "special://profile/pm4k_cache_template.xml" - translated_ctpl_path = translatePath(custom_tpl_path) - - # give Android a little more leeway with its sometimes weird memory management; otherwise stick with 23% of free mem - safeFactor = .20 if xbmc.getCondVisibility('System.Platform.Android') else .23 - - def __init__(self): - if KODI_BUILD_NUMBER >= 2090821: - self.memorySize = rpc.Settings.GetSettingValue(setting='filecache.memorysize')['value'] - self.readFactor = rpc.Settings.GetSettingValue(setting='filecache.readfactor')['value'] / 100.0 - if self.readFactor % 1 == 0: - self.readFactor = int(self.readFactor) - DEBUG_LOG("Not using advancedsettings.xml for cache/buffer management, we're at least Kodi 21 non-alpha") - self.useModernAPI = True - self.defRFSM = 7 - self.recRFRange = "1.5-4" - - if KODI_BUILD_NUMBER >= 2090830: - self.recRFRange = ADDON.getLocalizedString(32976) - - else: - self.load() - self.template = self.getTemplate() - - plexapp.util.APP.on('change:slow_connection', - lambda value=None, **kwargs: self.write(readFactor=value and self.defRFSM or self.defRF)) - - def getTemplate(self): - if xbmcvfs.exists(self.custom_tpl_path): - try: - f = xbmcvfs.File(self.custom_tpl_path) - data = f.read() - f.close() - if data: - return data - except: - pass - - DEBUG_LOG("Custom pm4k_cache_template.xml not found, using default") - f = xbmcvfs.File(self.orig_tpl_path) - data = f.read() - f.close() - return data - - def load(self): - try: - f = xbmcvfs.File("special://profile/advancedsettings.xml") - data = f.read() - f.close() - except: - LOG('script.plex: No advancedsettings.xml found') - else: - cachexml_match = ADV_CACHE_RE.search(data) - if cachexml_match: - cachexml = cachexml_match.group(0) - - try: - self.memorySize = int(ADV_MSIZE_RE.search(cachexml).group(1)) // 1024 // 1024 - except: - DEBUG_LOG("script.plex: invalid or not found memorysize in advancedsettings.xml") - - try: - self.readFactor = int(ADV_RFACT_RE.search(cachexml).group(1)) - except: - DEBUG_LOG("script.plex: invalid or not found readfactor in advancedsettings.xml") - - self._cleanData = data.replace(cachexml, "") - else: - self._cleanData = data - - def write(self, memorySize=None, readFactor=None): - memorySize = self.memorySize = memorySize if memorySize is not None else self.memorySize - readFactor = self.readFactor = readFactor if readFactor is not None else self.readFactor - - if self.useModernAPI: - # kodi cache settings have moved to Services>Caching - try: - rpc.Settings.SetSettingValue(setting='filecache.memorysize', value=self.memorySize) - rpc.Settings.SetSettingValue(setting='filecache.readfactor', value=int(self.readFactor * 100)) - except: - pass - return - - cd = self._cleanData - if not cd: - cd = "\n" - - finalxml = "{}\n".format( - cd.replace("", self.template.format(memorysize=memorySize * 1024 * 1024, - readfactor=readFactor)) - ) - - try: - f = xbmcvfs.File("special://profile/advancedsettings.xml", "w") - f.write(finalxml) - f.close() - except: - ERROR("Couldn't write advancedsettings.xml") - - def clamp16(self, x): - return x - x % 16 - - @property - def viableOptions(self): - default = list(filter(lambda x: x < self.recMax, - [16, 20, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024])) - - # add option to overcommit slightly - overcommit = [] - if xbmc.getCondVisibility('System.Platform.Android'): - overcommit.append(min(self.clamp16(int(self.free * 0.23)), 2048)) - - overcommit.append(min(self.clamp16(int(self.free * 0.26)), 2048)) - overcommit.append(min(self.clamp16(int(self.free * 0.3)), 2048)) - - # re-append current memorySize here, as recommended max might have changed - return list(sorted(list(set(default + [self.memorySize, self.recMax] + overcommit)))) - - @property - def readFactorOpts(self): - ret = list(sorted(list(set([1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5, 7, 10, 15, 20, 30, 50] + [self.readFactor])))) - if KODI_BUILD_NUMBER >= 2090830 and self.readFactor > 0: - # support for adaptive read factor from build 2090822 onwards - ret.insert(0, 0) - return ret - - @property - def free(self): - return float(xbmc.getInfoLabel('System.Memory(free)')[:-2]) - - @property - def recMax(self): - freeMem = self.free - recMem = min(int(freeMem * self.safeFactor), 2048) - LOG("Free memory: {} MB, recommended max: {} MB".format(freeMem, recMem)) - return recMem - - -kcm = KodiCacheManager() - -CACHE_SIZE = kcm.memorySize - def T(ID, eng=''): return ADDON.getLocalizedString(ID) @@ -486,14 +324,14 @@ def T(ID, eng=''): hasCustomBGColour = False if KODI_VERSION_MAJOR > 18: - hasCustomBGColour = not advancedSettings.dynamicBackgrounds and advancedSettings.backgroundColour and \ - advancedSettings.backgroundColour != "-" + hasCustomBGColour = not addonSettings.dynamicBackgrounds and addonSettings.backgroundColour and \ + addonSettings.backgroundColour != "-" def getAdvancedSettings(): # yes, global, hang me! - global advancedSettings - advancedSettings = AdvancedSettings() + global addonSettings + addonSettings = AddonSettings() def reInitAddon(): @@ -665,7 +503,7 @@ def shortenText(text, size): def scaleResolution(w, h, by=None): if by is None: - by = advancedSettings.posterResolutionScalePerc + by = addonSettings.posterResolutionScalePerc if 0 < by != 100.0: px = w * h * (by / 100.0) @@ -951,6 +789,103 @@ def getTimeFormat(): timeFormat, timeFormatKN, padHour = getTimeFormat() +DEF_THEME = "modern-colored" +THEME_VERSION = 2 + + +def applyTheme(theme=None): + """ + Dynamically build script-plex-seek_dialog.xml by combining a player button template with + script-plex-seek_dialog_skeleton.xml + """ + theme = theme or getSetting('theme', DEF_THEME) + skel = os.path.join(ADDON.getAddonInfo('path'), "resources", "skins", "Main", "1080i", + "script-plex-seek_dialog_skeleton.xml") + if theme == "custom": + btnTheme = os.path.join(ADDON.getAddonInfo("profile"), "templates", + "seek_dialog_buttons_custom.xml") + customSkel = os.path.join(ADDON.getAddonInfo("profile"), "templates", + "script-plex-seek_dialog_skeleton_custom.xml") + if xbmcvfs.exists(customSkel): + skel = customSkel + else: + btnTheme = os.path.join(ADDON.getAddonInfo('path'), "resources", "skins", "Main", "1080i", "templates", + "seek_dialog_buttons_{}.xml".format(theme)) + + if not xbmcvfs.exists(btnTheme): + LOG("Theme {} doesn't exist, falling back to modern".format(theme)) + setSetting('theme', DEF_THEME) + return applyTheme(DEF_THEME) + + try: + # read skeleton + f = xbmcvfs.File(skel) + skelData = f.read() + f.close() + except: + ERROR("Couldn't find {}".format("script-plex-seek_dialog_skeleton.xml")) + else: + try: + # read button theme + f = xbmcvfs.File(btnTheme) + btnData = f.read() + f.close() + except: + ERROR("Couldn't find {}".format("seek_dialog_buttons_{}.xml".format(theme))) + else: + # combine both + finalXML = skelData.replace('', btnData) + try: + # write final file + f = xbmcvfs.File(os.path.join(ADDON.getAddonInfo('path'), "resources", "skins", "Main", "1080i", + "script-plex-seek_dialog.xml"), "w") + f.write(finalXML) + f.close() + except: + ERROR("Couldn't write script-plex-seek_dialog.xml") + else: + LOG('Using theme: {}'.format(theme)) + + +# apply theme if version changed +theme = getSetting('theme', DEF_THEME) +curThemeVer = getSetting('theme_version', 0) +if curThemeVer < THEME_VERSION: + setSetting('theme_version', THEME_VERSION) + # apply seekdialog button theme + applyTheme(theme) + +# apply theme if seek_dialog xml missing +if not xbmcvfs.exists(os.path.join(ADDON.getAddonInfo('path'), "resources", "skins", "Main", "1080i", + "script-plex-seek_dialog.xml")): + applyTheme(theme) + +PM_MCMT_RE = re.compile(r'/\*.+\*/\s?', re.IGNORECASE | re.MULTILINE | re.DOTALL) +PM_CMT_RE = re.compile(r'[\t ]+//.+\n?') +PM_COMMA_RE = re.compile(r',\s*}\s*}') + +# path mapping +mapfile = os.path.join(translatePath(ADDON.getAddonInfo("profile")), "path_mapping.json") +PATH_MAP = None +if xbmcvfs.exists(mapfile): + try: + f = xbmcvfs.File(mapfile) + # sanitize json + + # remove multiline comments + data = PM_MCMT_RE.sub("", f.read()) + # remove comments + data = PM_CMT_RE.sub("", data) + # remove invalid trailing comma + + data = PM_COMMA_RE.sub("}}", data) + PATH_MAP = json.loads(data) + f.close() + except: + ERROR("Couldn't read path_mapping.json") + else: + LOG("Path mapping: {}".format(repr(PATH_MAP))) + def populateTimeFormat(): global timeFormat, timeFormatKN, padHour @@ -972,14 +907,16 @@ def getPlatform(): return key.rsplit('.', 1)[-1] -def getProgressImage(obj, perc=None): +def getProgressImage(obj, perc=None, view_offset=None): if not obj and not perc: return '' if obj: - if not obj.get('viewOffset') or not obj.get('duration'): + if not view_offset: + view_offset = obj.get('viewOffset') and obj.viewOffset.asInt() + if not view_offset or not obj.get('duration'): return '' - pct = int((obj.viewOffset.asInt() / obj.duration.asFloat()) * 100) + pct = int((view_offset / obj.duration.asFloat()) * 100) else: pct = perc pct = pct - pct % 2 # Round to even number - we have even numbered progress only @@ -992,8 +929,8 @@ def backgroundFromArt(art, width=1920, height=1080, background=colors.noAlpha.Ba return return art.asTranscodedImageURL( width, height, - blur=advancedSettings.backgroundArtBlurAmount2, - opacity=advancedSettings.backgroundArtOpacityAmount2, + blur=addonSettings.backgroundArtBlurAmount2, + opacity=addonSettings.backgroundArtOpacityAmount2, background=background ) diff --git a/script.plexmod/lib/windows/currentplaylist.py b/script.plexmod/lib/windows/currentplaylist.py index 91dffd23a..4a278cda7 100644 --- a/script.plexmod/lib/windows/currentplaylist.py +++ b/script.plexmod/lib/windows/currentplaylist.py @@ -61,7 +61,7 @@ def __init__(self, *args, **kwargs): self.musicPlayerWinID = kwargs.get('winID') def doClose(self, **kwargs): - player.PLAYER.off('playback.started', self.onPlayBackStarted) + player.PLAYER.off('av.started', self.onPlayBackStarted) player.PLAYER.off('playlist.changed', self.playQueueCallback) if player.PLAYER.handler.playQueue and player.PLAYER.handler.playQueue.isRemote: player.PLAYER.handler.playQueue.off('change', self.updateProperties) @@ -123,7 +123,6 @@ def onFocus(self, controlID): self.updateSelectedProgress() def onPlayBackStarted(self, **kwargs): - xbmc.sleep(2000) self.setDuration() def repeatButtonClicked(self): @@ -180,7 +179,8 @@ def optionsButtonClicked(self, pos=(670, 1060)): def stopButtonClicked(self): xbmc.executebuiltin('Action(Back, {})'.format(self.musicPlayerWinID)) - xbmc.sleep(500) + util.MONITOR.waitForAbort(0.5) + player.PLAYER.stopAndWait() self.doClose() def selectPlayingItem(self): @@ -257,7 +257,7 @@ def setupSeekbar(self): self.selectionBox = self.getControl(self.SELECTION_BOX) self.selectionBoxHalf = self.SELECTION_BOX_WIDTH // 2 self.selectionBoxMax = self.SEEK_IMAGE_WIDTH - player.PLAYER.on('playback.started', self.onPlayBackStarted) + player.PLAYER.on('av.started', self.onPlayBackStarted) def checkSeekActions(self, action, controlID): if controlID == self.SEEK_BUTTON_ID: diff --git a/script.plexmod/lib/windows/episodes.py b/script.plexmod/lib/windows/episodes.py index d6c6ed391..20ce0e44e 100644 --- a/script.plexmod/lib/windows/episodes.py +++ b/script.plexmod/lib/windows/episodes.py @@ -11,7 +11,6 @@ from lib import player from plexnet import plexapp, playlist, plexplayer -from plexnet.util import INTERFACE from . import busy from . import videoplayer @@ -22,7 +21,6 @@ from . import playersettings from . import info from . import optionsdialog -from . import preplayutils from . import pagination from . import playbacksettings @@ -232,7 +230,8 @@ def __init__(self, *args, **kwargs): self.initialized = False self.currentItemLoaded = False self.closing = False - self._reloadVideos = [] + self.manuallySelected = False + self._videoProgress = None def reset(self, episode, season=None, show=None): self.episode = episode @@ -245,7 +244,8 @@ def reset(self, episode, season=None, show=None): self.parentList = None self.seasons = None - self._reloadVideos = [] + self.manuallySelected = False + self._videoProgress = None #self.initialized = False def doClose(self): @@ -258,6 +258,7 @@ def doClose(self): self.tasks = None try: player.PLAYER.off('new.video', self.onNewVideo) + player.PLAYER.off('video.progress', self.onVideoProgress) except KeyError: pass @@ -294,8 +295,16 @@ def onReInit(self): if not self.tasks: self.tasks = backgroundthread.Tasks() + if self.manuallySelected and not self._videoProgress: + util.DEBUG_LOG("Episodes: ReInit: Not doing anything, as we've previously manually selected " + "this item and don't have progress") + return + + self.manuallySelected = False + util.DEBUG_LOG("Episodes: {}: Got progress info: {}".format( + self.episode and self.episode.ratingKey or None, self._videoProgress)) try: - self.selectEpisode() + self.selectEpisode(progress_data=self._videoProgress) except AttributeError: raise util.NoDataException @@ -303,26 +312,34 @@ def onReInit(self): if not mli or not self.episodesPaginator: return - reloadItems = [mli] - for v in self._reloadVideos: + reload_items = [mli] + skip_progress_for = None + if self._videoProgress: + skip_progress_for = [] for m in self.episodeListControl: - if m.dataSource == v: - reloadItems.append(m) - self.episodesPaginator.prepareListItem(v, m) + # pagination boundary + if not m.dataSource: + continue - # re-set current item's progress to a loading state - if util.getSetting("slow_connection", False): - self.progressImageControl.setWidth(1) - mli.setProperty('remainingTime', T(32914, "Loading")) + if m.dataSource.ratingKey in self._videoProgress: + reload_items.append(m) + skip_progress_for.append(m.dataSource.ratingKey) + del self._videoProgress[m.dataSource.ratingKey] + if not self._videoProgress: + break + + self._videoProgress = None + + reload_items = list(set(reload_items)) + select_episode = reload_items and reload_items[-1] or mli + + self.episodesPaginator.setEpisode(select_episode.dataSource) + self.reloadItems(items=reload_items, with_progress=True, skip_progress_for=skip_progress_for) - self.reloadItems(items=reloadItems, with_progress=True) - self.episodesPaginator.setEpisode(self._reloadVideos and self._reloadVideos[-1] or mli) - self._reloadVideos = [] self.fillRelated() - def postSetup(self, from_select_episode=False): - self.selectEpisode(from_select_episode=from_select_episode) - self.checkForHeaderFocus(xbmcgui.ACTION_MOVE_DOWN) + def postSetup(self): + self.checkForHeaderFocus(xbmcgui.ACTION_MOVE_DOWN, initial=True) selected = self.episodeListControl.getSelectedItem() if selected: @@ -336,16 +353,17 @@ def postSetup(self, from_select_episode=False): def setup(self): self._setup() - def _setup(self, from_select_episode=False): + def _setup(self): player.PLAYER.on('new.video', self.onNewVideo) + player.PLAYER.on('video.progress', self.onVideoProgress) (self.season or self.show_).reload(checkFiles=1, **VIDEO_RELOAD_KW) - if not from_select_episode or not self.episodesPaginator: + if not self.episodesPaginator: self.episodesPaginator = EpisodesPaginator(self.episodeListControl, leaf_count=int(self.season.leafCount) if self.season else 0, parent_window=self) - if not from_select_episode or not self.episodesPaginator: + if not self.episodesPaginator: self.relatedPaginator = RelatedPaginator(self.relatedListControl, leaf_count=int(self.show_.relatedCount), parent_window=self) @@ -361,20 +379,63 @@ def _setup(self, from_select_episode=False): hasPrev = self.fillRelated(hasPrev) self.fillRoles(hasPrev) - def selectEpisode(self, from_select_episode=False): - if not self.episode or not self.episodesPaginator: + def selectEpisode(self, progress_data=None): + if not self.episodesPaginator: return + had_progress_data = bool(progress_data) + progress_data_left = None + if had_progress_data: + progress_data_left = progress_data.copy() + + set_main_progress_to = None + for mli in self.episodeListControl: - if mli.dataSource == self.episode: - self.episodeListControl.selectItem(mli.pos()) - self.episodesPaginator.setEpisode(self.episode) + # pagination boundary + if not mli.dataSource: + continue + + just_fully_watched = False + if progress_data_left and mli.dataSource: + progress = progress_data_left.pop(mli.dataSource.ratingKey, False) + # progress can be False (no entry), a number (progress), or True (fully watched just now) + # select it if it's not watched or in progress + if progress is True: + just_fully_watched = True + mli.setProperty('unwatched', '') + mli.setProperty('progress', '') + + elif progress and progress > 60000: + mli.setProperty('progress', util.getProgressImage(mli.dataSource, view_offset=progress)) + set_main_progress_to = progress + + # after immediately updating the watched state, if we still have data left, continue + if progress is True and progress_data_left: + continue + + # first condition: we select self.episode if we've got no progress data or we haven't watched it just now. + # second condition: we've just come from playback with progress upon reinit. select the next available + # episode that's either unwatched or in progress. + # third condition: select the next unwatched episode if we don't have self.episode and didn't have any + # player progress, which happens when being called without an episode (season view, show view). + if (mli.dataSource == self.episode and not just_fully_watched and not progress_data_left) or \ + (had_progress_data and not progress_data_left and not just_fully_watched + and not mli.dataSource.isFullyWatched) or \ + (not had_progress_data and not self.episode and not mli.dataSource.isFullyWatched): + if self.episodeListControl.getSelectedPosition() < mli.pos(): + self.episodeListControl.selectItem(mli.pos()) + self.episodesPaginator.setEpisode(self.episode or mli.dataSource) + if just_fully_watched: + set_main_progress_to = 0 + + # this is a little counter intuitive - None is actually valid here, and if set to None, setProgress will + # use the actual item progress, not ours + self.setProgress(mli, view_offset=set_main_progress_to) break else: - if not from_select_episode: - self.reset(self.episode) - self._setup(from_select_episode=True) - self.postSetup(from_select_episode=True) + # no matching episode found + mli = self.episodeListControl.getSelectedItem() + self.setProgress(mli, view_offset=0) self.episode = None @@ -427,7 +488,7 @@ def onAction(self, action): elif action == xbmcgui.ACTION_NAV_BACK: if (not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( self.OPTIONS_GROUP_ID)) or not controlID) and \ - not util.advancedSettings.fastBack: + not util.addonSettings.fastBack: if self.getProperty('on.extras'): self.setFocusId(self.OPTIONS_GROUP_ID) return @@ -448,10 +509,18 @@ def onNewVideo(self, video=None, **kwargs): util.DEBUG_LOG('Updating selected episode: {0}'.format(video)) self.episode = video - self._reloadVideos.append(video) return True + def onVideoProgress(self, data=None, **kwargs): + if not data: + return + + util.DEBUG_LOG("Storing video progress data: {}".format(data)) + if not self._videoProgress: + self._videoProgress = {} + self._videoProgress[data[0]] = data[1] + def checkOptionsAction(self, action): if action == xbmcgui.ACTION_MOVE_UP: mli = self.episodeListControl.getSelectedItem() @@ -757,8 +826,6 @@ def episodeListClicked(self, force_episode=None, from_auto_play=False): if choice['key'] == 'resume': resume = True - self._reloadVideos.append(episode) - pl = playlist.LocalPlaylist(self.show_.all(), self.show_.getServer()) try: if len(pl): # Don't use playlist if it's only this video @@ -910,10 +977,10 @@ def _delete(self): (self.season or self.show_).reload() return success - def checkForHeaderFocus(self, action): + def checkForHeaderFocus(self, action, initial=False): # don't continue if we're still waiting for tasks if self.tasks or not self.episodesPaginator: - if self.tasks: + if self.tasks and not initial: util.DEBUG_LOG("Episodes: Moving too fast through paginator, throttling.") return @@ -943,7 +1010,10 @@ def checkForHeaderFocus(self, action): if action in (xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_PAGE_UP): if mli.getProperty('is.header'): xbmc.executebuiltin('Action(up)') - if action in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN, xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + if action in (xbmcgui.ACTION_MOVE_DOWN, xbmcgui.ACTION_PAGE_DOWN, xbmcgui.ACTION_MOVE_LEFT, + xbmcgui.ACTION_MOVE_RIGHT): + if not initial and action in (xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT): + self.manuallySelected = True if mli.getProperty('is.header'): xbmc.executebuiltin('Action(down)') @@ -1054,16 +1124,18 @@ def setItemAudioAndSubtitleInfo(self, video, mli): else: mli.setProperty('subtitles', T(32309, 'None')) - def setProgress(self, mli): + def setProgress(self, mli, view_offset=None): video = mli.dataSource - if video.viewOffset.asInt(): - width = video.viewOffset.asInt() and (1 + int((video.viewOffset.asInt() / video.duration.asFloat()) * self.width)) or 1 + view_offset = view_offset if view_offset is not None else video.viewOffset.asInt() + if view_offset: + width = view_offset and (1 + int((view_offset / video.duration.asFloat()) * self.width)) or 1 self.progressImageControl.setWidth(width) else: self.progressImageControl.setWidth(1) - if video.viewOffset.asInt(): - mli.setProperty('remainingTime', T(33615, "{time} left").format(time=video.remainingTimeString)) + if view_offset: + mli.setProperty('remainingTime', T(33615, + "{time} left").format(time=video._remainingTimeString(view_offset))) else: mli.setProperty('remainingTime', '') @@ -1087,15 +1159,21 @@ def createListItem(self, episode): def fillEpisodes(self, update=False): items = self.episodesPaginator.paginate() - self.reloadItems(items) + if not update: + self.selectEpisode() + self.reloadItems(items, with_progress=True) - def reloadItems(self, items, with_progress=False): + def reloadItems(self, items, with_progress=False, skip_progress_for=None): tasks = [] for mli in items: if not mli.dataSource: continue - task = EpisodeReloadTask().setup(mli.dataSource, self.reloadItemCallback, with_progress=with_progress) + item_progress = with_progress + if skip_progress_for: + item_progress = False if mli.dataSource.ratingKey in skip_progress_for else with_progress + + task = EpisodeReloadTask().setup(mli.dataSource, self.reloadItemCallback, with_progress=item_progress) self.tasks.add(task) tasks.append(task) @@ -1128,7 +1206,8 @@ def reloadItemCallback(self, task, episode, with_progress=False): self.episodesPaginator.prepareListItem(None, mli) if mli == selected: self.lastItem = mli - self.setProgress(mli) + if with_progress: + self.setProgress(mli) if not self.currentItemLoaded and ( mli == selected or (self.episode and self.episode == mli.dataSource)): @@ -1145,7 +1224,8 @@ def reloadItemCallback(self, task, episode, with_progress=False): tries += 1 if xbmc.getCondVisibility('Control.IsVisible({})'.format(PBID)): self.setFocusId(PBID) - return + + break def fillExtras(self, has_prev=False): items = [] diff --git a/script.plexmod/lib/windows/home.py b/script.plexmod/lib/windows/home.py index 3777707c6..7724205d1 100644 --- a/script.plexmod/lib/windows/home.py +++ b/script.plexmod/lib/windows/home.py @@ -21,6 +21,7 @@ from . import optionsdialog from lib.util import T +from lib.plex_hosts import pdm from six.moves import range HUBS_REFRESH_INTERVAL = 300 # 5 Minutes @@ -280,18 +281,24 @@ class HomeWindow(kodigui.BaseWindow, util.CronReceiver): HUBMAP = { # HOME 'home.continue': {'index': 0, 'with_progress': True, 'with_art': True, 'do_updates': True, 'text2lines': True}, + # This hub can be enabled in the settings so PM4K behaves like any other Plex client. + # It overrides home.continue and home.ondeck + 'continueWatching': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, 'home.ondeck': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, 'home.television.recent': {'index': 2, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + # This is a virtual hub and it appears when the library recommendation is customized in Plex and + # Recently Released is checked. + 'home.VIRTUAL.movies.recentlyreleased': {'index': 3, 'do_updates': True, 'with_progress': True, 'text2lines': True}, 'home.movies.recent': {'index': 4, 'do_updates': True, 'with_progress': True, 'text2lines': True}, 'home.music.recent': {'index': 5, 'text2lines': True}, 'home.videos.recent': {'index': 6, 'with_progress': True, 'ar16x9': True}, #'home.playlists': {'index': 9}, # No other Plex home screen shows playlists so removing it from here 'home.photos.recent': {'index': 10, 'text2lines': True}, # SHOW - 'tv.ondeck': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, - 'tv.recentlyaired': {'index': 2, 'do_updates': True, 'with_progress': True, 'text2lines': True}, - 'tv.recentlyadded': {'index': 3, 'do_updates': True, 'with_progress': True, 'text2lines': True}, - 'tv.inprogress': {'index': 4, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'tv.inprogress': {'index': 1, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'tv.ondeck': {'index': 2, 'with_progress': True, 'do_updates': True, 'text2lines': True}, + 'tv.recentlyaired': {'index': 3, 'do_updates': True, 'with_progress': True, 'text2lines': True}, + 'tv.recentlyadded': {'index': 4, 'do_updates': True, 'with_progress': True, 'text2lines': True}, 'tv.startwatching': {'index': 7, 'with_progress': True, 'do_updates': True}, 'tv.rediscover': {'index': 8, 'with_progress': True, 'do_updates': True}, 'tv.morefromnetwork': {'index': 13, 'with_progress': True, 'do_updates': True}, @@ -364,7 +371,7 @@ def __init__(self, *args, **kwargs): def onFirstInit(self): # set last BG image if possible - if util.advancedSettings.dynamicBackgrounds: + if util.addonSettings.dynamicBackgrounds: bgUrl = util.getSetting("last_bg_url") if bgUrl: self.windowSetBackground(bgUrl) @@ -417,6 +424,7 @@ def onFirstInit(self): self.hookSignals() util.CRON.registerReceiver(self) self.updateProperties() + self.checkPlexDirectHosts(plexapp.SERVERMANAGER.allConnections, source="stored") def onReInit(self): if self.lastFocusID: @@ -436,9 +444,63 @@ def onReInit(self): else: self.setFocusId(self.lastFocusID) + def checkPlexDirectHosts(self, hosts, source="stored", *args, **kwargs): + handlePD = util.getSetting('handle_plexdirect', 'ask') + if handlePD == "never": + return + + knownHosts = pdm.getHosts() + pdHosts = [host for host in hosts if ".plex.direct:" in host] + + util.DEBUG_LOG("Checking host mapping for {} {} connections".format(len(pdHosts), source)) + + newHosts = set(pdHosts) - set(knownHosts) + if newHosts: + pdm.newHosts(newHosts, source=source) + diffLen = len(pdm.diff) + + # there are situations where the myPlexManager's resources are ready earlier than + # any other. In that case, force the check. + force = plexapp.MANAGER.gotResources + + if ((source == "stored" and plexapp.ACCOUNT.isOffline) or source == "myplex" or force) and pdm.differs: + if handlePD == 'ask': + button = optionsdialog.show( + T(32993, '').format(diffLen), + T(32994, '').format(diffLen), + T(32328, 'Yes'), + T(32035, 'Always'), + T(32033, 'Never'), + ) + if button not in (0, 1, 2): + return + + if button == 1: + util.setSetting('handle_plexdirect', 'always') + elif button == 2: + util.setSetting('handle_plexdirect', 'never') + return + + hadHosts = pdm.hadHosts + pdm.write() + + if not hadHosts and handlePD == "ask": + optionsdialog.show( + T(32995, ''), + T(32996, ''), + T(32997, 'OK'), + ) + else: + # be less intrusive + util.showNotification(T(32996, ''), header=T(32995, '')) + def updateProperties(self, *args, **kwargs): self.setBoolProperty('bifurcation_lines', util.getSetting('hubs_bifurcation_lines', False)) + def setTheme(self, *args, **kwargs): + util.theme = kwargs["value"] + util.applyTheme() + def focusFirstValidHub(self, startIndex=None): indices = self.hubFocusIndexes if startIndex is not None: @@ -471,9 +533,12 @@ def hookSignals(self): plexapp.SERVERMANAGER.on('reachable:server', self.displayServerAndUser) plexapp.util.APP.on('change:selectedServer', self.onSelectedServerChange) + plexapp.util.APP.on('loaded:server_connections', self.checkPlexDirectHosts) plexapp.util.APP.on('account:response', self.displayServerAndUser) plexapp.util.APP.on('sli:reachability:received', self.displayServerAndUser) plexapp.util.APP.on('change:hubs_bifurcation_lines', self.updateProperties) + plexapp.util.APP.on('change:hubs_use_new_continue_watching', self.fullyRefreshHome) + plexapp.util.APP.on('change:theme', self.setTheme) player.PLAYER.on('session.ended', self.updateOnDeckHubs) util.MONITOR.on('changed.watchstatus', self.updateOnDeckHubs) @@ -485,9 +550,12 @@ def unhookSignals(self): plexapp.SERVERMANAGER.off('reachable:server', self.displayServerAndUser) plexapp.util.APP.off('change:selectedServer', self.onSelectedServerChange) + plexapp.util.APP.off('loaded:server_connections', self.checkPlexDirectHosts) plexapp.util.APP.off('account:response', self.displayServerAndUser) plexapp.util.APP.off('sli:reachability:received', self.displayServerAndUser) plexapp.util.APP.off('change:hubs_bifurcation_lines', self.updateProperties) + plexapp.util.APP.off('change:hubs_use_new_continue_watching', self.fullyRefreshHome) + plexapp.util.APP.off('change:theme', self.setTheme) player.PLAYER.off('session.ended', self.updateOnDeckHubs) util.MONITOR.off('changed.watchstatus', self.updateOnDeckHubs) @@ -500,7 +568,7 @@ def tick(self): if not hubs: return - if time.time() - hubs.lastUpdated > HUBS_REFRESH_INTERVAL: + if time.time() - hubs.lastUpdated > HUBS_REFRESH_INTERVAL and not xbmc.Player().isPlayingVideo(): self.showHubs(self.lastSection, update=True) def shutdown(self): @@ -514,7 +582,7 @@ def shutdown(self): self.storeLastBG() def storeLastBG(self): - if util.advancedSettings.dynamicBackgrounds: + if util.addonSettings.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 @@ -617,7 +685,7 @@ def onAction(self, action): self.showHubs(HomeSection) return - if util.advancedSettings.fastBack and not optionsFocused and offSections \ + if util.addonSettings.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): self.setProperty('hub.focus', '0') @@ -626,7 +694,7 @@ def onAction(self, action): 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): + and (not util.addonSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): self.lastNonOptionsFocusID = self.lastFocusID self.setFocusId(self.OPTIONS_GROUP_ID) return @@ -745,6 +813,11 @@ def updateOnDeckHubs(self, **kwargs): def showBusy(self, on=True): self.setProperty('busy', on and '1' or '') + def fullyRefreshHome(self, *args, **kwargs): + self.showSections() + self.backgroundSet = False + self.showHubs(HomeSection) + @busy.dialog() def serverRefresh(self): backgroundthread.BGThreader.reset() @@ -759,9 +832,7 @@ def serverRefresh(self): self.setFocusId(self.USER_BUTTON_ID) return False - self.showSections() - self.backgroundSet = False - self.showHubs(HomeSection) + self.fullyRefreshHome() return True def hubItemClicked(self, hubControlID, auto_play=False): @@ -844,7 +915,7 @@ def checkSectionItem(self, force=False, action=None): self.storeLastBG() if item.dataSource != self.lastSection: - self.sectionChanged(force) + self.sectionChanged() def checkHubItem(self, controlID, actionID=None): control = self.hubControls[controlID - 400] @@ -852,20 +923,21 @@ def checkHubItem(self, controlID, actionID=None): 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: + if util.addonSettings.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) + if mli: + 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 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 + return + self._lastSelectedItem = (controlID, mlipos) return mli.setBoolProperty('is.updating', True) @@ -897,18 +969,27 @@ def displayServerAndUser(self, **kwargs): self.setProperty('server.iconmod2', '') def cleanTasks(self): - self.tasks = [t for t in self.tasks if t.isValid()] + self.tasks = [t for t in self.tasks if t] - def sectionChanged(self, force=False): + def sectionChanged(self): self.sectionChangeTimeout = time.time() + 0.5 - if not self.sectionChangeThread or not self.sectionChangeThread.is_alive() or force: - if self.sectionChangeThread and self.sectionChangeThread.is_alive(): - self.sectionChangeThread.join() + # wait 2s at max if we're currently awaiting any hubs to reload + # fixme: this can be done in a better way, probably + waited = 0 + while any(self.tasks) and waited < 20: + self.showBusy(True) + util.MONITOR.waitForAbort(0.1) + waited += 1 + self.showBusy(False) + + if not self.sectionChangeThread or (self.sectionChangeThread and not self.sectionChangeThread.is_alive()): self.sectionChangeThread = threading.Thread(target=self._sectionChanged, name="sectionchanged") self.sectionChangeThread.start() def _sectionChanged(self): + if not self.sectionChangeTimeout: + return while not util.MONITOR.waitForAbort(0.1): if time.time() >= self.sectionChangeTimeout: break @@ -917,21 +998,18 @@ def _sectionChanged(self): if self.lastSection == ds: return - self.lastSection = ds + self._sectionReallyChanged(ds) - self._sectionReallyChanged() - - def _sectionReallyChanged(self): + def _sectionReallyChanged(self, section): with self.lock: - section = self.lastSection self.setProperty('hub.focus', '') - if util.advancedSettings.dynamicBackgrounds: + if util.addonSettings.dynamicBackgrounds: self.backgroundSet = False util.DEBUG_LOG('Section changed ({0}): {1}'.format(section.key, repr(section.title))) self.showHubs(section) self.lastSection = section - self.checkSectionItem(force=True) + #self.checkSectionItem(force=True) def sectionHubsCallback(self, section, hubs): with self.lock: @@ -1065,16 +1143,18 @@ def _showHubs(self, section=None, update=False): try: hasContent = False skip = {} + for hub in hubs: - identifier = hub.getCleanHubIdentifier() + identifier = hub.getCleanHubIdentifier(is_home=not section.key) if identifier not in self.HUBMAP: - util.DEBUG_LOG('UNHANDLED - Hub: {0} [{1}]({2})'.format(hub.hubIdentifier, identifier, len(hub.items))) + util.DEBUG_LOG('UNHANDLED - Hub: {0} [{1}]({2})'.format(hub.hubIdentifier, identifier, + len(hub.items))) continue skip[self.HUBMAP[identifier]['index']] = 1 - if self.showHub(hub): + if self.showHub(hub, is_home=not section.key): if hub.items: hasContent = True if self.HUBMAP[identifier].get('do_updates'): @@ -1103,8 +1183,8 @@ def _showHubs(self, section=None, update=False): finally: self.showBusy(False) - def showHub(self, hub, items=None): - identifier = hub.getCleanHubIdentifier() + def showHub(self, hub, items=None, is_home=False): + identifier = hub.getCleanHubIdentifier(is_home=is_home) if identifier in self.HUBMAP: util.DEBUG_LOG('HUB: {0} [{1}]({2}, {3})'.format(hub.hubIdentifier, @@ -1277,7 +1357,7 @@ def _showHub(self, hub, hubitems=None, index=None, with_progress=False, with_art mli.setProperty('progress', util.getProgressImage(mli.dataSource)) if with_art: for mli in items: - thumb = (util.advancedSettings.continueUseThumb + thumb = (util.addonSettings.continueUseThumb and mli.dataSource.type == 'episode' and mli.dataSource.thumb ) \ @@ -1471,7 +1551,7 @@ def doUserOption(self): elif option == 'go_online': plexapp.ACCOUNT.refreshAccount() elif option == 'refresh_users': - plexapp.ACCOUNT.updateHomeUsers() + plexapp.ACCOUNT.updateHomeUsers(refreshSubscription=True) return True else: self.closeOption = option diff --git a/script.plexmod/lib/windows/info.py b/script.plexmod/lib/windows/info.py index 7c610b9ca..d44f4991f 100644 --- a/script.plexmod/lib/windows/info.py +++ b/script.plexmod/lib/windows/info.py @@ -72,6 +72,7 @@ def getVideoInfo(self): addMedia.append("Unavailable: {}".format(os.path.basename(part.file))) continue + pmFolder = part.getPathMappedUrl(return_only_folder=True) addMedia.append("File: ") splitFnAt = 74 fnLen = len(os.path.basename(part.file)) @@ -82,6 +83,8 @@ def getVideoInfo(self): appended = True continue addMedia.append("{}\n".format(s)) + if pmFolder: + addMedia.append("Mapped via: {}\n".format(pmFolder)) addMedia.append("Duration: {}, Size: {}\n".format(util.durationToShortText(int(part.duration)), util.simpleSize(int(part.size)))) diff --git a/script.plexmod/lib/windows/kodigui.py b/script.plexmod/lib/windows/kodigui.py index 40f466b7d..08e2e268d 100644 --- a/script.plexmod/lib/windows/kodigui.py +++ b/script.plexmod/lib/windows/kodigui.py @@ -118,19 +118,19 @@ def onInit(self): BaseFunctions.lastWinID = self._winID self.setProperty('use_solid_background', util.hasCustomBGColour and '1' or '') if util.hasCustomBGColour: - bgColour = util.advancedSettings.backgroundColour if util.advancedSettings.backgroundColour != "-" \ + bgColour = util.addonSettings.backgroundColour if util.addonSettings.backgroundColour != "-" \ else "ff000000" self.setProperty('background_colour', "0x%s" % bgColour.lower()) self.setProperty('background_colour_opaque', "0x%s" % bgColour.lower()) else: # set background color to 0 to avoid kodi UI BG clearing, improves performance - if util.advancedSettings.dbgCrossfade: + if util.addonSettings.dbgCrossfade: self.setProperty('background_colour', "0x00000000") else: self.setProperty('background_colour', "0xff111111") self.setProperty('background_colour_opaque', "0xff111111") - self.setBoolProperty('use_bg_fallback', util.advancedSettings.useBgFallback) + self.setBoolProperty('use_bg_fallback', util.addonSettings.useBgFallback) try: if self.started: @@ -151,9 +151,11 @@ def onFirstInit(self): def onReInit(self): pass - def waitForOpen(self): + def waitForOpen(self, base_win_id=None): tries = 0 - while not self.isOpen and not util.MONITOR.waitForAbort(2) and tries < 60: + while ((not base_win_id and not self.isOpen) or + (base_win_id and xbmcgui.getCurrentWindowId() <= base_win_id)) \ + and not util.MONITOR.waitForAbort(1) and tries < 120: if tries == 0: util.LOG("Couldn't open window {}, other dialog open? Retrying for 120s.".format(self)) self.show() @@ -177,12 +179,12 @@ def setProperty(self, key, value): xbmc.log('kodigui.BaseWindow.setProperty: Missing window', xbmc.LOGDEBUG) def updateBackgroundFrom(self, ds): - if util.advancedSettings.dynamicBackgrounds: + if util.addonSettings.dynamicBackgrounds: return self.windowSetBackground(util.backgroundFromArt(ds.art, width=self.width, height=self.height)) def windowSetBackground(self, value): - if not util.advancedSettings.dbgCrossfade: + if not util.addonSettings.dbgCrossfade: if not value: return self.setProperty("background_static", value) diff --git a/script.plexmod/lib/windows/library.py b/script.plexmod/lib/windows/library.py index 1ef8812da..2b9c77d98 100644 --- a/script.plexmod/lib/windows/library.py +++ b/script.plexmod/lib/windows/library.py @@ -348,7 +348,7 @@ def reset(self): self.alreadyFetchedChunkList = set() self.finalChunkPosition = 0 - self.CHUNK_SIZE = util.advancedSettings.libraryChunkSize + self.CHUNK_SIZE = util.addonSettings.libraryChunkSize key = self.section.key if not key.isdigit(): @@ -412,7 +412,7 @@ def onAction(self, action): if mli: self.requestChunk(mli.pos()) - if util.advancedSettings.dynamicBackgrounds: + if util.addonSettings.dynamicBackgrounds: if mli and mli.dataSource: self.updateBackgroundFrom(mli.dataSource) @@ -434,7 +434,7 @@ def onAction(self, action): elif action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)) and \ - (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): + (not util.addonSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): if xbmc.getCondVisibility('Integer.IsGreater(Container(101).ListItem.Property(index),5)'): self.showPanelControl.selectItem(0) return @@ -566,7 +566,7 @@ def playButtonClicked(self, shuffle=False): args['unwatched'] = '1' pq = playqueue.createPlayQueueForItem(self.section, options={'shuffle': shuffle}, args=args) - opener.open(pq) + opener.open(pq, auto_play=True) def shuffleButtonClicked(self): self.playButtonClicked(shuffle=True) @@ -786,7 +786,7 @@ def sortShowPanel(self, choice, force_refresh=False): self.showPanelControl.selectItem(0) self.setFocusId(self.POSTERS_PANEL_ID) self.backgroundSet = False - self.setBackground([item.dataSource for item in self.showPanelControl], 0, randomize=not util.advancedSettings.dynamicBackgrounds) + self.setBackground([item.dataSource for item in self.showPanelControl], 0, randomize=not util.addonSettings.dynamicBackgrounds) def subOptionCallback(self, option): check = 'script.plex/home/device/check.png' @@ -1156,7 +1156,7 @@ def fillShows(self): # 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: + if not util.addonSettings.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 @@ -1290,7 +1290,7 @@ def _chunkCallback(self, items, start): with self.lock: pos = start - self.setBackground(items, pos, randomize=not util.advancedSettings.dynamicBackgrounds) + self.setBackground(items, pos, randomize=not util.addonSettings.dynamicBackgrounds) thumbDim = TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie'])['thumb_dim'] artDim = TYPE_KEYS.get(self.section.type, TYPE_KEYS['movie']).get('art_dim', (256, 256)) @@ -1372,7 +1372,10 @@ def _chunkCallback(self, items, start): mli.setProperty('art', obj.artCompositeURL(*colArtDim)) mli.setThumbnailImage(obj.artCompositeURL(*thumbDim)) else: - mli.setThumbnailImage(obj.defaultThumb.asTranscodedImageURL(*thumbDim)) + if obj.TYPE == 'photodirectory' and obj.composite: + mli.setThumbnailImage(obj.composite.asTranscodedImageURL(*thumbDim)) + else: + mli.setThumbnailImage(obj.defaultThumb.asTranscodedImageURL(*thumbDim)) mli.dataSource = obj mli.setProperty('summary', obj.get('summary')) @@ -1397,7 +1400,7 @@ def _chunkCallback(self, items, start): pos += 1 def requestChunk(self, start): - if util.advancedSettings.retrieveAllMediaUpFront: + if util.addonSettings.retrieveAllMediaUpFront: return # Calculate the correct starting chunk position for the item they passed in diff --git a/script.plexmod/lib/windows/musicplayer.py b/script.plexmod/lib/windows/musicplayer.py index d2277eb5e..85d63afc2 100644 --- a/script.plexmod/lib/windows/musicplayer.py +++ b/script.plexmod/lib/windows/musicplayer.py @@ -49,6 +49,7 @@ def __init__(self, *args, **kwargs): self.album = kwargs.get('album') self.selectedOffset = 0 self.exitCommand = None + self.ignoreStopCommands = False if self.track: self.duration = self.track.duration.asInt() @@ -61,17 +62,41 @@ def onFirstInit(self): self.setupSeekbar() self.selectionBoxMax = self.SEEK_IMAGE_WIDTH - (self.selectionBoxHalf - 3) + player.PLAYER.on('starting.audio', self.onAudioStarting) + player.PLAYER.on('started.audio', self.onAudioStarted) + player.PLAYER.on('changed.audio', self.onAudioChanged) + self.updateProperties() self.play() self.setFocusId(406) def doClose(self, **kwargs): - player.PLAYER.off('playback.started', self.onPlayBackStarted) + player.PLAYER.off('av.started', self.onPlayBackStarted) if self.playlist and self.playlist.isRemote: self.playlist.off('change', self.updateProperties) + + player.PLAYER.off('starting.audio', self.onAudioStarting) + player.PLAYER.off('started.audio', self.onAudioStarted) + player.PLAYER.off('changed.audio', self.onAudioChanged) kodigui.ControlledWindow.doClose(self) + def onAudioStarting(self, *args, **kwargs): + util.setGlobalProperty('ignore_spinner', '1') + self.ignoreStopCommands = True + + def onAudioStarted(self, *args, **kwargs): + util.setGlobalProperty('ignore_spinner', '') + self.ignoreStopCommands = False + + def onAudioChanged(self, *args, **kwargs): + util.setGlobalProperty('ignore_spinner', '') + self.ignoreStopCommands = False + def onAction(self, action): + if self.ignoreStopCommands and action in (xbmcgui.ACTION_PREVIOUS_MENU, + xbmcgui.ACTION_NAV_BACK, + xbmcgui.ACTION_STOP): + return try: if action == xbmcgui.ACTION_STOP: self.stopButtonClicked() @@ -79,7 +104,7 @@ def onAction(self, action): except: util.ERROR() - super().onAction(action) + super(MusicPlayerWindow, self).onAction(action) def onClick(self, controlID): if controlID == self.PLAYLIST_BUTTON_ID: @@ -119,6 +144,7 @@ def skipPrevButtonClicked(self): if not self.playlist.refresh(force=True, wait=True): return + self.onAudioStarting() xbmc.executebuiltin('PlayerControl(Previous)') def skipNextButtonClicked(self): @@ -127,12 +153,14 @@ def skipNextButtonClicked(self): if not self.playlist.refresh(force=True, wait=True): return + self.onAudioStarting() xbmc.executebuiltin('PlayerControl(Next)') def showPlaylist(self): self.processCommand(opener.handleOpen(currentplaylist.CurrentPlaylistWindow, winID=xbmcgui.getCurrentWindowId())) def stopButtonClicked(self): + player.PLAYER.stopAndWait() self.doClose() def updateProperties(self, **kwargs): @@ -153,6 +181,8 @@ def play(self): if util.trackIsPlaying(self.track): return + self.onAudioStarting() + fanart = None if self.playlist: fanart = self.playlist.get('composite') or self.playlist.defaultArt diff --git a/script.plexmod/lib/windows/opener.py b/script.plexmod/lib/windows/opener.py index 591163d2a..9ea161bf1 100644 --- a/script.plexmod/lib/windows/opener.py +++ b/script.plexmod/lib/windows/opener.py @@ -14,7 +14,7 @@ def open(obj, **kwargs): return handleOpen(musicplayer.MusicPlayerWindow, track=obj.current(), playlist=obj) elif obj.type == 'photo': from . import photos - return handleOpen(photos.PhotoWindow, play_queue=obj) + return handleOpen(photos.PhotoWindow, play_queue=obj, **kwargs) else: from . import videoplayer videoplayer.play(play_queue=obj) diff --git a/script.plexmod/lib/windows/photos.py b/script.plexmod/lib/windows/photos.py index 7595ba7f5..ef94ccaf6 100644 --- a/script.plexmod/lib/windows/photos.py +++ b/script.plexmod/lib/windows/photos.py @@ -2,7 +2,6 @@ import threading import time import os -import tempfile import shutil import hashlib import requests @@ -15,7 +14,7 @@ from lib import util, colors from plexnet import plexapp, plexplayer, playqueue - +from plexnet import util as plexnetUtil class PhotoWindow(kodigui.BaseWindow): xmlFile = 'script-plex-photo.xml' @@ -49,6 +48,7 @@ class PhotoWindow(kodigui.BaseWindow): def __init__(self, *args, **kwargs): kodigui.BaseWindow.__init__(self, *args, **kwargs) self.photo = kwargs.get('photo') + self.autoPlay = False self.playQueue = kwargs.get('play_queue') self.playerObject = None self.timelineType = 'photo' @@ -87,6 +87,13 @@ def onFirstInit(self): self.osdTimer = kodigui.PropertyTimer(self._winID, 4, 'OSD', '', init_value=False, callback=self.osdTimerCallback) self.imageControl = self.getControl(600) + if self.autoPlay: + self.play() + + def doAutoPlay(self): + self.autoPlay = True + return True + def osdTimerCallback(self): self.setFocusId(self.OVERLAY_BUTTON_ID) @@ -221,7 +228,7 @@ def getPlayQueue(self, shuffle=False): if busy.widthDialog(self.playQueue.waitForInitialization, None, delay=True): util.DEBUG_LOG('playQueue initialized: {0}'.format(self.playQueue)) else: - util.DEBUG_LOG('playQueue timed out wating for initialization') + util.DEBUG_LOG('playQueue timed out waiting for initialization') self.showPhoto() @@ -522,7 +529,16 @@ def updateNowPlaying(self, force=False, refreshQueue=False, state=None): if refreshQueue and self.playQueue: self.playQueue.refreshOnTimeline = True - plexapp.util.APP.nowplayingmanager.updatePlaybackState(self.timelineType, self.playerObject, state, time, self.playQueue) + data = plexnetUtil.AttributeDict({ + "key": str(item.key), + "ratingKey": str(item.ratingKey), + "guid": str(item.guid), + "url": str(item.url), + "duration": item.duration.asInt(), + "containerKey": str(item.container.address) + }) + + plexapp.util.APP.nowplayingmanager.updatePlaybackState(self.timelineType, data, state, time, self.playQueue) def showOSD(self): self.osdTimer.reset(init=False) diff --git a/script.plexmod/lib/windows/playlist.py b/script.plexmod/lib/windows/playlist.py index 705fed372..087bb5c17 100644 --- a/script.plexmod/lib/windows/playlist.py +++ b/script.plexmod/lib/windows/playlist.py @@ -167,7 +167,7 @@ def playlistListClicked(self, no_item=False, shuffle=False): pq = plexnet.playqueue.createPlayQueueForItem(self.playlist, options=args) opener.open(pq) elif self.playlist.playlistType == 'video': - if not util.advancedSettings.playlistVisitMedia: + if not util.addonSettings.playlistVisitMedia: if self.playlist.leafCount.asInt() <= PLAYLIST_INITIAL_SIZE: self.playlist.setShuffle(shuffle) self.playlist.setCurrent(mli and mli.pos() or 0) diff --git a/script.plexmod/lib/windows/preplay.py b/script.plexmod/lib/windows/preplay.py index e72bae109..49a1b8747 100644 --- a/script.plexmod/lib/windows/preplay.py +++ b/script.plexmod/lib/windows/preplay.py @@ -141,7 +141,7 @@ def onAction(self, action): elif action == xbmcgui.ACTION_NAV_BACK: if (not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( self.OPTIONS_GROUP_ID)) or not controlID) and \ - not util.advancedSettings.fastBack: + not util.addonSettings.fastBack: if self.getProperty('on.extras'): self.setFocusId(self.OPTIONS_GROUP_ID) return diff --git a/script.plexmod/lib/windows/seekdialog.py b/script.plexmod/lib/windows/seekdialog.py index 1be1fd0f7..db1fe423d 100644 --- a/script.plexmod/lib/windows/seekdialog.py +++ b/script.plexmod/lib/windows/seekdialog.py @@ -2,12 +2,12 @@ import re import time import threading -import math from kodi_six import xbmc from kodi_six import xbmcgui from collections import OrderedDict +import lib.cache from . import kodigui from . import playersettings from . import dropdown @@ -17,7 +17,6 @@ from lib import util from plexnet.videosession import VideoSessionInfo, ATTRIBUTE_TYPES as SESSION_ATTRIBUTE_TYPES from plexnet.exceptions import ServerNotOwned, NotFound -from plexnet.signalsmixin import SignalsMixin from lib.kodijsonrpc import builtin @@ -171,7 +170,7 @@ def __init__(self, *args, **kwargs): self._delayedSeekTimeout = 0 self._osdHideAnimationTimeout = 0 self._hideDelay = self.HIDE_DELAY - self._autoSeekDelay = util.advancedSettings.autoSeek and util.advancedSettings.autoSeekDelay or 0 + self._autoSeekDelay = util.addonSettings.autoSeek and util.addonSettings.autoSeekDelay or 0 self._atSkipStep = -1 self._lastSkipDirection = None self._forcedLastSkipAmount = None @@ -203,8 +202,8 @@ def __init__(self, *args, **kwargs): self._creditsSkipShownStarted = None self._currentMarker = None self.skipSteps = self.SKIP_STEPS - self.useAutoSeek = util.advancedSettings.autoSeek - self.useDynamicStepsForTimeline = util.advancedSettings.dynamicTimelineSeek + self.useAutoSeek = util.addonSettings.autoSeek + self.useDynamicStepsForTimeline = util.addonSettings.dynamicTimelineSeek self.bingeMode = False self.autoSkipIntro = False @@ -212,14 +211,14 @@ def __init__(self, *args, **kwargs): self.showIntroSkipEarly = False self.skipPostPlay = False - self.skipIntroButtonTimeout = util.advancedSettings.skipIntroButtonTimeout - self.skipCreditsButtonTimeout = util.advancedSettings.skipCreditsButtonTimeout - self.showItemEndsInfo = util.advancedSettings.showMediaEndsInfo - self.showItemEndsLabel = util.advancedSettings.showMediaEndsLabel + self.skipIntroButtonTimeout = util.addonSettings.skipIntroButtonTimeout + self.skipCreditsButtonTimeout = util.addonSettings.skipCreditsButtonTimeout + self.showItemEndsInfo = util.addonSettings.showMediaEndsInfo + self.showItemEndsLabel = util.addonSettings.showMediaEndsLabel self.player.video.server.on("np:timelineResponse", self.timelineResponseCallback) - if util.kodiSkipSteps and util.advancedSettings.kodiSkipStepping: + if util.kodiSkipSteps and util.addonSettings.kodiSkipStepping: self.skipSteps = {"negative": [], "positive": []} for step in util.kodiSkipSteps: key = "negative" if step < 0 else "positive" @@ -290,7 +289,6 @@ def trueOffset(self): @property def markers(self): - # fixme: fix transcoded marker skip if not self._enableMarkerSkip: return None @@ -300,6 +298,10 @@ def markers(self): for marker in self.handler.player.video.markers: if marker.type in MARKERS: m = MARKERS[marker.type].copy() + marker.startTimeOffset = marker.startTimeOffset.asInt() \ + if not isinstance(marker.startTimeOffset, int) else marker.startTimeOffset + marker.endTimeOffset = marker.endTimeOffset.asInt() \ + if not isinstance(marker.endTimeOffset, int) else marker.endTimeOffset m["marker"] = marker m["marker_type"] = marker.type markers.append(m) @@ -312,6 +314,42 @@ def markers(self): def markers(self, val): self._markers = val + def getCurrentMarkerDef(self, offset=None): + """ + Show intro/credits skip button at current time + """ + + if not self.markers: + return + + off = offset if offset is not None else self.trueOffset() + + for markerDef in self.markers: + marker = markerDef["marker"] + if marker: + startTimeOffset = marker.startTimeOffset + + # show intro skip early? (only if intro is during the first X minutes) + if self.showIntroSkipEarly and markerDef["marker_type"] == "intro" and \ + startTimeOffset <= util.addonSettings.skipIntroButtonShowEarlyThreshold1 * 1000: + startTimeOffset = 0 + markerDef["overrideStartOff"] = 0 + + # fix markers with a bad endTimeOffset + if marker.endTimeOffset > self.duration: + marker.endTimeOffset = self.duration + util.DEBUG_LOG("Fixing marker endTimeOffset for: {}".format(marker)) + + # skip completely bad markers + if marker.startTimeOffset > self.duration: + continue + + markerEndNegoff = FINAL_MARKER_NEGOFF if getattr(markerDef["marker"], "final", False) else 0 + + if startTimeOffset - MARKER_SHOW_NEGOFF <= off < marker.endTimeOffset - markerEndNegoff: + + return markerDef + def onFirstInit(self): try: self._onFirstInit() @@ -384,15 +422,13 @@ def setup(self, duration, meta, offset=0, bif_url=None, title='', title2='', cha self.chapters = chapters or [] self.isDirectPlay = not meta.isTranscoded self.isTranscoded = not self.isDirectPlay - self.showChapters = util.getUserSetting('show_chapters', True) and ( - bool(chapters) or (util.getUserSetting('virtual_chapters', True) and bool(self.markers))) self.setProperty('video.title', title) self.setProperty('is.show', (self.player.video.type == 'episode') and '1' or '') self.setProperty('ep.year', (self.player.video.type == 'episode') and self.player.video.year or '') self.setProperty('has.playlist', self.handler.playlist and '1' or '') self.setProperty('shuffled', (self.handler.playlist and self.handler.playlist.isShuffled) and '1' or '') - self.setProperty('has.chapters', self.showChapters and '1' or '') - self.setProperty('show.buffer', (util.advancedSettings.playerShowBuffer and self.isDirectPlay) and '1' or '') + self.setProperty('show.buffer', (util.addonSettings.playerShowBuffer and self.isDirectPlay) and '1' or '') + self.setProperty('theme', 'modern') self.killTimeKeeper() @@ -434,6 +470,11 @@ def setup(self, duration, meta, offset=0, bif_url=None, title='', title2='', cha except IndexError: self.doClose(delete=True) raise util.NoDataException + + self.showChapters = util.getUserSetting('show_chapters', True) and ( + bool(chapters) or (util.getUserSetting('virtual_chapters', True) and bool(self.markers))) + self.setProperty('has.chapters', self.showChapters and '1' or '') + self.baseOffset = offset self.offset = 0 self.idleTime = None @@ -513,13 +554,15 @@ def onAction(self, action): if markerDef["marker"]: marker = markerDef["marker"] final = getattr(marker, "final", False) - markerOff = 0 if final else MARKER_END_JUMP_OFF + markerOff = -FINAL_MARKER_NEGOFF if final else MARKER_END_JUMP_OFF - util.DEBUG_LOG('MarkerSkip: Skipping marker {}'.format(markerDef["marker"])) + util.DEBUG_LOG('MarkerSkip: Skipping marker' + ' {} (final: {}, to: {}, offset: {})'.format(markerDef["marker"], + final, marker.endTimeOffset, markerOff)) self.setProperty('show.markerSkip', '') self.setProperty('show.markerSkip_OSDOnly', '') markerDef["skipped"] = True - self.doSeek(math.ceil(float(marker.endTimeOffset)) + markerOff) + self.doSeek(marker.endTimeOffset + markerOff) self.hideOSD(skipMarkerFocus=True) if marker.type == "credits" and not final: @@ -663,16 +706,16 @@ def onAction(self, action): # immediate marker timer actions if self.countingDownMarker: if controlID != self.BIG_SEEK_LIST_ID and \ - (util.advancedSettings.skipMarkerTimerCancel - or util.advancedSettings.skipMarkerTimerImmediate): - if util.advancedSettings.skipMarkerTimerCancel and \ + (util.addonSettings.skipMarkerTimerCancel + or util.addonSettings.skipMarkerTimerImmediate): + if util.addonSettings.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 \ + elif util.addonSettings.skipMarkerTimerImmediate \ and action == xbmcgui.ACTION_SELECT_ITEM and \ self._currentMarker["countdown"] is not None and \ self._currentMarker["countdown_initial"] is not None and \ @@ -779,7 +822,7 @@ def onClick(self, controlID): if not self._seeking: # we might be reacting to an immediate marker skip while showing a marker with timeout; # in that case, don't show the OSD - if not self._currentMarker or not util.advancedSettings.skipMarkerTimerImmediate or \ + if not self._currentMarker or not util.addonSettings.skipMarkerTimerImmediate or \ self._currentMarker["countdown"] is None: self.showOSD() else: @@ -1123,7 +1166,8 @@ def optionsButtonClicked(self): # Button currently commented out. def subtitleButtonClicked(self): options = [] - options.append({'key': 'download', 'display': T(32405, 'Download Subtitles')}) + if self.isDirectPlay: + options.append({'key': 'download', 'display': T(32405, 'Download Subtitles')}) # select "enable" by default selectIndex = 1 @@ -1177,10 +1221,14 @@ def subtitleButtonClicked(self): if self.handler and self.handler.player and self.handler.player.playerObject \ and util.getSetting('calculate_oshash', False): meta = self.handler.player.playerObject.metadata - oss_hash = util.getOpenSubtitlesHash(meta.size, meta.streamUrls[0]) - if oss_hash: - util.DEBUG_LOG("OpenSubtitles hash: %s" % oss_hash) - util.setGlobalProperty("current_oshash", oss_hash, base='videoinfo.{0}') + if not meta.size: + util.LOG("Can't calculate OpenSubtitles hash because we're transcoding") + + else: + oss_hash = util.getOpenSubtitlesHash(meta.size, meta.streamUrls[0]) + if oss_hash: + util.DEBUG_LOG("OpenSubtitles hash: %s" % oss_hash) + util.setGlobalProperty("current_oshash", oss_hash, base='videoinfo.{0}') else: util.setGlobalProperty("current_oshash", '', base='videoinfo.{0}') self.lastSubtitleNavAction = "download" @@ -1213,6 +1261,8 @@ def toggleSubtitles(self): def disableSubtitles(self): self.player.video.disableSubtitles() self.setSubtitles() + if self.isTranscoded: + self.doSeek(self.trueOffset(), settings_changed=True) def cycleSubtitles(self, forward=True): """ @@ -1221,6 +1271,8 @@ def cycleSubtitles(self, forward=True): stream = self.player.video.cycleSubtitles(forward=forward) self.setSubtitles(honor_forced_subtitles_override=False) util.showNotification(str(stream), time_ms=1500, header=util.T(32396, "Subtitles")) + if self.isTranscoded: + self.doSeek(self.trueOffset(), settings_changed=True) def setSubtitles(self, do_sleep=False, honor_forced_subtitles_override=False): self.handler.setSubtitles(do_sleep=do_sleep, honor_forced_subtitles_override=honor_forced_subtitles_override) @@ -1315,6 +1367,7 @@ def updateProperties(self, **kwargs): self.setProperty('is.show', (self.player.video.type == 'episode') and '1' or '') self.setProperty('media.show_ends', self.showItemEndsInfo and '1' or '') self.setProperty('time.ends_label', self.showItemEndsLabel and (util.T(32543, 'Ends at')) or '') + self.setBoolProperty('no.osd.hide_info', util.getSetting('no_spoilers', False)) if self.isDirectPlay: self.setProperty('time.fmt', self.timeFmtKodi) @@ -1392,8 +1445,8 @@ def updateChapters(self): marker = markerDef["marker"] if marker: if markerDef["marker_type"] == "intro": - preparedMarkers.append((int(marker.startTimeOffset), T(33608, "Intro"), False)) - preparedMarkers.append((int(marker.endTimeOffset), T(33610, "Main"), False)) + preparedMarkers.append((marker.startTimeOffset, T(33608, "Intro"), False)) + preparedMarkers.append((marker.endTimeOffset, T(33610, "Main"), False)) elif markerDef["marker_type"] == "credits": creditsCounter += 1 @@ -1401,7 +1454,7 @@ def updateChapters(self): label = T(33635, "Final Credits") else: label = T(33609, "Credits") + "{}" - preparedMarkers.append((int(marker.startTimeOffset), label, True)) + preparedMarkers.append((marker.startTimeOffset, label, True)) # add staggered virtual markers preparedMarkers.append((int(self.duration * 0.25), "25 %", False)) @@ -1458,7 +1511,7 @@ def updateCurrent(self, update_position_control=True, atOffset=None): self.positionControl.setWidth(w) # update cache/buffer bar - if util.advancedSettings.playerShowBuffer and self.isDirectPlay and util.KODI_VERSION_MAJOR > 18: + if util.addonSettings.playerShowBuffer and self.isDirectPlay and util.KODI_VERSION_MAJOR > 18: cache_w = int(xbmc.getInfoLabel("Player.ProgressCache")) * self.SEEK_IMAGE_WIDTH // 100 self.cacheControl.setWidth(cache_w) @@ -1554,33 +1607,6 @@ def seekMouse(self, action, without_osd=False, preview=False): self.updateProgress(set_to_current=False) self.setProperty('button.seek', '1') - def getCurrentMarkerDef(self, offset=None): - """ - Show intro/credits skip button at current time - """ - - if not self.markers: - return - - off = offset if offset is not None else self.trueOffset() - - for markerDef in self.markers: - marker = markerDef["marker"] - if marker: - startTimeOffset = int(marker.startTimeOffset) - - # show intro skip early? (only if intro is during the first X minutes) - if self.showIntroSkipEarly and markerDef["marker_type"] == "intro" and \ - startTimeOffset <= util.advancedSettings.skipIntroButtonShowEarlyThreshold1 * 1000: - startTimeOffset = 0 - markerDef["overrideStartOff"] = 0 - - markerEndNegoff = FINAL_MARKER_NEGOFF if getattr(markerDef["marker"], "final", False) else 0 - - if startTimeOffset - MARKER_SHOW_NEGOFF <= off < int(marker.endTimeOffset) - markerEndNegoff: - - return markerDef - @property def duration(self): @@ -1672,7 +1698,7 @@ def waitForBuffer(self): currentBufferPerc = int(xbmc.getInfoLabel("Player.ProgressCache")) - int(xbmc.getInfoLabel("Player.Progress")) # configured buffer size - bufferBytes = util.kcm.memorySize * 1024 * 1024 + bufferBytes = lib.cache.kcm.memorySize * 1024 * 1024 # wait for the full buffer or for 10% of the file at max # a full buffer is typically 30% of the configured cache value @@ -1693,7 +1719,7 @@ def waitForBuffer(self): wasPlaying = True waitedFor = 0 - waitMax = util.advancedSettings.bufferWaitMax + waitMax = util.addonSettings.bufferWaitMax waitExceeded = False self.waitingForBuffer = True self.showOSD(focusButton=False) @@ -1744,7 +1770,7 @@ def waitForBuffer(self): util.DEBUG_LOG("SeekDialog.buffer: Buffer already filled, not waiting for buffer") else: - wait = util.advancedSettings.bufferInsufficientWait + wait = util.addonSettings.bufferInsufficientWait util.DEBUG_LOG("SeekDialog.buffer: Buffer is too small for us to see, waiting {} seconds".format(wait)) self.waitingForBuffer = True @@ -1905,7 +1931,7 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F # getCurrentMarkerDef might have overridden the startTimeOffset, use that startTimeOff = markerDef["overrideStartOff"] if markerDef["overrideStartOff"] is not None else \ - int(markerDef["marker"].startTimeOffset) + markerDef["marker"].startTimeOffset markerAutoSkip = getattr(self, markerDef["markerAutoSkip"]) @@ -1916,14 +1942,14 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F markerAutoSkipped = markerDef["markerAutoSkipped"] - sTOffWThres = startTimeOff + util.advancedSettings.autoSkipOffset * 1000 + sTOffWThres = startTimeOff + util.addonSettings.autoSkipOffset * 1000 # we just want to return an early marker if we want to autoSkip it, so we can tell the handler to seekOnStart if onlyReturnIntroMD and markerDef["marker_type"] == "intro" and markerAutoSkip: if startTimeOff == 0 and not markerDef["markerAutoSkipped"]: if setSkipped: markerDef["markerAutoSkipped"] = True - return int(markerDef["marker"].endTimeOffset) + MARKER_END_JUMP_OFF + return markerDef["marker"].endTimeOffset + MARKER_END_JUMP_OFF return False if cancelTimer and self.countingDownMarker: @@ -1953,8 +1979,8 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F if getattr(markerDef["marker"], "final", False): # final marker is _not_ at the end of video, seek and do nothing - if int(markerDef["marker"].endTimeOffset) < self.duration - FINAL_MARKER_NEGOFF: - target = int(markerDef["marker"].endTimeOffset) + if markerDef["marker"].endTimeOffset < self.duration - FINAL_MARKER_NEGOFF: + target = markerDef["marker"].endTimeOffset util.DEBUG_LOG( "MarkerAutoSkip: Skipping final marker, its endTime is too early, " "though, seeking and playing back") @@ -1982,7 +2008,7 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F return False util.DEBUG_LOG('MarkerAutoSkip: Skipping marker {}'.format(markerDef["marker"])) - self.doSeek(int(markerDef["marker"].endTimeOffset) + MARKER_END_JUMP_OFF) + self.doSeek(markerDef["marker"].endTimeOffset + MARKER_END_JUMP_OFF) return True # got a marker, display logic @@ -2014,13 +2040,22 @@ def displayMarkers(self, cancelTimer=False, immediate=False, onlyReturnIntroMD=F # 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)) + to = self.trueOffset() + # set the countdown to either the auto skip offset, or, if we're already "inside" the marker time + # area through seeking, at max the difference between the current offset and the end of the + # video + markerDef["countdown"] = int( + max( + round((sTOffWThres - to) / 1000.0) + 1, + min(util.addonSettings.autoSkipOffset, int((self.duration - to) / 1000.0)) + ) + ) isNew = True if self.player.playState == self.player.STATE_PLAYING and not self.osdVisible(): markerDef["countdown"] -= 1 - if isNew: - markerDef["countdown_initial"] = markerDef["countdown"] + if isNew: + markerDef["countdown_initial"] = markerDef["countdown"] self.setProperty('marker.countdown', '1') diff --git a/script.plexmod/lib/windows/settings.py b/script.plexmod/lib/windows/settings.py index 841cae917..5b1d8a011 100644 --- a/script.plexmod/lib/windows/settings.py +++ b/script.plexmod/lib/windows/settings.py @@ -2,6 +2,8 @@ from kodi_six import xbmc from kodi_six import xbmcgui from kodi_six import xbmcvfs + +import lib.cache from . import kodigui from . import windowutils @@ -150,7 +152,7 @@ def optionIndex(self): class BufferSetting(OptionsSetting): def get(self): - return util.kcm.memorySize + return lib.cache.kcm.memorySize def set(self, val): old = self.get() @@ -158,12 +160,12 @@ def set(self, val): util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.ID, old, val)) plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val) - util.kcm.write(memorySize=val) + lib.cache.kcm.write(memorySize=val) class ReadFactorSetting(OptionsSetting): def get(self): - return util.kcm.readFactor + return lib.cache.kcm.readFactor def set(self, val): old = self.get() @@ -171,7 +173,7 @@ def set(self, val): util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]'.format(self.ID, old, val)) plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val) - util.kcm.write(readFactor=val) + lib.cache.kcm.write(readFactor=val) class InfoSetting(BasicSetting): @@ -263,6 +265,11 @@ class Settings(object): "only those likely affected. Use this if you find a hub that doesn't update properly." ) ), + BoolSetting( + 'hubs_use_new_continue_watching', T(32998, ''), False + ).description( + T(32999, "") + ), BoolSetting( 'hubs_bifurcation_lines', T(32961, 'Show hub bifurcation lines'), False ).description( @@ -359,6 +366,22 @@ class Settings(object): ), 'player': ( T(32940, 'Player UI'), ( + OptionsSetting( + 'theme', + T(32983, 'Player Theme'), + util.DEF_THEME, + ( + ('modern', T(32985, 'Modern')), + ('modern-dotted', T(32986, 'Modern (dotted)')), + ('modern-colored', T(32989, 'Modern (colored)')), + ('classic', T(32987, 'Classic')), + ('custom', T(32988, 'Custom')), + ) + ).description( + T(32984, 'stub') + ), + BoolSetting('no_spoilers', T(33004, ''), False).description( + T(33005, '')), BoolSetting('subtitle_downloads', T(32932, 'Show subtitle quick-actions button'), False).description( T(32939, 'Only applies to video player UI')), BoolSetting('video_show_ffwdrwd', T(32933, 'Show FFWD/RWD buttons'), False).description( @@ -467,6 +490,12 @@ class Settings(object): ) ), BoolSetting('gdm_discovery', T(32042, 'Server Discovery (GDM)'), False), + OptionsSetting( + 'handle_plexdirect', T(32990), 'ask', + (('ask', T(32991)), ('always', T(32035)), ('never', T(32033))) + ).description( + T(32992, 'stub') + ), IPSetting('manual_ip_0', T(32044, 'Connection 1 IP'), ''), IntegerSetting('manual_port_0', T(32045, 'Connection 1 Port'), 32400), IPSetting('manual_ip_1', T(32046, 'Connection 2 IP'), ''), @@ -479,25 +508,26 @@ class Settings(object): BoolSetting('kiosk.mode', T(32043, 'Start Plex On Kodi Startup'), False), BoolSetting('exit_default_is_quit', T(32965, 'Start Plex On Kodi Startup'), False) .description(T(32966, "stub")), + BoolSetting('path_mapping', T(33000, ''), True).description(T(33001, '')), BufferSetting('cache_size', T(33613, 'Kodi Buffer Size (MB)'), 20, - [(mem, '{} MB'.format(mem)) for mem in util.kcm.viableOptions]) + [(mem, '{} MB'.format(mem)) for mem in lib.cache.kcm.viableOptions]) .description( '{}{}'.format(T(33614, 'stub1').format( - util.kcm.free, util.kcm.recMax), - '' if util.kcm.useModernAPI else ' '+T(32954, 'stub2')) + lib.cache.kcm.free, lib.cache.kcm.recMax), + '' if lib.cache.kcm.useModernAPI else ' ' + T(32954, 'stub2')) ), ReadFactorSetting('readfactor', T(32922, 'Kodi Cache Readfactor'), 4, - [(rf, str(rf) if rf > 0 else T(32976, 'stub')) for rf in util.kcm.readFactorOpts]) + [(rf, str(rf) if rf > 0 else T(32976, 'stub')) for rf in lib.cache.kcm.readFactorOpts]) .description( T(32923, '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.').format(util.kcm.defRF, - util.kcm.recRFRange, - util.kcm.defRFSM) + 'fill fast/aggressively enough.').format(lib.cache.kcm.defRF, + lib.cache.kcm.recRFRange, + lib.cache.kcm.defRFSM) ), BoolSetting( 'slow_connection', T(32915, 'Slow connection'), False diff --git a/script.plexmod/lib/windows/slidehshow.py b/script.plexmod/lib/windows/slidehshow.py index d25f8b670..efb0b0df1 100644 --- a/script.plexmod/lib/windows/slidehshow.py +++ b/script.plexmod/lib/windows/slidehshow.py @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): self.timeBetweenImages = self.TIME_BETWEEN_IMAGES self.timeBetweenDisplayMove = self.TIME_DISPLAY_MOVE self.timeTitleIsHidden = self.TIME_HIDE_TITLE_IN_QUIZ - self.quizMode = util.advancedSettings.screensaverQuiz + self.quizMode = util.addonSettings.screensaverQuiz self.initialized = False def onFirstInit(self): diff --git a/script.plexmod/lib/windows/subitems.py b/script.plexmod/lib/windows/subitems.py index 915ec15f1..4cac3536a 100644 --- a/script.plexmod/lib/windows/subitems.py +++ b/script.plexmod/lib/windows/subitems.py @@ -136,7 +136,7 @@ def updateProperties(self): elif self.mediaItem.studio: self.setProperty('directors', u'{0} {1}'.format(T(32386, 'Studio').upper(), self.mediaItem.studio)) - cast = u' / '.join([r.tag for r in self.mediaItem.roles()][:5]) + cast = self.mediaItem.roles and u' / '.join([r.tag for r in self.mediaItem.roles()][:5]) or '' castLabel = T(32419, 'Cast').upper() self.setProperty('writers', cast and u'{0} {1}'.format(castLabel, cast) or '') @@ -188,7 +188,7 @@ def onAction(self, action): elif action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format( self.OPTIONS_GROUP_ID)) and \ - (not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): + (not util.addonSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU): if self.getProperty('on.extras'): self.setFocusId(self.OPTIONS_GROUP_ID) return diff --git a/script.plexmod/lib/windows/tracks.py b/script.plexmod/lib/windows/tracks.py index 90dc43043..de7e511fd 100644 --- a/script.plexmod/lib/windows/tracks.py +++ b/script.plexmod/lib/windows/tracks.py @@ -3,7 +3,7 @@ from kodi_six import xbmcgui from . import kodigui -from lib import colors +from lib import player from lib import util from plexnet import playlist diff --git a/script.plexmod/lib/windows/userselect.py b/script.plexmod/lib/windows/userselect.py index 656e70553..12b475355 100644 --- a/script.plexmod/lib/windows/userselect.py +++ b/script.plexmod/lib/windows/userselect.py @@ -82,7 +82,7 @@ def onClick(self, controlID): with self.propertyContext('busy'): self.userList.reset() self.setProperty('initialized', '') - plexapp.ACCOUNT.updateHomeUsers() + plexapp.ACCOUNT.updateHomeUsers(refreshSubscription=True) self.start(with_busy=False) else: self.userSelected(item) @@ -216,8 +216,10 @@ def finished(self): self.task.cancel() -def start(): - w = UserSelectWindow.open() +def start(base_win_id): + w = UserSelectWindow.create() + if w.waitForOpen(base_win_id=base_win_id): + w.modal() selected = w.selected del w return selected diff --git a/script.plexmod/lib/windows/videoplayer.py b/script.plexmod/lib/windows/videoplayer.py index a67e5018d..fc0269e1b 100644 --- a/script.plexmod/lib/windows/videoplayer.py +++ b/script.plexmod/lib/windows/videoplayer.py @@ -152,7 +152,7 @@ def onAction(self, action): self.resetPassoutProtection() if action in(xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_CONTEXT_MENU): if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.OPTIONS_GROUP_ID)): - if not util.advancedSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU: + if not util.addonSettings.fastBack or action == xbmcgui.ACTION_CONTEXT_MENU: self.lastNonOptionsFocusID = self.lastFocusID self.setFocusId(self.OPTIONS_GROUP_ID) return @@ -195,7 +195,7 @@ def onClick(self, controlID): return timeoutCanceled = False - if util.advancedSettings.postplayCancel: + if util.addonSettings.postplayCancel: timeoutCanceled = bool(self.timeout) self.cancelTimer() @@ -422,7 +422,7 @@ def startTimer(self): millis = (self.passoutProtection - time.time()) * 1000 util.DEBUG_LOG('Post play auto-play: Passout protection in {0}'.format(util.durationToShortText(millis))) - self.timeout = time.time() + abs(util.advancedSettings.postplayTimeout) + self.timeout = time.time() + abs(util.addonSettings.postplayTimeout) util.DEBUG_LOG('Starting post-play timer until: %i' % self.timeout) threading.Thread(target=self.countdown).start() @@ -447,8 +447,8 @@ def countdown(self): # self.playVideo() break elif self.timeout is not None: - cd = min(abs(util.advancedSettings.postplayTimeout-1), int((self.timeout or now) - now)) - base = 15 / float(util.advancedSettings.postplayTimeout-1) + cd = min(abs(util.addonSettings.postplayTimeout - 1), int((self.timeout or now) - now)) + base = 15 / float(util.addonSettings.postplayTimeout - 1) self.setProperty('countdown', str(int(math.ceil(base*cd)))) def getHubs(self): 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 e40ba633f..37a347e3d 100644 --- a/script.plexmod/resources/language/resource.language.de_de/strings.po +++ b/script.plexmod/resources/language/resource.language.de_de/strings.po @@ -1594,8 +1594,8 @@ msgid "Kodi Buffer Size (MB)" msgstr "Kodi Puffergröße (MB)" msgctxt "#33614" -msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~100 MB, Recommended max: {} MB, Default: 20 MB." -msgstr "Setzt die Kodi Cache/Puffer Größe. Frei: {} MB, empfohlen: ~100 MB, empfohlenes Max.: {} MB, Default: 20 MB." +msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~50 MB, Recommended max: {} MB, Default: 20 MB." +msgstr "Setzt die Kodi Cache/Puffer Größe. Frei: {} MB, empfohlen: ~50 MB, empfohlenes Max.: {} MB, Default: 20 MB." msgctxt "#33615" msgid "{time} left" @@ -2028,3 +2028,103 @@ msgstr "Erlaubt dem Server nur Streams eines Videos zu transkodieren, die dies b msgctxt "#32980" msgid "Refresh Users" msgstr "Benutzer aktual." + +msgctxt "#32981" +msgid "Background worker count" +msgstr "Anzahl Hintergrund-Worker" + +msgctxt "#32982" +msgid "Depending on how many cores your CPU has and how much it can handle, increasing this might improve certain situations. If you experience crashes or other annormalities, leave this at its default (3). Needs an addon restart." +msgstr "Je nachdem, wie viele Kerne Deine CPU hat und wie viel sie verarbeiten kann, kann eine höhere Zahl bestimmte Situationen verbessern. Solltest Du Abstürze oder andere Annomalien bemerken, stelle dies auf den Standardwert zurück (3). Benötigt einen Addon-Neustart." + +msgctxt "#32983" +msgid "Player Theme" +msgstr "Player-Theme" + +msgctxt "#32984" +msgid "Sets the player theme. Currently only customizes the playback control buttons. ATTENTION: [I]Might[/I] need an addon restart.\nIn order to customize this, copy one of the xml's in script.plexmod/resources/skins/Main/1080i/templates to addon_data/script.plexmod/templates/seek_dialog_buttons_custom.xml and adjust it to your liking, then select \"Custom\" as your theme." +msgstr "Wählt das Theme des Players. Verändert aktuell nur die Abspiel-Knöpfe. ACHTUNG: Ein Addon-Neustart [I]könnte[/I] notwendig sein.\nUm dies individuell anzupassen, kopiere eine der xml's aus script.plexmod/resources/skins/Main/1080i/templates nach addon_data/script.plexmod/templates/seek_dialog_buttons_custom.xml und passe es nach Deinen Ansprüchen an, danach \"Individualisiert\" als Theme wählen." + +msgctxt "#32985" +msgid "Modern" +msgstr "Modern" + +msgctxt "#32986" +msgid "Modern (dotted)" +msgstr "Modern (gepunktet)" + +msgctxt "#32987" +msgid "Classic" +msgstr "Klassisch" + +msgctxt "#32988" +msgid "Custom" +msgstr "Individualisiert" + +msgctxt "#32989" +msgid "Modern (colored)" +msgstr "Modern (eingefärbt)" + +msgctxt "#32990" +msgid "Handle plex.direct mapping" +msgstr "plex.direct Zuordnung abwickeln" + +msgctxt "#32991" +msgid "Notify" +msgstr "Benachrichtigen" + +msgctxt "#32992" +msgid "When using servers with a plex.direct connection (most of them), should we automatically adjust advancedsettings.xml to cope with plex.direct domains? If not, you might want to add plex.direct to your router's DNS rebind exemption list." +msgstr "Wenn Server mit einer plex.direct Verbindung verwendet werden (die meisten), sollen wir automatisch die advancedsettings.xml anpassen? Wenn nicht, solltest Du plex.direct in die DNS Rebind Ausschlussliste Deines Routers eintragen." + +msgctxt "#32993" +msgid "{} unhandled plex.direct connections found: {}" +msgstr "{} unbehandelte plex.direct Verbindungen gefunden" + +msgctxt "#32994" +msgid "In order for PM4K to work properly, we need to add special handling for plex.direct connections. We've found {} new unhandled connections. Do you want us to write those to Kodi's advancedsettings.xml automatically? If not, you might want to add plex.direct to your router's DNS rebind exemption list. This can be changed in the settings as well." +msgstr "Damit PM4K korrekt funktioniert, müssen wir spezielles Handling für plex.direct Verbindungen einrichten. Es wurden {} neue unbehandelte Verbindungen gefunden. Sollen wir diese in Kodi's advancedsettings.xml eintragen? Wenn nicht, solltest Du plex.direct in die DNS Rebind Ausschlussliste Deines Routers eintragen. Dies kann später in den Einstellungen verändert werden." + +msgctxt "#32995" +msgid "Advancedsettings.xml modified (plex.direct mappings)" +msgstr "Advancedsettings.xml modifiziert (plex.direct mappings)" + +msgctxt "#32996" +msgid "The advancedsettings.xml file has been modified. Please restart Kodi for them to take effect." +msgstr "Die advancedsettings.xml-Datei wurde modifiziert. Bitte starte Kodi neu, damit die Änderungen in Effekt treten." + +msgctxt "#32997" +msgid "OK" +msgstr "OK" + +msgctxt "#32998" +msgid "Use new Continue Watching hub on Home" +msgstr "Neuen \"Fortsetzen\" Home-Hub verwenden" + +msgctxt "#32999" +msgid "Instead of separating Continue Watching and On Deck hubs, behave like the modern Plex clients, which combine those two types of hubs into one Continue Watching hub." +msgstr "Anstatt die separaten Fortsetzen und " + +msgctxt "#33000" +msgid "Enable path mapping" +msgstr "Path mapping aktivieren" + +msgctxt "#33001" +msgid "Honor path_mapping.json in the addon_data/script.plexmod folder when DirectPlaying media. This can be used to stream using other techniques such as SMB/NFS/etc. instead of the default HTTP handler. path_mapping.example.json is included in the addon's main directory." +msgstr "Lese path_mapping.json im addon_data/script.plexmod Ordner wenn etwas DirectPlayed wird. Dies kann verwendet werden, um andere Techniken beim Streamen zu benutzen, wie z. B. SMB/NFS/etc., anstatt des HTTP handlers. path_mapping.example.json liegt im Hauptverzeichnis vom Addon." + +msgctxt "#33002" +msgid "Verify mapped files exist" +msgstr "Gemappte Dateien verifizieren" + +msgctxt "#33003" +msgid "When path mapping is enabled and we've successfully mapped a file, verify its existence." +msgstr "Wenn path mapping aktiviert ist und wir erfolgreich eine Datei gemappt haben, auch ihre Existenz verifizieren." + +msgctxt "#33004" +msgid "No spoilers without OSD" +msgstr "Keine Spoiler ohne OSD" + +msgctxt "#33005" +msgid "When seeking without the OSD open, hide all time-related information from the user." +msgstr "Wenn ohne OSD gesprungen wird, alle zeitrelevanten Informationen verstecken." 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 3125aff8b..d942912c3 100644 --- a/script.plexmod/resources/language/resource.language.en_gb/strings.po +++ b/script.plexmod/resources/language/resource.language.en_gb/strings.po @@ -1295,7 +1295,7 @@ msgid "Kodi Buffer Size (MB)" msgstr "" msgctxt "#33614" -msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~100 MB, Recommended max: {} MB, Default: 20 MB." +msgid "Set the Kodi Cache/Buffer size. Free: {} MB, Recommended: ~50 MB, Recommended max: {} MB, Default: 20 MB." msgstr "" msgctxt "#33615" @@ -1729,3 +1729,103 @@ msgstr "" msgctxt "#32980" msgid "Refresh Users" msgstr "" + +msgctxt "#32981" +msgid "Background worker count" +msgstr "" + +msgctxt "#32982" +msgid "Depending on how many cores your CPU has and how much it can handle, increasing this might improve certain situations. If you experience crashes or other annormalities, leave this at its default (3). Needs an addon restart." +msgstr "" + +msgctxt "#32983" +msgid "Player Theme" +msgstr "" + +msgctxt "#32984" +msgid "Sets the player theme. Currently only customizes the playback control buttons. ATTENTION: [I]Might[/I] need an addon restart.\nIn order to customize this, copy one of the xml's in script.plexmod/resources/skins/Main/1080i/templates to addon_data/script.plexmod/templates/seek_dialog_buttons_custom.xml and adjust it to your liking, then select \"Custom\" as your theme." +msgstr "" + +msgctxt "#32985" +msgid "Modern" +msgstr "" + +msgctxt "#32986" +msgid "Modern (dotted)" +msgstr "" + +msgctxt "#32987" +msgid "Classic" +msgstr "" + +msgctxt "#32988" +msgid "Custom" +msgstr "" + +msgctxt "#32989" +msgid "Modern (colored)" +msgstr "" + +msgctxt "#32990" +msgid "Handle plex.direct mapping" +msgstr "" + +msgctxt "#32991" +msgid "Notify" +msgstr "" + +msgctxt "#32992" +msgid "When using servers with a plex.direct connection (most of them), should we automatically adjust advancedsettings.xml to cope with plex.direct domains? If not, you might want to add plex.direct to your router's DNS rebind exemption list." +msgstr "" + +msgctxt "#32993" +msgid "{} unhandled plex.direct connections found" +msgstr "" + +msgctxt "#32994" +msgid "In order for PM4K to work properly, we need to add special handling for plex.direct connections. We've found {} new unhandled connections. Do you want us to write those to Kodi's advancedsettings.xml automatically? If not, you might want to add plex.direct to your router's DNS rebind exemption list. This can be changed in the settings as well." +msgstr "" + +msgctxt "#32995" +msgid "Advancedsettings.xml modified (plex.direct mappings)" +msgstr "" + +msgctxt "#32996" +msgid "The advancedsettings.xml file has been modified. Please restart Kodi for the changes to apply." +msgstr "" + +msgctxt "#32997" +msgid "OK" +msgstr "" + +msgctxt "#32998" +msgid "Use new Continue Watching hub on Home" +msgstr "" + +msgctxt "#32999" +msgid "Instead of separating Continue Watching and On Deck hubs, behave like the modern Plex clients, which combine those two types of hubs into one Continue Watching hub." +msgstr "" + +msgctxt "#33000" +msgid "Enable path mapping" +msgstr "" + +msgctxt "#33001" +msgid "Honor path_mapping.json in the addon_data/script.plexmod folder when DirectPlaying media. This can be used to stream using other techniques such as SMB/NFS/etc. instead of the default HTTP handler. path_mapping.example.json is included in the addon's main directory." +msgstr "" + +msgctxt "#33002" +msgid "Verify mapped files exist" +msgstr "" + +msgctxt "#33003" +msgid "When path mapping is enabled and we've successfully mapped a file, verify its existence." +msgstr "" + +msgctxt "#33004" +msgid "No spoilers without OSD" +msgstr "" + +msgctxt "#33005" +msgid "When seeking without the OSD open, hide all time-related information from the user." +msgstr "" diff --git a/script.plexmod/resources/settings.xml b/script.plexmod/resources/settings.xml index b994c848f..20ff630fa 100644 --- a/script.plexmod/resources/settings.xml +++ b/script.plexmod/resources/settings.xml @@ -324,9 +324,14 @@ + + 0 + true + + 0 - 5 + 10 0.1 0.1 @@ -360,6 +365,18 @@ false + + 0 + 3 + + 2 + 1 + 32 + + + false + + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml index f32092dc3..a72c4da39 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-album.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -597,9 +597,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml index 8d1dac581..df25674fc 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-artist.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -737,9 +737,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml index 7d28bf2fb..8dcf4816c 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-background.xml @@ -13,7 +13,7 @@ 710 459 500 - 162 + 232.5 script.plex/splash.png @@ -23,7 +23,7 @@ 812 135 300 - 97 + 139 script.plex/user_select/plex.png 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 38fe38352..e1b9f72fc 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-episodes.xml @@ -10,12 +10,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -23,7 +23,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -31,7 +31,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -310,7 +310,7 @@ horizontal true - auto + auto 60 font13 left @@ -1923,9 +1923,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png 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 2817f0deb..d2caf4ced 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-home.xml @@ -10,12 +10,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -23,7 +23,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -31,7 +31,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -7989,9 +7989,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml index b4d1deef7..0b690fea3 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-info.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -278,9 +278,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml deleted file mode 100644 index b209ee461..000000000 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9-chunked.xml +++ /dev/null @@ -1,879 +0,0 @@ - - - 100 - - 1 - 0 - 0 - - $INFO[Window.Property(background_colour)] - - - 0 - 0 - 1920 - 1080 - 1000 - $INFO[Window.Property(background)] - - - - 0 - 135 - 101 - - - Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) - 101 - 750 - 0 - 1170 - 1080 - - 0 - 0 - 1170 - 1080 - script.plex/white-square.png - 20000000 - - - - 0 - 0 - 1170 - 945 - 600 - 151 - 304 - 0 - vertical - 4 - 152 - 5 - - - - 120 - 24 - - - !String.IsEmpty(ListItem.Property(unwatched)) - 880 - -3 - 35 - 35 - script.plex/indicators/unwatched.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 861 - 14 - - 0 - 0 - 54 - 42 - script.plex/white-square-rounded.png - - - 0 - 0 - 54 - 42 - font10 - center - center - FF000000 - - - - - 0 - 0 - - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - - - String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) - 0 - 72 - 915 - 2 - script.plex/white-square.png - 40000000 - - - - - - - - - !Control.HasFocus(101) - 120 - 24 - - - !String.IsEmpty(ListItem.Property(unwatched)) - 880 - -2 - 35 - 35 - script.plex/indicators/unwatched.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 861 - 14 - - 0 - 0 - 54 - 42 - script.plex/white-square-rounded.png - - - 0 - 0 - 54 - 42 - font10 - center - center - FF000000 - - - - - 0 - 0 - - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - - - String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) - 0 - 72 - 915 - 2 - script.plex/white-square.png - 40000000 - - - - - Control.HasFocus(101) - 63 - 21 - - -40 - -40 - 1085 - 156 - script.plex/square-rounded-shadow.png - - - 0 - 0 - 1005 - 76 - script.plex/white-square-rounded.png - FFE5A00D - - - - - !String.IsEmpty(ListItem.Property(unwatched)) - 957 - 0 - 48 - 48 - script.plex/indicators/unwatched-rounded.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 933 - 15 - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 0 - 0 - 57 - 46 - script.plex/white-square-rounded.png - - - 0 - 0 - 57 - 46 - font10 - center - center - FF000000 - - - - - 60 - 0 - - 0 - 23 - 915 - 30 - font12 - left - center - DF000000 - - - - - - - - - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),episode) - - 1128 - 33 - 10 - 879 - 101 - 951 - true - script.plex/white-square-rounded.png - script.plex/white-square-rounded.png - script.plex/white-square-rounded.png - - - - - false - vertical - false - 151 - - - String.IsEqual(Window(10000).Property(script.plex.item.type),episode) - 1128 - 33 - 10 - 879 - - 0 - 0 - 10 - 879 - script.plex/white-square-rounded.png - - - 0 - 0 - - !Control.HasFocus(951) + String.IsEmpty(Window.Property(dragging)) - 0 - 0 - 10 - 10 - script.plex/white-square-rounded.png - - - Control.HasFocus(951) | !String.IsEmpty(Window.Property(dragging)) - 0 - 0 - 10 - 10 - script.plex/white-square-rounded.png - - - - - 0 - 0 - 10 - 879 - 152 - - - - - - - - - - - 0 - -135 - 1920 - 1080 - 1000 - $INFO[Window.Property(background)] - - - - VisibleChange - 301 - 30 - -25 - 1000 - 145 - 200 - 101 - 101 - -20 - horizontal - 200 - true - !String.IsEmpty(Window.Property(initialized)) - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/play-focus.png - script.plex/buttons/play.png - - - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/shuffle-focus.png - script.plex/buttons/shuffle.png - - - - - String.IsEmpty(Window.Property(no.options)) | Player.HasAudio - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/more-focus.png - script.plex/buttons/more.png - - - - - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/chapters-focus.png - script.plex/buttons/chapters.png - - - - - - - 60 - 248 - - !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) - 0 - 0 - 630 - 355 - 500 - $INFO[Container(101).ListItem.Property(art)] - scale - - - String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) - 0 - 0 - 630 - 355 - 500 - $INFO[Container(101).ListItem.Property(art)] - scale - - - 0 - 355 - 440 - 80 - font12 - left - center - FFFFFFFF - - - - 630 - 355 - 180 - 80 - font12 - right - center - FFFFFFFF - - - - 0 - 435 - 630 - 2 - script.plex/white-square.png - 40000000 - - - 0 - 463 - 630 - 307 - font12 - left - FFDDDDDD - - - - - - String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) - 151 - 1830 - 150 - 20 - 920 - - 0 - 0 - 34 - 1050 - 100 - 152 - 200 - vertical - - - - 0 - 0 - - 0 - 0 - - !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) - 0 - 0 - 34 - 32 - font10 - center - center - 99FFFFFF - - - - String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) - 0 - 0 - 34 - 32 - font10 - center - center - FFE5A00D - - - - - - - - - - 0 - 0 - - 0 - 0 - - !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) - 0 - 0 - 34 - 32 - font10 - center - center - 99FFFFFF - - - - String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) - 0 - 0 - 34 - 32 - font10 - center - center - FFE5A00D - - - - - - Control.HasFocus(151) - 0 - 0 - - Control.HasFocus(151) - 0 - 0 - 34 - 34 - FFE5A00D - script.plex/white-outline-rounded.png - - - - - - - - - 201 - 0 - 0 - 1920 - 135 - !String.IsEmpty(Window.Property(initialized)) - - 60 - 47.5 - 1000 - 40 - left - 60 - horizontal - 50 - - 40 - 40 - - Focus - UnFocus - 40 - 40 - 202 - 50 - font12 - FF000000 - script.plex/buttons/home-focus.png - script.plex/buttons/home.png - - - - - auto - 40 - font12 - left - center - FFFFFFFF - - - - 40 - 40 - - Focus - UnFocus - 40 - 40 - 204 - 201 - 50 - font12 - FF000000 - script.plex/buttons/search-focus.png - script.plex/buttons/search.png - - - - - - Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) - 438 - 0 - - Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) - -10 - 38 - 260 - 75 - 202 - 211 - 50 - font12 - FFFFFFFF - FF000000 - right - center - script.plex/white-square-rounded.png - - - 100 - 0 - - - - 0 - 48 - 42 - 42 - $INFO[Player.Art(thumb)] - - - - !Control.HasFocus(204) - - 53 - 48 - 187 - 20 - font10 - left - center - FFFFFFFF - MusicPlayer.Artist - - - 53 - 72 - 187 - 20 - font10 - left - center - FFFFFFFF - MusicPlayer.Title - - - - Control.HasFocus(204) - - 53 - 48 - 187 - 20 - font10 - left - center - FF000000 - MusicPlayer.Artist - - - 53 - 72 - 187 - 20 - font10 - left - center - FF000000 - MusicPlayer.Title - - - - - Progressbar - 0 - 102 - 240 - 1 - script.plex/white-square-1px.png - - - script.plex/white-square-1px.png - - - - - Player.Progress - - - - 311 - String.IsEmpty(Window.Property(hide.filteroptions)) - 340 - 35 - 1000 - 65 - right - 30 - horizontal - 204 - 210 - 50 - - !String.IsEqual(Window.Property(media.itemType),folder) - false - auto - 65 - font12 - FFFFFFFF - FFFFFFFF - FFFFFFFF - center - center - - - - - 0 - 0 - - - - !String.IsEqual(Window.Property(media.itemType),folder) - auto - 65 - font12 - FFFFFFFF - FF000000 - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - !String.IsEqual(Window.Property(media),show) + !String.IsEqual(Window.Property(media),movie) - false - auto - 65 - font12 - FFFFFFFF - FFFFFFFF - FFFFFFFF - center - center - - - - - 20 - 0 - - - - String.IsEqual(Window.Property(media),show) | String.IsEqual(Window.Property(media),movie) - auto - 65 - font12 - FFFFFFFF - FF000000 - FFFFFFFF - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - !String.IsEqual(Window.Property(media.itemType),folder) - auto - 65 - font12 - FFFFFFFF - FF000000 - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - - 213 - 35 - 200 - 65 - font12 - right - center - FFFFFFFF - - - - 153r - 54 - 93 - 30 - script.plex/home/plex.png - - - - - !String.IsEmpty(Window.Property(search.dialog)) - - !String.IsEmpty(Window.Property(search.dialog.hasresults)) - - 0 - 0 - 1920 - 1080 - script.plex/home/background-fallback.png - - - 0 - 0 - 1920 - 1080 - $INFO[Window.Property(background)] - - - - 0 - 0 - 1920 - 1080 - script.plex/white-square.png - - - - - diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml index 26fe1b8a1..a6a892171 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-16x9.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -811,9 +811,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml deleted file mode 100644 index b3aab3078..000000000 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square-chunked.xml +++ /dev/null @@ -1,943 +0,0 @@ - - - 100 - - 1 - 0 - 0 - - $INFO[Window.Property(background_colour)] - - - 0 - 0 - 1920 - 1080 - 1000 - $INFO[Window.Property(background)] - - - - 0 - 135 - 101 - - - Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) - 101 - 750 - 0 - 1170 - 1080 - - 0 - 0 - 1170 - 1080 - script.plex/white-square.png - 20000000 - - - - 0 - 0 - 1170 - 945 - 600 - 151 - 304 - 0 - vertical - 4 - 152 - 5 - - - - 120 - 24 - - - !String.IsEmpty(ListItem.Property(unwatched)) - 880 - -3 - 35 - 35 - script.plex/indicators/unwatched.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 861 - 14 - - 0 - 0 - 54 - 42 - script.plex/white-square-rounded.png - - - 0 - 0 - 54 - 42 - font10 - center - center - FF000000 - - - - - 0 - 0 - - String.IsEmpty(ListItem.Property(is.folder)) - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - !String.IsEmpty(ListItem.Property(is.folder)) - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - - - String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) - 0 - 72 - 915 - 2 - script.plex/white-square.png - 40000000 - - - - - - - - - !Control.HasFocus(101) - 120 - 24 - - - !String.IsEmpty(ListItem.Property(unwatched)) - 880 - -2 - 35 - 35 - script.plex/indicators/unwatched.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 861 - 14 - - 0 - 0 - 54 - 42 - script.plex/white-square-rounded.png - - - 0 - 0 - 54 - 42 - font10 - center - center - FF000000 - - - - - 0 - 0 - - 0 - 0 - - String.IsEmpty(ListItem.Property(is.folder)) - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - !String.IsEmpty(ListItem.Property(is.folder)) - 0 - 0 - 915 - 72 - font10 - left - center - FFFFFFFF - - - - - - - String.IsEmpty(ListItem.Property(is.footer)) + !String.IsEmpty(ListItem.Label) - 0 - 72 - 915 - 2 - script.plex/white-square.png - 40000000 - - - - - Control.HasFocus(101) - 63 - 21 - - -40 - -40 - 1085 - 156 - script.plex/square-rounded-shadow.png - - - 0 - 0 - 1005 - 76 - script.plex/white-square-rounded.png - FFE5A00D - - - - - !String.IsEmpty(ListItem.Property(unwatched)) - 957 - 0 - 48 - 48 - script.plex/indicators/unwatched-rounded.png - - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 933 - 15 - - !String.IsEmpty(ListItem.Property(unwatched.count)) - 0 - 0 - 57 - 46 - script.plex/white-square-rounded.png - - - 0 - 0 - 57 - 46 - font10 - center - center - FF000000 - - - - - 60 - 0 - - String.IsEmpty(ListItem.Property(is.folder)) - 0 - 23 - 885 - 30 - font12 - left - center - DF000000 - - - - !String.IsEmpty(ListItem.Property(is.folder)) - 0 - 23 - 885 - 30 - font10 - left - center - FF000000 - - - - - - - - - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),album) - - 1128 - 33 - 10 - 879 - 101 - 951 - true - script.plex/white-square-rounded.png - script.plex/white-square-rounded.png - script.plex/white-square-rounded.png - - - - - false - vertical - false - 151 - - - String.IsEqual(Window(10000).Property(script.plex.item.type),album) - 1128 - 33 - 10 - 879 - - 0 - 0 - 10 - 879 - script.plex/white-square-rounded.png - - - 0 - 0 - - !Control.HasFocus(951) + String.IsEmpty(Window.Property(dragging)) - 0 - 0 - 10 - 10 - script.plex/white-square-rounded.png - - - Control.HasFocus(951) | !String.IsEmpty(Window.Property(dragging)) - 0 - 0 - 10 - 10 - script.plex/white-square-rounded.png - - - - - 0 - 0 - 10 - 879 - 152 - - - - - - - - - - - 0 - -135 - 1920 - 1080 - 1000 - $INFO[Window.Property(background)] - - - - 301 - 30 - -25 - 1000 - 145 - 200 - 101 - 101 - -20 - horizontal - 200 - true - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/play-focus.png - script.plex/buttons/play.png - - - - - !String.IsEqual(Window(10000).Property(script.plex.item.type),collection) | String.IsEqual(Window.Property(media),collection) - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/shuffle-focus.png - script.plex/buttons/shuffle.png - - - - - String.IsEmpty(Window.Property(no.options)) | Player.HasAudio - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/more-focus.png - script.plex/buttons/more.png - - - - - String.IsEmpty(Window.Property(hide.filteroptions)) - Focus - UnFocus - 0 - 0 - 126 - 100 - font12 - script.plex/buttons/chapters-focus.png - script.plex/buttons/chapters.png - - - - - - - 60 - 248 - - String.IsEqual(Window.Property(media),photo) | String.IsEqual(Window.Property(media),photodirectory) - - 0 - 0 - 630 - 355 - script.plex/white-square.png - - - 0 - 0 - 630 - 355 - 500 - $INFO[Container(101).ListItem.Thumb] - keep - - - - String.IsEqual(Window.Property(media),artist) - 0 - 0 - 355 - 355 - 500 - $INFO[Container(101).ListItem.Thumb] - scale - - - !String.IsEmpty(Container(101).ListItem.Label2) - - 0 - 355 - 510 - 80 - font12 - left - center - FFFFFFFF - - - - 630 - 355 - 110 - 80 - font12 - right - center - FFFFFFFF - - - - - String.IsEmpty(Container(101).ListItem.Label2) - - 0 - 355 - 630 - 80 - font12 - left - center - FFFFFFFF - - - - - 0 - 435 - 630 - 2 - script.plex/white-square.png - 40000000 - - - 0 - 463 - 630 - 307 - font12 - left - FFDDDDDD - - - - - - String.IsEqual(Window(10000).Property(script.plex.sort),titleSort) + Integer.IsGreater(Container(101).NumItems,0) + String.IsEmpty(Window.Property(drawing)) - 151 - 1830 - 150 - 20 - 920 - - 0 - 0 - 34 - 1050 - 100 - 152 - 200 - vertical - - - - 0 - 0 - - 0 - 0 - - !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) - 0 - 0 - 34 - 32 - font10 - center - center - 99FFFFFF - - - - String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) - 0 - 0 - 34 - 32 - font10 - center - center - FFE5A00D - - - - - - - - - - 0 - 0 - - 0 - 0 - - !String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(letter)) - 0 - 0 - 34 - 32 - font10 - center - center - 99FFFFFF - - - - String.IsEqual(Window(10000).Property(script.plex.key), ListItem.Property(key)) - 0 - 0 - 34 - 32 - font10 - center - center - FFE5A00D - - - - - - Control.HasFocus(151) - 0 - 0 - - Control.HasFocus(151) - 0 - 0 - 34 - 34 - FFE5A00D - script.plex/white-outline-rounded.png - - - - - - - - - 201 - 0 - 0 - 1920 - 135 - - 60 - 47.5 - 1000 - 40 - left - 60 - horizontal - 50 - - 40 - 40 - - Focus - UnFocus - 40 - 40 - 202 - 50 - font12 - FF000000 - script.plex/buttons/home-focus.png - script.plex/buttons/home.png - - - - - auto - 40 - font12 - left - center - FFFFFFFF - - - - 40 - 40 - - Focus - UnFocus - 40 - 40 - 204 - 201 - 50 - font12 - FF000000 - script.plex/buttons/search-focus.png - script.plex/buttons/search.png - - - - - - Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) - 438 - 0 - - Player.HasAudio + String.IsEmpty(Window(10000).Property(script.plex.theme_playing)) - -10 - 38 - 260 - 75 - 202 - 211 - 50 - font12 - FFFFFFFF - FF000000 - right - center - script.plex/white-square-rounded.png - - - 100 - 0 - - - - 0 - 48 - 42 - 42 - $INFO[Player.Art(thumb)] - - - - !Control.HasFocus(204) - - 53 - 48 - 187 - 20 - font10 - left - center - FFFFFFFF - MusicPlayer.Artist - - - 53 - 72 - 187 - 20 - font10 - left - center - FFFFFFFF - MusicPlayer.Title - - - - Control.HasFocus(204) - - 53 - 48 - 187 - 20 - font10 - left - center - FF000000 - MusicPlayer.Artist - - - 53 - 72 - 187 - 20 - font10 - left - center - FF000000 - MusicPlayer.Title - - - - - Progressbar - 0 - 102 - 240 - 1 - script.plex/white-square-1px.png - - - script.plex/white-square-1px.png - - - - - Player.Progress - - - - 311 - String.IsEmpty(Window.Property(hide.filteroptions)) - 340 - 35 - 1000 - 65 - right - 30 - horizontal - 204 - 210 - 50 - - false - auto - 65 - font12 - FFFFFFFF - FFFFFFFF - FFFFFFFF - center - center - - - - - 0 - 0 - - - - auto - 65 - font12 - FFFFFFFF - FF000000 - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - !String.IsEqual(Window.Property(media),artist) - false - auto - 65 - font12 - FFFFFFFF - FFFFFFFF - FFFFFFFF - center - center - - - - - 20 - 0 - - - - String.IsEqual(Window.Property(media),artist) - auto - 65 - font12 - FFFFFFFF - FF000000 - FFFFFFFF - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - auto - 65 - font12 - FFFFFFFF - FF000000 - center - center - script.plex/white-square-rounded.png - - - 20 - 0 - - - - - 213 - 35 - 200 - 65 - font12 - right - center - FFFFFFFF - - - - 153r - 54 - 93 - 30 - script.plex/home/plex.png - - - - - !String.IsEmpty(Window.Property(search.dialog)) - - !String.IsEmpty(Window.Property(search.dialog.hasresults)) - - 0 - 0 - 1920 - 1080 - script.plex/home/background-fallback.png - - - 0 - 0 - 1920 - 1080 - $INFO[Window.Property(background)] - - - - 0 - 0 - 1920 - 1080 - script.plex/white-square.png - - - - - diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml index b5349dbb0..7dbc4b5ff 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-listview-square.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -877,9 +877,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml index 3c0ab818f..1a6027418 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-options_dialog.xml @@ -67,6 +67,8 @@ font10 left FFFFFFFF + 200 + diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml index 5c642c0c3..859d1bb04 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-playlist.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -852,9 +852,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml index f53b7f477..3532e385d 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-playlists.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -536,9 +536,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml index e8f738077..75d3eebd4 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-posters-small.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -790,9 +790,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml index afc001480..43154ee67 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-posters.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -764,9 +764,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png 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 6cfca6581..167c6e4d8 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 @@ -10,12 +10,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -23,7 +23,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -31,7 +31,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -1447,9 +1447,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png 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 114dafa31..2cf45910d 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-seasons.xml @@ -10,12 +10,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -23,7 +23,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -31,7 +31,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -1398,9 +1398,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png 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 deleted file mode 100644 index 818fb6904..000000000 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-seek_dialog.xml +++ /dev/null @@ -1,1101 +0,0 @@ - - - - 1 - 0 - 0 - - 100 - 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) + !Window.IsVisible(osdcmssettings) - Hidden - - String.IsEmpty(Window.Property(settings.visible)) + [Window.IsVisible(seekbar) | Window.IsVisible(videoosd) | Player.ShowInfo] - Hidden - 0 - 0 - - 0 - 0 - 1920 - 1080 - script.plex/player-fade.png - FF080808 - - - - - 0 - 0 - - 0 - 0 - 1920 - 140 - script.plex/white-square.png - A0000000 - - - 0 - 940 - 1920 - 140 - script.plex/white-square.png - A0000000 - - - - - 0 - 40 - - !String.IsEmpty(Window.Property(is.show)) - 60 - 0 - 1720 - 60 - font13 - left - center - FFFFFFFF - true - 15 - - - - String.IsEmpty(Window.Property(is.show)) - 60 - 0 - 1720 - 60 - font13 - left - center - FFFFFFFF - true - 15 - - - - 1860 - 0 - 300 - 60 - font12 - right - center - FFFFFFFF - - - - - - 0 - 965 - - !String.IsEmpty(Window.Property(direct.play)) - 60 - 0 - 1000 - 60 - font13 - left - center - FFFFFFFF - - - - String.IsEmpty(Window.Property(direct.play)) - 60 - 0 - 1000 - 60 - font13 - left - center - FFFFFFFF - - - - Player.IsTempo - 60 - 40 - 1000 - 60 - font13 - left - center - A0FFFFFF - - - - !String.IsEmpty(Window.Property(direct.play)) - 1860 - 0 - 800 - 60 - font13 - right - center - FFFFFFFF - - - - String.IsEmpty(Window.Property(direct.play)) - 1860 - 0 - 800 - 60 - font13 - right - center - FFFFFFFF - - - - !String.IsEmpty(Window.Property(media.show_ends)) + !String.IsEmpty(Window.Property(direct.play)) - 1860 - 40 - 800 - 60 - font13 - right - center - A0FFFFFF - - - - !String.IsEmpty(Window.Property(media.show_ends)) + String.IsEmpty(Window.Property(direct.play)) - 1860 - 40 - 800 - 60 - font13 - right - center - A0FFFFFF - - - - Player.Paused + String.IsEmpty(Window.Property(show.OSD)) - Visible - 0 - 20 - 1920 - 60 - font13 - center - center - FFCC7B19 - - - - - - 0 - 940 - - 0 - 0 - 1920 - 10 - script.plex/white-square.png - A0000000 - - - !String.IsEmpty(Window.Property(show.buffer)) - 0 - 2 - 1 - 6 - script.plex/white-square.png - EE4E4842 - - - 0 - 2 - 1 - 6 - script.plex/white-square.png - FFAC5B00 - - - Control.HasFocus(100) | !String.IsEmpty(Window.Property(button.seek)) - 0 - 2 - 1 - 6 - script.plex/white-square.png - FFE5A00D - - - - - String.IsEmpty(Window.Property(show.OSD)) - 0 - 0 - 1920 - 1080 - - - - - - SetProperty(show.OSD,1) - - - - - 0 - 350 - !String.IsEmpty(Window.Property(show.PPI)) + String.IsEmpty(Window.Property(settings.visible)) + String.IsEmpty(Window.Property(playlist.visible)) - Visible - Hidden - - 10 - -220 - 10 - 420 - buttons/dialogbutton-nofo.png - - - 52 - -184 - 1786 - 350 - horizontal - 10 - - 0 - 0 - 793 - - 793 - 50 - bottom - - font14 - black - Player.HasVideo - - - 793 - 50 - bottom - - font14 - black - Player.HasVideo - - - 793 - 50 - bottom - - font14 - black - Player.HasVideo - - - 793 - 50 - bottom - - font14 - black - Player.HasVideo - - - 793 - 50 - bottom - - - font14 - black - - - 793 - 50 - bottom - - font14 - black - - - - 0 - 0 - 993 - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Status)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Mode)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Container)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Video)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + [!String.IsEmpty(Window.Property(ppi.Audio)) | !String.IsEmpty(Window.Property(ppi.Subtitles))] - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.User)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + String.IsEmpty(Window.Property(ppi.Buffered)) - - - 893 - 50 - bottom - - font14 - black - Player.HasVideo + !String.IsEmpty(Window.Property(ppi.Buffered)) - - - - - 52 - 120 - 1786 - 50 - bottom - - font14 - black - - - - !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))] - Visible - 0 - 752 - - 0 - 0 - 324 - 184 - script.plex/white-square.png - FF000000 - - - 2 - 2 - 320 - 180 - 10 - $INFO[Window.Property(bif.image)] - - - - - 406 - - 360 - 964 - 1200 - - 124 - center - 100 - -40 - horizontal - 200 - true - - !String.IsEmpty(Window.Property(nav.repeat)) - Conditional - Conditional - 125 - 101 - - - 0 - 0 - 125 - 101 - 100 - 402 - 412 - font12 - - - - - - - - !Control.HasFocus(401) - - !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat.png - - - Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat.png - - - Playlist.IsRepeatOne | !String.IsEmpty(Window.Property(pq.repeat.one)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat-one.png - - - - Control.HasFocus(401) - - !Playlist.IsRepeatOne + !Playlist.IsRepeat + String.IsEmpty(Window.Property(pq.repeat)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat-focus.png - - - Playlist.IsRepeat | !String.IsEmpty(Window.Property(pq.repeat)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat-focus.png - - - Playlist.IsRepeatOne | !String.IsEmpty(Window.Property(pq.repeat.one)) - 0 - 0 - 125 - 101 - script.plex/buttons/repeat-one-focus.png - - - - - - !String.IsEmpty(Window.Property(has.playlist)) + !String.IsEmpty(Window.Property(nav.shuffle)) - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/shuffle-focus.png - script.plex/buttons/shuffle.png - !String.IsEmpty(Window.Property(pq.shuffled)) - script.plex/buttons/shuffle-focus.png - script.plex/buttons/shuffle.png - - - - false - String.IsEmpty(Window.Property(has.playlist)) + !String.IsEmpty(Window.Property(nav.shuffle)) - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/shuffle-focus.png - script.plex/buttons/shuffle.png - - - - - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/settings-focus.png - script.plex/buttons/settings.png - - - - - - !String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.prevnext)) - Focus - UnFocus - - 30 - 0 - 125 - 101 - font12 - script.plex/buttons/next-focus.png - script.plex/buttons/next.png - - - - false - String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.prevnext)) - 30 - 0 - 125 - 101 - font12 - script.plex/buttons/next-focus.png - script.plex/buttons/next.png - - - - !String.IsEmpty(Window.Property(nav.ffwdrwd)) - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/skip-forward-focus.png - script.plex/buttons/skip-forward.png - - - - - Conditional - Conditional - 125 - 101 - - - 0 - 0 - 125 - 101 - 100 - 407 - 405 - font12 - - - - - - PlayerControl(Play) - - - !Control.HasFocus(406) - - !Player.Paused + !Player.Forwarding + !Player.Rewinding - 0 - 0 - 125 - 101 - script.plex/buttons/pause.png - - - Player.Paused | Player.Forwarding | Player.Rewinding - 0 - 0 - 125 - 101 - script.plex/buttons/play.png - - - - Control.HasFocus(406) - - !Player.Paused + !Player.Forwarding + !Player.Rewinding - 0 - 0 - 125 - 101 - script.plex/buttons/pause-focus.png - - - Player.Paused | Player.Forwarding | Player.Rewinding - 0 - 0 - 125 - 101 - script.plex/buttons/play-focus.png - - - - - - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/stop-focus.png - script.plex/buttons/stop.png - - - - !String.IsEmpty(Window.Property(nav.ffwdrwd)) - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/skip-forward-focus.png - script.plex/buttons/skip-forward.png - - - - !String.IsEmpty(Window.Property(pq.hasnext)) + !String.IsEmpty(Window.Property(nav.prevnext)) - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/next-focus.png - script.plex/buttons/next.png - - - - false - String.IsEmpty(Window.Property(pq.hasnext)) + !String.IsEmpty(Window.Property(nav.prevnext)) - 0 - 0 - 125 - 101 - script.plex/buttons/next-focus.png - script.plex/buttons/next.png - - - - - - [!String.IsEmpty(Window.Property(pq.hasnext)) | !String.IsEmpty(Window.Property(pq.hasprev))] + !String.IsEmpty(Window.Property(nav.playlist)) - Focus - UnFocus - - 30 - 0 - 125 - 101 - font12 - script.plex/buttons/pqueue-focus.png - script.plex/buttons/pqueue.png - - - - false - String.IsEmpty(Window.Property(pq.hasnext)) + String.IsEmpty(Window.Property(pq.hasprev)) + !String.IsEmpty(Window.Property(nav.playlist)) - Focus - UnFocus - - 30 - 0 - 125 - 101 - font12 - script.plex/buttons/pqueue-focus.png - script.plex/buttons/pqueue.png - - - - !String.IsEmpty(Window.Property(nav.quick_subtitles)) - Focus - UnFocus - - 0 - 0 - 125 - 101 - font12 - script.plex/buttons/subtitle-focus.png - script.plex/buttons/subtitle.png - - - - - - 0 - 940 - - - 0 - 0 - 1920 - 10 - 501 - 400 - - - - - - - - - Conditional - - - Conditional - - - String.IsEmpty(Window.Property(mouse.mode)) + String.IsEmpty(Window.Property(hide.bigseek)) + [Control.HasFocus(501) | Control.HasFocus(100)] + [!String.IsEmpty(Window.Property(show.chapters)) | String.IsEmpty(Window.Property(has.chapters))] - -8 - 917 - - -200 - 5 - 2320 - 6 - script.plex/white-square.png - A0000000 - String.IsEmpty(Window.Property(has.chapters)) - - - - 0 - -175 - 1928 - 200 - script.plex/white-square.png - A0000000 - !String.IsEmpty(Window.Property(has.chapters)) - - - 40 - -162 - auto - 20 - font10 - left - center - CC606060 - - !String.IsEmpty(Window.Property(has.chapters)) + !Control.HasFocus(501) - - - 40 - -162 - auto - 20 - font10 - left - center - FFFFFFFF - - !String.IsEmpty(Window.Property(has.chapters)) + Control.HasFocus(501) - - - - - 0 - 0 - 1928 - 16 - 100 - SetProperty(hide.bigseek,) - 200 - horizontal - 4 - - - - 0 - 0 - 16 - 16 - script.plex/indicators/seek-selection-marker.png - FF606060 - - - - - - - !Control.HasFocus(501) - 0 - 0 - 16 - 16 - script.plex/indicators/seek-selection-marker.png - FF606060 - - - Control.HasFocus(501) - 0 - 0 - 16 - 16 - script.plex/indicators/seek-selection-marker.png - FFE5A00D - - - - - - - - 40 - 0 - 178 - 100 - script.plex/thumb_fallbacks/movie16x9.png - scale - CC606060 - !Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - $INFO[ListItem.Thumb] - scale - DDAAAAAA - !Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - script.plex/thumb_fallbacks/movie16x9.png - scale - FFAAAAAA - Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - $INFO[ListItem.Thumb] - scale - FFAAAAAA - Control.HasFocus(501) - - - 40 - 120 - auto - 10 - font10 - center - center - CC606060 - - !Control.HasFocus(501) - - - 40 - 120 - auto - 10 - font10 - center - center - FFAAAAAA - - Control.HasFocus(501) - - - - - - - - - 40 - 0 - 178 - 100 - script.plex/thumb_fallbacks/movie16x9.png - scale - CC909090 - !Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - $INFO[ListItem.Thumb] - scale - FFAAAAAA - !Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - script.plex/thumb_fallbacks/movie16x9.png - scale - - Control.HasFocus(501) - - - 40 - 0 - 178 - 100 - $INFO[ListItem.Thumb] - scale - - Control.HasFocus(501) - - - 40 - 120 - auto - 10 - font10 - center - center - FFAAAAAA - - !Control.HasFocus(501) - - - 40 - 120 - auto - 10 - font10 - center - center - - - Control.HasFocus(501) - - - - - - - - Control.HasFocus(100) | Control.HasFocus(501) | !String.IsEmpty(Window.Property(button.seek)) - 0 - 896 - - -50 - 0 - - Visible - 0 - 0 - 101 - 39 - script.plex/indicators/player-selection-time_box.png - D0000000 - - - 0 - 0 - 101 - 40 - font10 - center - center - FFFFFFFF - - - - - Visible - -6 - 39 - 15 - 7 - script.plex/indicators/player-selection-time_arrow.png - D0000000 - - - - - - 30 - 797 - 1670 - 143 - right - horizontal - - [!String.IsEmpty(Window.Property(show.markerSkip)) + String.IsEmpty(Window.Property(show.markerSkip_OSDOnly))] | [!String.IsEmpty(Window.Property(show.markerSkip_OSDOnly)) + !String.IsEmpty(Window.Property(show.OSD))] - Focus - UnFocus - - - - auto - 143 - center - 0 - 0 - script.plex/buttons/blank-focus.png - script.plex/buttons/blank.png - 70 - FF000000 - FF000000 - - - - - diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml index 35321b29b..8ea7e0dac 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-settings.xml @@ -673,9 +673,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml index c6eab7662..e9ea15ad8 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-squares.xml @@ -11,12 +11,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -24,7 +24,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -32,7 +32,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -682,9 +682,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml index 0ab23658b..96b22fdfb 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-user_select.xml @@ -911,9 +911,9 @@ 153r - 54 + 47.5 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml b/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml index b9abc3a73..6885b5188 100644 --- a/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml +++ b/script.plexmod/resources/skins/Main/1080i/script-plex-video_player.xml @@ -10,12 +10,12 @@ String.IsEmpty(Window.Property(use_solid_background)) - String.IsEmpty(Window.Property(use_solid_background)) + String.IsEmpty(Window.Property(use_bg_fallback)) + String.IsEmpty(Window.Property(background_static)) + String.IsEmpty(Window.Property(use_bg_fallback)) 0 0 1920 1080 - script.plex/home/background-fallback_black.png + script.plex/home/background-fallback_black.png !String.IsEmpty(Window.Property(use_bg_fallback)) @@ -23,7 +23,7 @@ 0 1920 1080 - script.plex/home/background-fallback.png + script.plex/home/background-fallback.png String.IsEmpty(Window.Property(use_bg_fallback)) @@ -31,7 +31,7 @@ 0 1920 1080 - $INFO[Window.Property(background_static)] + $INFO[Window.Property(background_static)] String.IsEmpty(Window.Property(use_bg_fallback)) @@ -1314,7 +1314,7 @@ 153r 54 93 - 30 + 43 script.plex/home/plex.png diff --git a/script.plexmod/resources/skins/Main/media/script.plex/home/plex.png b/script.plexmod/resources/skins/Main/media/script.plex/home/plex.png index 9a94e24096b26273c8398858395fa77d23b08bd1..e4c95a6340014961dab8dfaebc7129a0df91031f 100644 GIT binary patch literal 2046 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 ZzW}^GpnTlSyl4Ob002ovPDHLkV1f%V_hbM7 diff --git a/script.plexmod/resources/skins/Main/media/script.plex/splash.png b/script.plexmod/resources/skins/Main/media/script.plex/splash.png index e7a2a53ac5f5bca42c13de3624a4c304311e9bee..baf74bd359ce6ac19096986e2ac33463a2bfd3a5 100644 GIT binary patch literal 15885 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 R2a<@0^t6mL8`T|O{STLR)H(nF diff --git a/script.plexmod/resources/skins/Main/media/script.plex/user_select/plex.png b/script.plexmod/resources/skins/Main/media/script.plex/user_select/plex.png index 2fbd92adff07dca33b6b5297f372c44fe9443850..74590fed031713282ad493ac2129ebacf02ec531 100644 GIT binary patch literal 4767 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