diff --git a/addon.xml b/addon.xml index 8bc7f3f42..dc54a553c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -14,6 +14,16 @@ + + + + [[[String.IsEqual(ListItem.DBTYPE,movie) | String.IsEqual(ListItem.DBTYPE,tvshow) | String.IsEqual(ListItem.DBTYPE,season) | String.IsEqual(ListItem.DBTYPE,episode)] + !String.IsEmpty(ListItem.DBID)] + !String.IsEqual(ListItem.Property(LISTINGKEY),watchlist)] + + + + + [[[String.IsEqual(ListItem.DBTYPE,movie) | String.IsEqual(ListItem.DBTYPE,tvshow)] + !String.IsEmpty(ListItem.DBID)] + String.IsEqual(ListItem.Property(LISTINGKEY),watchlist)] + @@ -103,7 +113,37 @@ Plex를 Kodi에 기본 통합 Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오! 자신의 책임하에 사용 - version 3.9.0: + version 3.10.0: +- WARNING: You will need to reset the Kodi database! +- Versions 3.9.1-3.9.6 for everyone + +version 3.9.6 (beta only): +- Fix Base.check_db() got an unexpected keyword argument 'check_by_guid' #2074 + +version 3.9.5 (beta only): +- Fix sync crashing with "plex_guid is not defined" #2072 + +version 3.9.4 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix certain movie elements suddenly duplicating in library #2070 + +version 3.9.3 (beta only): +- Add Watchlist context menu commands (thanks @Spacetech) #2060 +- Sync Plex Labels (thanks @Spacetech) #2058 +- Fix nested folder navigation (thanks @Spacetech) #2063 +- Reduce database locking (thanks @Spacetech) #2061 + +version 3.9.2 (beta only): +- Fix getVideoInfoTag error (thanks @Spacetech) #2052 +- Automatically hide the skip button after some time (thanks @Spacetech) #2053 +- Force a database reset, necessary due to #2041 #2054 + +version 3.9.1 (beta only): +- Add support for Plex Watchlist (thanks @Spacetech) #2041 +- Use InfoTagVideo to set list item data (thanks @Spacetech) #2040 +- New setting: Allow delaying background sync while playing videos (thanks @Spacetech) #2039 + +version 3.9.0: - WARNING: You will need to reset the Kodi database! - versions 3.8.4-3.8.8 for everyone diff --git a/changelog.txt b/changelog.txt index 4d048b704..bd75e350c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,33 @@ +version 3.10.0: +- WARNING: You will need to reset the Kodi database! +- Versions 3.9.1-3.9.6 for everyone + +version 3.9.6 (beta only): +- Fix Base.check_db() got an unexpected keyword argument 'check_by_guid' #2074 + +version 3.9.5 (beta only): +- Fix sync crashing with "plex_guid is not defined" #2072 + +version 3.9.4 (beta only): +- WARNING: You will need to reset the Kodi database! +- Fix certain movie elements suddenly duplicating in library #2070 + +version 3.9.3 (beta only): +- Add Watchlist context menu commands (thanks @Spacetech) #2060 +- Sync Plex Labels (thanks @Spacetech) #2058 +- Fix nested folder navigation (thanks @Spacetech) #2063 +- Reduce database locking (thanks @Spacetech) #2061 + +version 3.9.2 (beta only): +- Fix getVideoInfoTag error (thanks @Spacetech) #2052 +- Automatically hide the skip button after some time (thanks @Spacetech) #2053 +- Force a database reset, necessary due to #2041 #2054 + +version 3.9.1 (beta only): +- Add support for Plex Watchlist (thanks @Spacetech) #2041 +- Use InfoTagVideo to set list item data (thanks @Spacetech) #2040 +- New setting: Allow delaying background sync while playing videos (thanks @Spacetech) #2039 + version 3.9.0: - WARNING: You will need to reset the Kodi database! - versions 3.8.4-3.8.8 for everyone diff --git a/context_watchlist_add.py b/context_watchlist_add.py new file mode 100644 index 000000000..e6173d1b8 --- /dev/null +++ b/context_watchlist_add.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Grabs kodi_id and kodi_type for the Kodi item the context menu was called for +and sends a request to our main Python instance +""" +from urllib.parse import urlencode + +from xbmc import sleep +from xbmcgui import Window + +from resources.lib.contextmenu.common import kodi_item_from_listitem + + +def main(): + kodi_id, kodi_type = kodi_item_from_listitem() + args = { + 'kodi_id': kodi_id, + 'kodi_type': kodi_type + } + window = Window(10000) + while window.getProperty('plexkodiconnect.command'): + sleep(20) + window.setProperty('plexkodiconnect.command', + 'WATCHLIST_ADD?%s' % urlencode(args)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/context_watchlist_remove.py b/context_watchlist_remove.py new file mode 100644 index 000000000..57d27cab6 --- /dev/null +++ b/context_watchlist_remove.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Grabs kodi_id and kodi_type for the Kodi item the context menu was called for +and sends a request to our main Python instance +""" +from urllib.parse import urlencode + +from xbmc import sleep +from xbmcgui import Window + +from resources.lib.contextmenu.common import kodi_item_from_listitem + + +def main(): + kodi_id, kodi_type = kodi_item_from_listitem() + args = { + 'kodi_id': kodi_id, + 'kodi_type': kodi_type + } + window = Window(10000) + while window.getProperty('plexkodiconnect.command'): + sleep(20) + window.setProperty('plexkodiconnect.command', + 'WATCHLIST_REMOVE?%s' % urlencode(args)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/default.py b/default.py index 792020ffa..029552012 100644 --- a/default.py +++ b/default.py @@ -86,6 +86,8 @@ def triage(mode, params, path, arguments, itemid): entrypoint.show_section(params.get('section_index')) elif mode == 'watchlater': entrypoint.watchlater() + elif mode == 'watchlist': + entrypoint.watchlist(section_id=params.get('section_id')) elif mode == 'channels': entrypoint.browse_plex(key='/channels/all') elif mode == 'search': diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index c3d3c9843..e4fcbecc8 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -409,6 +409,16 @@ msgctxt "#30401" msgid "Plex options" msgstr "" +# contextmenu entry +msgctxt "#30402" +msgid "Add to Plex Watchlist" +msgstr "" + +# contextmenu entry +msgctxt "#30403" +msgid "Remove from Plex Watchlist" +msgstr "" + # contextmenu entry msgctxt "#30405" msgid "Add to Plex favorites" @@ -878,6 +888,11 @@ msgctxt "#39026" msgid "Enable constant background sync" msgstr "" +# PKC Settings - Sync +msgctxt "#39027" +msgid "Delay background sync while media is playing" +msgstr "" + # Pop-up on initial sync msgctxt "#39028" msgid "CAUTION! If you choose \"Native\" mode , you might loose access to certain Plex features such as: Plex trailers and transcoding options. ALL Plex shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or \\\\myNAS/mymovie.mkv)!" @@ -1231,6 +1246,10 @@ msgctxt "#39211" msgid "Watch later" msgstr "" +msgctxt "#39212" +msgid "Watchlist" +msgstr "" + # Error message pop-up if {0} cannot be contacted. {0} will be replaced by e.g. the PMS' name msgctxt "#39213" msgid "{0} offline" @@ -1526,3 +1545,13 @@ msgstr "" msgctxt "#39722" msgid "Auto skip commercials" msgstr "" + +# PKC Settings - Playback +msgctxt "#39723" +msgid "Auto hide skip button" +msgstr "" + +# PKC Settings - Playback +msgctxt "#39724" +msgid "Seconds until skip button is hidden" +msgstr "" diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py index 456390ed3..f53f00c4c 100644 --- a/resources/lib/app/playstate.py +++ b/resources/lib/app/playstate.py @@ -35,6 +35,7 @@ class PlayState(object): 'playcount': None, 'external_player': False, # bool - xbmc.Player().isExternalPlayer() 'markers': [], + 'markers_hidden': {}, 'first_credits_marker': None, 'final_credits_marker': None } diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 9c56544e6..bff4b1763 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -151,10 +151,12 @@ def show_main_menu(content_type=None): directory_item(utils.lang(136), path) # Plex Search "Search" directory_item(utils.lang(137), "plugin://%s?mode=search" % v.ADDON_ID) - # Plex Watch later + # Plex Watch later and Watchlist if content_type not in ('image', 'audio'): directory_item(utils.lang(39211), "plugin://%s?mode=watchlater" % v.ADDON_ID) + directory_item(utils.lang(39212), + "plugin://%s?mode=watchlist" % v.ADDON_ID) # Plex Channels directory_item(utils.lang(30173), "plugin://%s?mode=channels" % v.ADDON_ID) # Plex user switch @@ -203,7 +205,7 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None): return api = API(xml[0]) # Determine content type for Kodi's Container.content - if key == '/hubs/home/continueWatching': + if key == '/hubs/home/continueWatching' or key == 'watchlist': # Mix of movies and episodes plex_type = v.PLEX_TYPE_VIDEO elif key == '/hubs/home/recentlyAdded?type=2': @@ -249,9 +251,19 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None): # Need to chain keys for navigation widgets.KEY = key # Process all items to show - all_items = mass_api(xml) + all_items = mass_api(xml, check_by_guid=key == "watchlist") + + if key == "watchlist": + # filter out items that are not in the kodi db (items that will not be playable) + all_items = [item for item in all_items if item.kodi_id is not None] + + # filter out items in the wrong section id when it's specified + if section_id is not None: + all_items = [item for item in all_items + if item.section_id == utils.cast(int, section_id)] + all_items = [widgets.generate_item(api) for api in all_items] - all_items = [widgets.prepare_listitem(item) for item in all_items] + all_items = [widgets.prepare_listitem(item, key) for item in all_items] # fill that listing... all_items = [widgets.create_listitem(item) for item in all_items] xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items)) @@ -468,6 +480,29 @@ def watchlater(): show_listing(xml) +def watchlist(section_id=None): + """ + Listing for plex.tv Watchlist section (if signed in to plex.tv) + """ + _wait_for_auth() + if utils.window('plex_token') == '': + LOG.error('No watchlist - not signed in to plex.tv') + raise ListingException + if utils.window('plex_restricteduser') == 'true': + LOG.error('No watchlist - restricted user') + raise ListingException + app.init(entrypoint=True) + xml = DU().downloadUrl('https://metadata.provider.plex.tv/library/sections/watchlist/all', + authenticate=False, + headerOptions={'X-Plex-Token': utils.window('plex_token')}) + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not download watch list list from plex.tv') + raise ListingException + show_listing(xml, None, section_id, False, "watchlist") + + def browse_plex(key=None, plex_type=None, section_id=None, synched=True, args=None, prompt=None, query=None): """ diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index f1536bb3f..b67ee9002 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -158,7 +158,9 @@ def add_update(self, xml, section_name=None, section_id=None, self.kodidb.modify_streams(file_id, api.mediastreams(), api.runtime()) self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, api.studios()) + # Process tags: section, PMS labels, PMS collection tags tags = [section_name] + tags.extend(api.labels()) self._process_collections(api, tags, kodi_id, section_id, children) self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags) # Process playstate @@ -168,6 +170,7 @@ def add_update(self, xml, section_name=None, section_id=None, api.viewcount(), api.lastplayed()) self.plexdb.add_movie(plex_id=plex_id, + plex_guid=api.plex_guid, checksum=api.checksum(), section_id=section_id, kodi_id=kodi_id, diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index f0273e590..38381b9f5 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -248,11 +248,13 @@ def add_update(self, xml, section_name=None, section_id=None, self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, api.genres()) # Process studios self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, api.studios()) - # Process tags: view, PMS collection tags + # Process tags: section, PMS labels, PMS collection tags tags = [section_name] + tags.extend(api.labels()) tags.extend([i for _, i in api.collections()]) self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags) self.plexdb.add_show(plex_id=plex_id, + plex_guid=api.plex_guid, checksum=api.checksum(), section_id=section_id, kodi_id=kodi_id, @@ -340,6 +342,7 @@ def add_update(self, xml, section_name=None, section_id=None, kodi_id, v.KODI_TYPE_SEASON) self.plexdb.add_season(plex_id=plex_id, + plex_guid=api.plex_guid, checksum=api.checksum(), section_id=section_id, show_id=show_id, @@ -496,6 +499,7 @@ def add_update(self, xml, section_name=None, section_id=None, api.viewcount(), api.lastplayed()) self.plexdb.add_episode(plex_id=plex_id, + plex_guid=api.plex_guid, checksum=api.checksum(), section_id=section_id, show_id=api.show_id(), @@ -566,6 +570,7 @@ def add_update(self, xml, section_name=None, section_id=None, api.viewcount(), api.lastplayed()) self.plexdb.add_episode(plex_id=plex_id, + plex_guid=api.plex_guid, checksum=api.checksum(), section_id=section_id, show_id=api.show_id(), diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index e18776b09..39839237e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -157,7 +157,7 @@ def _get_ids(kodi_id, kodi_type, path): if not kodi_id and kodi_type and path: kodi_id, _ = kodi_db.kodiid_from_filename(path, kodi_type) if kodi_id: - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_kodi_id(kodi_id, kodi_type) if db_item: plex_id = db_item['plex_id'] @@ -336,6 +336,7 @@ def PlayBackStart(self, data): or utils.settings('enableSkipCredits') == 'true' \ or utils.settings('enableSkipCommercials') == 'true': status['markers'] = item.api.markers() + status['markers_hidden'] = {} if utils.settings('enableSkipCredits') == 'true': status['first_credits_marker'] = item.api.first_credits_marker() status['final_credits_marker'] = item.api.final_credits_marker() @@ -418,7 +419,7 @@ def _record_playstate(status, ended): if status['plex_type'] not in v.PLEX_VIDEOTYPES: LOG.debug('Not messing with non-video entries') return - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_id(status['plex_id'], status['plex_type']) if not db_item: # Item not (yet) in Kodi library @@ -465,7 +466,7 @@ def _playback_progress(status, ended, db_item): playcount = status['playcount'] if playcount is None: LOG.debug('playcount not found, looking it up in the Kodi DB') - with kodi_db.KodiVideoDB() as kodidb: + with kodi_db.KodiVideoDB(lock=False) as kodidb: playcount = kodidb.get_playcount(db_item['kodi_fileid']) or 0 if status['external_player']: # video has either been entirely watched - or not. @@ -534,7 +535,7 @@ def _external_player_correct_plex_watch_count(db_item): playcountminimumtime set in playercorefactory.xml) See https://kodi.wiki/view/External_players """ - with kodi_db.KodiVideoDB() as kodidb: + with kodi_db.KodiVideoDB(lock=False) as kodidb: playcount = kodidb.get_playcount(db_item['kodi_fileid']) LOG.debug('External player detected. Playcount: %s', playcount) PF.scrobble(db_item['plex_id'], 'watched' if playcount else 'unwatched') diff --git a/resources/lib/library_sync/additional_metadata.py b/resources/lib/library_sync/additional_metadata.py index ac87d708b..e6b4f2655 100644 --- a/resources/lib/library_sync/additional_metadata.py +++ b/resources/lib/library_sync/additional_metadata.py @@ -43,7 +43,7 @@ def should_suspend(self): def _process_in_batches(self, item_getter, processor, plex_type): offset = 0 while True: - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: # Keep DB connection open only for a short period of time! if self.refresh: # Simply grab every single item if we want to refresh diff --git a/resources/lib/library_sync/additional_metadata_tmdb.py b/resources/lib/library_sync/additional_metadata_tmdb.py index c68e57144..e66510d27 100644 --- a/resources/lib/library_sync/additional_metadata_tmdb.py +++ b/resources/lib/library_sync/additional_metadata_tmdb.py @@ -48,13 +48,13 @@ def get_tmdb_details(unique_ids): def process_trailers(plex_id, plex_type, refresh=False): done = True try: - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_id(plex_id, plex_type) if not db_item: logger.error('Could not get Kodi id for %s %s', plex_type, plex_id) done = False return - with KodiVideoDB() as kodidb: + with KodiVideoDB(lock=False) as kodidb: trailer = kodidb.get_trailer(db_item['kodi_id'], db_item['kodi_type']) if trailer and (trailer.startswith(f'plugin://{v.ADDON_ID}') or @@ -103,14 +103,14 @@ def process_fanart(plex_id, plex_type, refresh=False): done = True try: artworks = None - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_id(plex_id, plex_type) if not db_item: logger.error('Could not get Kodi id for %s %s', plex_type, plex_id) done = False return if not refresh: - with KodiVideoDB() as kodidb: + with KodiVideoDB(lock=False) as kodidb: artworks = kodidb.get_art(db_item['kodi_id'], db_item['kodi_type']) # Check if we even need to get additional art diff --git a/resources/lib/library_sync/nodes.py b/resources/lib/library_sync/nodes.py index 7a922775a..f73647d59 100644 --- a/resources/lib/library_sync/nodes.py +++ b/resources/lib/library_sync/nodes.py @@ -88,6 +88,14 @@ 'section_id': '{self.section_id}' }, v.CONTENT_TYPE_MOVIE), + ('watchlist', + utils.lang(39212), # "Watchlist" + { + 'mode': 'watchlist', + 'key': '/library/sections/{self.section_id}/watchlist', + 'section_id': '{self.section_id}' + }, + v.CONTENT_TYPE_MOVIE), ('browse', utils.lang(39702), # "Browse by folder" { @@ -175,6 +183,14 @@ 'section_id': '{self.section_id}' }, v.CONTENT_TYPE_EPISODE), + ('watchlist', + utils.lang(39212), # "Watchlist" + { + 'mode': 'watchlist', + 'key': '/library/sections/{self.section_id}/watchlist', + 'section_id': '{self.section_id}' + }, + v.CONTENT_TYPE_SHOW), ('browse', utils.lang(39702), # "Browse by folder" { @@ -391,3 +407,8 @@ def node_more(section, node_name, args): def node_plex_sets(section, node_name, args): return _folder_template(section, node_name, args) + + +def node_watchlist(section, node_name, args): + return _folder_template(section, node_name, args) + diff --git a/resources/lib/library_sync/sections.py b/resources/lib/library_sync/sections.py index 153ed0167..3df3361b1 100644 --- a/resources/lib/library_sync/sections.py +++ b/resources/lib/library_sync/sections.py @@ -649,7 +649,7 @@ def _sync_from_pms(pick_libraries): if api.plex_type in v.UNSUPPORTED_PLEX_TYPES: continue sections.append(Section(index=i, xml_element=xml_element)) - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: for section_db in plexdb.all_sections(): old_sections.append(Section(section_db_element=section_db)) # Update our latest PMS sections with info saved in the PMS DB diff --git a/resources/lib/library_sync/websocket.py b/resources/lib/library_sync/websocket.py index 5765655c3..4545f0b7d 100644 --- a/resources/lib/library_sync/websocket.py +++ b/resources/lib/library_sync/websocket.py @@ -205,7 +205,8 @@ def store_activity_message(data): elif message['Activity']['type'] != 'library.refresh.items': # Not the type of message relevant for us continue - elif message['Activity']['Context']['refreshed'] != True: + elif message['Activity']['Context'].get('refreshed') is not None and \ + message['Activity']['Context']['refreshed'] == False: # The item was scanned but not actually refreshed continue plex_id = PF.GetPlexKeyNumber(message['Activity']['Context']['key'])[1] @@ -368,6 +369,6 @@ def cache_artwork(plex_id, plex_type, kodi_id=None, kodi_type=None): LOG.error('Could not retrieve Plex db info for %s', plex_id) return kodi_id, kodi_type = item['kodi_id'], item['kodi_type'] - with kodi_db.KODIDB_FROM_PLEXTYPE[plex_type]() as kodidb: + with kodi_db.KODIDB_FROM_PLEXTYPE[plex_type](lock=False) as kodidb: for url in kodidb.art_urls(kodi_id, kodi_type): artwork.cache_url(url) diff --git a/resources/lib/playlists/db.py b/resources/lib/playlists/db.py index a309f01be..a73643dbe 100644 --- a/resources/lib/playlists/db.py +++ b/resources/lib/playlists/db.py @@ -22,7 +22,7 @@ def plex_playlist_ids(): """ Returns a list of all Plex ids of the playlists already in our DB """ - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: return list(plexdb.playlist_ids()) @@ -30,7 +30,7 @@ def kodi_playlist_paths(): """ Returns a list of all Kodi playlist paths of the playlists already synced """ - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: return list(plexdb.kodi_playlist_paths()) @@ -53,7 +53,7 @@ def get_playlist(path=None, plex_id=None): Returns the playlist as a Playlist for either the plex_id or path """ playlist = Playlist() - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: playlist = plexdb.playlist(playlist, plex_id, path) return playlist @@ -62,7 +62,7 @@ def get_all_kodi_playlist_paths(): """ Returns a list with all paths for the playlists on the Kodi side """ - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: paths = list(plexdb.all_kodi_paths()) return paths @@ -112,7 +112,7 @@ def m3u_to_plex_ids(playlist): db_type=playlist.kodi_type) if not kodi_id: continue - with PlexDB() as plexdb: + with PlexDB(lock=False) as plexdb: item = plexdb.item_by_kodi_id(kodi_id, kodi_type) if item: plex_ids.append(item['plex_id']) diff --git a/resources/lib/plex_api/__init__.py b/resources/lib/plex_api/__init__.py index 33c53d8c6..28134d0a4 100644 --- a/resources/lib/plex_api/__init__.py +++ b/resources/lib/plex_api/__init__.py @@ -17,14 +17,37 @@ class API(Base, Artwork, File, Media, User, Playback): pass -def mass_api(xml): +def mass_api(xml, check_by_guid=False): """ Pass in an entire XML PMS response with e.g. several movies or episodes Will Look-up Kodi ids in the Plex.db for every element (thus speeding up this process for several PMS items!) + Avoid a lookup by guid instead of the plex id (check_by_guid=True) as + it will be way slower """ apis = [API(x) for x in xml] - with PlexDB(lock=False) as plexdb: - for api in apis: - api.check_db(plexdb=plexdb) - return apis + if check_by_guid: + # A single guid might return a bunch of different plex id's + # We extend the xml list with these ids + new_apis = list() + with PlexDB(lock=False) as plexdb: + for i, api in enumerate(apis): + items = api.check_by_guid(plexdb=plexdb) + for item in items: + new_api = apis[i] + # Since we cannot simply set the plex_id and type. + # This will overwrite a weird "guid ratingKey" that + # plex set, originally looking e.g. like + # ratingKey="5d776883ebdf2200209c104e" + if item['plex_type']: + new_api.xml.set('type', item['plex_type']) + new_api.xml.set('ratingKey', str(item['plex_id'])) + new_api.xml.set('key', f'/library/metadata/{item["plex_id"]}') + new_api.check_db(plexdb=plexdb) + new_apis.append(new_api) + return new_apis + else: + with PlexDB(lock=False) as plexdb: + for api in apis: + api.check_db(plexdb=plexdb) + return apis diff --git a/resources/lib/plex_api/base.py b/resources/lib/plex_api/base.py index 6c74a39d2..a491efdbe 100644 --- a/resources/lib/plex_api/base.py +++ b/resources/lib/plex_api/base.py @@ -46,6 +46,7 @@ def __init__(self, xml): self._producers = [] self._locations = [] self._markers = [] + self._labels = [] self._guids = {} self._coll_match = None # Plex DB attributes @@ -84,6 +85,16 @@ def plex_id(self): """ return cast(int, self.xml.get('ratingKey')) + @property + def plex_guid(self): + """ + Returns the Plex guid as unicode or None. Note that you can get + SEVERAL plex_ids per Plex guid as you have unique Plex id's per + edition (e.g. director's cut, normal cut, ...) but potentially + only a single guid per "movie" encompassing all editions + """ + return self.xml.get('guid') + @property def fast_key(self): """ @@ -181,6 +192,21 @@ def check_db(self, plexdb=None): if 'fanart_synced' in db_item: self._fanart_synced = db_item['fanart_synced'] + def check_by_guid(self, plexdb=None): + """ + Returns a list of db_items synced to Kodi by using the Plex guid or an + empty list. There can be several items matching a single guid, e.g. + movie editions. + """ + if self.plex_guid is None: + return list() + if plexdb: + db_items = plexdb.items_by_guid(self.plex_guid, self.plex_type) + else: + with PlexDB(lock=False) as plexdb: + db_items = plexdb.items_by_guid(self.plex_guid, self.plex_type) + return db_items + def path_and_plex_id(self): """ Returns the Plex key such as '/library/metadata/246922' or None @@ -529,6 +555,8 @@ def _scan_children(self): end / 1000.0, child.get('type'), child.get('final') == '1')) + elif child.tag == 'Label': + self._labels.append(child.get('tag')) # Plex Movie agent (legacy) or "normal" Plex tv show agent if not self._guids: guid = self.xml.get('guid') @@ -630,6 +658,13 @@ def people(self): 'writer': [(x, ) for x in self._writers] } + def labels(self): + """ + Returns a list of labels found + """ + self._scan_children() + return self._labels + def extras(self): """ Returns an iterator for etree elements for each extra, e.g. trailers diff --git a/resources/lib/plex_api/file.py b/resources/lib/plex_api/file.py index 7c367fd0f..8d2dbce0f 100644 --- a/resources/lib/plex_api/file.py +++ b/resources/lib/plex_api/file.py @@ -1,8 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from logging import getLogger from .. import utils, variables as v, app +LOG = getLogger('PLEX.api.file') + def _transcode_image_path(key, AuthToken, path, width, height): """ Transcode Image support @@ -121,10 +124,15 @@ def directory_path(self, section_id=None, plex_type=None, old_key=None, key = self.xml.get('fastKey') if not key: key = self.xml.get('key') - if old_key: - key = '%s/%s' % (old_key, key) - elif not key.startswith('/'): - key = '/library/sections/%s/%s' % (section_id, key) + LOG.debug('directory_path. section_id: %s, key: %s, old_key: %s', section_id, key, old_key) + # the key returned by plex might be an absolute path, ex: "/library/sections/1/folder?parent=3030" + # we should directly use the key if it starts with a slash + if not key.startswith('/'): + if old_key: + key = '%s/%s' % (old_key, key) + else: + key = '/library/sections/%s/%s' % (section_id, key) + LOG.debug('directory_path. browseplex key will be "%s"', key) params = { 'mode': 'browseplex', 'key': key diff --git a/resources/lib/plex_db/common.py b/resources/lib/plex_db/common.py index d3d39a141..9064047fc 100644 --- a/resources/lib/plex_db/common.py +++ b/resources/lib/plex_db/common.py @@ -92,6 +92,32 @@ def item_by_id(self, plex_id, plex_type=None): break return answ + def items_by_guid(self, plex_guid, plex_type=None): + """ + Returns a list of items for plex_guid or an empty list. + Supply with the correct plex_type to speed up lookup + """ + answ = list() + if plex_type == v.PLEX_TYPE_MOVIE: + answ = self.movies_by_guid(plex_guid) + elif plex_type == v.PLEX_TYPE_EPISODE: + answ = self.episodes_by_guid(plex_guid) + elif plex_type == v.PLEX_TYPE_SHOW: + answ = self.shows_by_guid(plex_guid) + elif plex_type == v.PLEX_TYPE_SEASON: + answ = self.seasons_by_guid(plex_guid) + elif plex_type is None: + # SLOW - lookup plex_id in all our tables + for kind in (v.PLEX_TYPE_MOVIE, + v.PLEX_TYPE_EPISODE, + v.PLEX_TYPE_SHOW, + v.PLEX_TYPE_SEASON): + method = getattr(self, kind + "s_by_guid") + answ = method(plex_guid) + if answ: + break + return answ + def item_by_kodi_id(self, kodi_id, kodi_type): """ """ @@ -223,6 +249,7 @@ def initialize(): plexdb.cursor.execute(''' CREATE TABLE IF NOT EXISTS movie( plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, kodi_id INTEGER, @@ -235,6 +262,7 @@ def initialize(): plexdb.cursor.execute(''' CREATE TABLE IF NOT EXISTS show( plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, kodi_id INTEGER, @@ -245,6 +273,7 @@ def initialize(): plexdb.cursor.execute(''' CREATE TABLE IF NOT EXISTS season( plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, show_id INTEGER, @@ -256,6 +285,7 @@ def initialize(): plexdb.cursor.execute(''' CREATE TABLE IF NOT EXISTS episode( plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, show_id INTEGER, @@ -313,12 +343,16 @@ def initialize(): commands = ( 'CREATE INDEX IF NOT EXISTS ix_movie_1 ON movie (last_sync)', 'CREATE UNIQUE INDEX IF NOT EXISTS ix_movie_2 ON movie (kodi_id)', + 'CREATE INDEX IF NOT EXISTS ix_movie_3 ON movie (plex_guid)', 'CREATE INDEX IF NOT EXISTS ix_show_1 ON show (last_sync)', 'CREATE UNIQUE INDEX IF NOT EXISTS ix_show_2 ON show (kodi_id)', + 'CREATE INDEX IF NOT EXISTS ix_show_3 ON show (plex_guid)', 'CREATE INDEX IF NOT EXISTS ix_season_1 ON season (last_sync)', 'CREATE UNIQUE INDEX IF NOT EXISTS ix_season_2 ON season (kodi_id)', + 'CREATE INDEX IF NOT EXISTS ix_season_3 ON season (plex_guid)', 'CREATE INDEX IF NOT EXISTS ix_episode_1 ON episode (last_sync)', 'CREATE UNIQUE INDEX IF NOT EXISTS ix_episode_2 ON episode (kodi_id)', + 'CREATE INDEX IF NOT EXISTS ix_episode_3 ON season (plex_guid)', 'CREATE INDEX IF NOT EXISTS ix_artist_1 ON artist (last_sync)', 'CREATE UNIQUE INDEX IF NOT EXISTS ix_artist_2 ON artist (kodi_id)', 'CREATE INDEX IF NOT EXISTS ix_album_1 ON album (last_sync)', diff --git a/resources/lib/plex_db/movies.py b/resources/lib/plex_db/movies.py index 38c7a5518..91953d801 100644 --- a/resources/lib/plex_db/movies.py +++ b/resources/lib/plex_db/movies.py @@ -4,7 +4,7 @@ class Movies(object): - def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid, + def add_movie(self, plex_id, plex_guid, checksum, section_id, kodi_id, kodi_fileid, kodi_pathid, trailer_synced, last_sync): """ Appends or replaces an entry into the plex table for movies @@ -12,6 +12,7 @@ def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid, query = ''' INSERT OR REPLACE INTO movie( plex_id, + plex_guid, checksum, section_id, kodi_id, @@ -20,11 +21,12 @@ def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid, fanart_synced, trailer_synced, last_sync) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''' self.cursor.execute( query, (plex_id, + plex_guid, checksum, section_id, kodi_id, @@ -38,6 +40,7 @@ def movie(self, plex_id): """ Returns the show info as a tuple for the TV show with plex_id: plex_id INTEGER PRIMARY KEY ASC, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, kodi_id INTEGER, @@ -52,6 +55,26 @@ def movie(self, plex_id): (plex_id, )) return self.entry_to_movie(self.cursor.fetchone()) + def movies_by_guid(self, plex_guid): + """ + Returns a list of movies or an empty list, each list element looking + like this: + plex_id INTEGER PRIMARY KEY ASC, + plex_guid TEXT, + checksum INTEGER UNIQUE, + section_id INTEGER, + kodi_id INTEGER, + kodi_fileid INTEGER, + kodi_pathid INTEGER, + fanart_synced INTEGER, + last_sync INTEGER + """ + if plex_guid is None: + return list() + self.cursor.execute('SELECT * FROM movie WHERE plex_guid = ?', + (plex_guid, )) + return list(self.entry_to_movie(x) for x in self.cursor.fetchall()) + @staticmethod def entry_to_movie(entry): if not entry: @@ -60,11 +83,12 @@ def entry_to_movie(entry): 'plex_type': v.PLEX_TYPE_MOVIE, 'kodi_type': v.KODI_TYPE_MOVIE, 'plex_id': entry[0], - 'checksum': entry[1], - 'section_id': entry[2], - 'kodi_id': entry[3], - 'kodi_fileid': entry[4], - 'kodi_pathid': entry[5], - 'fanart_synced': entry[6], - 'last_sync': entry[7] + 'plex_guid': entry[1], + 'checksum': entry[2], + 'section_id': entry[3], + 'kodi_id': entry[4], + 'kodi_fileid': entry[5], + 'kodi_pathid': entry[6], + 'fanart_synced': entry[7], + 'last_sync': entry[8] } diff --git a/resources/lib/plex_db/tvshows.py b/resources/lib/plex_db/tvshows.py index e7ba3e7c7..169d14830 100644 --- a/resources/lib/plex_db/tvshows.py +++ b/resources/lib/plex_db/tvshows.py @@ -3,7 +3,7 @@ from .. import variables as v class TVShows(object): - def add_show(self, plex_id, checksum, section_id, kodi_id, kodi_pathid, + def add_show(self, plex_id, plex_guid, checksum, section_id, kodi_id, kodi_pathid, last_sync): """ Appends or replaces tv show entry into the plex table @@ -12,15 +12,17 @@ def add_show(self, plex_id, checksum, section_id, kodi_id, kodi_pathid, ''' INSERT OR REPLACE INTO show( plex_id, + plex_guid, checksum, section_id, kodi_id, kodi_pathid, fanart_synced, last_sync) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', (plex_id, + plex_guid, checksum, section_id, kodi_id, @@ -28,7 +30,7 @@ def add_show(self, plex_id, checksum, section_id, kodi_id, kodi_pathid, 0, last_sync)) - def add_season(self, plex_id, checksum, section_id, show_id, parent_id, + def add_season(self, plex_id, plex_guid, checksum, section_id, show_id, parent_id, kodi_id, last_sync): """ Appends or replaces an entry into the plex table @@ -37,6 +39,7 @@ def add_season(self, plex_id, checksum, section_id, show_id, parent_id, ''' INSERT OR REPLACE INTO season( plex_id, + plex_guid, checksum, section_id, show_id, @@ -44,9 +47,10 @@ def add_season(self, plex_id, checksum, section_id, show_id, parent_id, kodi_id, fanart_synced, last_sync) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (plex_id, + plex_guid, checksum, section_id, show_id, @@ -55,7 +59,7 @@ def add_season(self, plex_id, checksum, section_id, show_id, parent_id, 0, last_sync)) - def add_episode(self, plex_id, checksum, section_id, show_id, + def add_episode(self, plex_id, plex_guid, checksum, section_id, show_id, grandparent_id, season_id, parent_id, kodi_id, kodi_fileid, kodi_fileid_2, kodi_pathid, last_sync): """ @@ -65,6 +69,7 @@ def add_episode(self, plex_id, checksum, section_id, show_id, ''' INSERT OR REPLACE INTO episode( plex_id, + plex_guid, checksum, section_id, show_id, @@ -77,9 +82,10 @@ def add_episode(self, plex_id, checksum, section_id, show_id, kodi_pathid, fanart_synced, last_sync) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (plex_id, + plex_guid, checksum, section_id, show_id, @@ -97,6 +103,7 @@ def show(self, plex_id): """ Returns the show info as a tuple for the TV show with plex_id: plex_id INTEGER PRIMARY KEY ASC, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, kodi_id INTEGER, @@ -110,10 +117,30 @@ def show(self, plex_id): (plex_id, )) return self.entry_to_show(self.cursor.fetchone()) + def shows_by_guid(self, plex_guid): + """ + Returns a list of shows or an empty list, each element being a + dictionary looking like this: + plex_id INTEGER PRIMARY KEY ASC, + plex_guid TEXT, + checksum INTEGER UNIQUE, + section_id INTEGER, + kodi_id INTEGER, + kodi_pathid INTEGER, + fanart_synced INTEGER, + last_sync INTEGER + """ + if plex_guid is None: + return list() + self.cursor.execute('SELECT * FROM show WHERE plex_guid = ?', + (plex_guid, )) + return list(self.entry_to_show(x) for x in self.cursor.fetchall()) + def season(self, plex_id): """ Returns the show info as a tuple for the TV show with plex_id: plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, checksum INTEGER UNIQUE, section_id INTEGER, show_id INTEGER, # plex_id of the parent show @@ -128,6 +155,26 @@ def season(self, plex_id): (plex_id, )) return self.entry_to_season(self.cursor.fetchone()) + def seasons_by_guid(self, plex_guid): + """ + Returns a list of seasons or an empty list, with each list element + being a dictionary looking like this: + plex_id INTEGER PRIMARY KEY, + plex_guid TEXT, + checksum INTEGER UNIQUE, + section_id INTEGER, + show_id INTEGER, # plex_id of the parent show + parent_id INTEGER, # kodi_id of the parent show + kodi_id INTEGER, + fanart_synced INTEGER, + last_sync INTEGER + """ + if plex_guid is None: + return list() + self.cursor.execute('SELECT * FROM season WHERE plex_guid = ?', + (plex_guid, )) + return list(self.entry_to_season(x) for x in self.cursor.fetchall()) + def episode(self, plex_id): if plex_id is None: return @@ -135,6 +182,33 @@ def episode(self, plex_id): (plex_id, )) return self.entry_to_episode(self.cursor.fetchone()) + def episodes_by_guid(self, plex_guid): + """ + Returns a list of episodes or an empty list, each list element being + a dictionary with the following keys: + plex_type + kodi_type + plex_id + plex_guid + checksum + section_id + show_id + grandparent_id + season_id + parent_id + kodi_id + kodi_fileid + kodi_fileid_2 + kodi_pathid + fanart_synced + last_sync + """ + if plex_guid is None: + return list() + self.cursor.execute('SELECT * FROM episode WHERE plex_guid = ?', + (plex_guid, )) + return list(self.entry_to_episode(x) for x in self.cursor.fetchall()) + @staticmethod def entry_to_episode(entry): if not entry: @@ -143,18 +217,19 @@ def entry_to_episode(entry): 'plex_type': v.PLEX_TYPE_EPISODE, 'kodi_type': v.KODI_TYPE_EPISODE, 'plex_id': entry[0], - 'checksum': entry[1], - 'section_id': entry[2], - 'show_id': entry[3], - 'grandparent_id': entry[4], - 'season_id': entry[5], - 'parent_id': entry[6], - 'kodi_id': entry[7], - 'kodi_fileid': entry[8], - 'kodi_fileid_2': entry[9], - 'kodi_pathid': entry[10], - 'fanart_synced': entry[11], - 'last_sync': entry[12] + 'plex_guid': entry[1], + 'checksum': entry[2], + 'section_id': entry[3], + 'show_id': entry[4], + 'grandparent_id': entry[5], + 'season_id': entry[6], + 'parent_id': entry[7], + 'kodi_id': entry[8], + 'kodi_fileid': entry[9], + 'kodi_fileid_2': entry[10], + 'kodi_pathid': entry[11], + 'fanart_synced': entry[12], + 'last_sync': entry[13] } @staticmethod @@ -165,12 +240,13 @@ def entry_to_show(entry): 'plex_type': v.PLEX_TYPE_SHOW, 'kodi_type': v.KODI_TYPE_SHOW, 'plex_id': entry[0], - 'checksum': entry[1], - 'section_id': entry[2], - 'kodi_id': entry[3], - 'kodi_pathid': entry[4], - 'fanart_synced': entry[5], - 'last_sync': entry[6] + 'plex_guid': entry[1], + 'checksum': entry[2], + 'section_id': entry[3], + 'kodi_id': entry[4], + 'kodi_pathid': entry[5], + 'fanart_synced': entry[6], + 'last_sync': entry[7] } @staticmethod @@ -181,13 +257,14 @@ def entry_to_season(entry): 'plex_type': v.PLEX_TYPE_SEASON, 'kodi_type': v.KODI_TYPE_SEASON, 'plex_id': entry[0], - 'checksum': entry[1], - 'section_id': entry[2], - 'show_id': entry[3], - 'parent_id': entry[4], - 'kodi_id': entry[5], - 'fanart_synced': entry[6], - 'last_sync': entry[7] + 'plex_guid': entry[1], + 'checksum': entry[2], + 'section_id': entry[3], + 'show_id': entry[4], + 'parent_id': entry[5], + 'kodi_id': entry[6], + 'fanart_synced': entry[7], + 'last_sync': entry[8] } def season_has_episodes(self, plex_id): diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index d0b81532e..19a9b3ad5 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -11,6 +11,7 @@ from . import kodimonitor from . import sync, library_sync from . import websocket_client +from . import plex_db from . import plex_companion from . import plex_functions as PF from . import playback_starter @@ -19,6 +20,7 @@ from . import loghandler from . import backgroundthread from . import skip_plex_markers +from . import downloadutils from .windows import userselect ############################################################################### @@ -240,6 +242,53 @@ def toggle_plex_tv(self): # Enable the main loop to continue app.APP.suspend = False + def watchlist_add(self, raw_params): + return self.watchlist_modify('addToWatchlist', raw_params) + + def watchlist_remove(self, raw_params): + return self.watchlist_modify('removeFromWatchlist', raw_params) + + def watchlist_modify(self, api_type, raw_params): + params = dict(utils.parse_qsl(raw_params)) + kodi_id = params.get('kodi_id') + kodi_type = params.get('kodi_type') + + LOG.info('watchlist_modify %s %s %s', api_type, kodi_id, kodi_type) + + watchlist_plex_guid = None + + with plex_db.PlexDB(lock=False) as plexdb: + plex_item = plexdb.item_by_kodi_id(kodi_id, kodi_type) + if not plex_item: + return False + + plex_guid = plex_item['plex_guid'] + if not plex_guid: + return False + + plex_type = plex_item['plex_type'] + + if plex_type == v.PLEX_TYPE_MOVIE or plex_type == v.PLEX_TYPE_SHOW: + watchlist_plex_guid = plex_guid + + elif plex_type == v.PLEX_TYPE_SEASON or plex_type == v.PLEX_TYPE_EPISODE: + plex_show_item = plexdb.item_by_id(plex_item['show_id'], v.PLEX_TYPE_SHOW) + if plex_show_item: + watchlist_plex_guid = plex_show_item['plex_guid'] + + if watchlist_plex_guid is None: + return False + + # ratingKey query param accepts the last section in the plex_guid + watchlist_rating_key = watchlist_plex_guid.split('/')[-1] + + downloadutils.DownloadUtils().downloadUrl('https://discover.provider.plex.tv/actions/%s?ratingKey=%s' % (api_type, watchlist_rating_key), + action_type = 'PUT', + authenticate=False, + headerOptions={'X-Plex-Token': utils.window('plex_token')}) + + xbmc.executebuiltin('UpdateLibrary(video)') + def authenticate(self): """ Authenticate the current user or prompt to log-in @@ -473,6 +522,12 @@ def ServiceEntryPoint(self): task = playback_starter.PlaybackTask( 'dummy?mode=context_menu&%s' % plex_command.replace('CONTEXT_menu?', '')) + elif plex_command.startswith('WATCHLIST_ADD?'): + task = backgroundthread.FunctionAsTask( + self.watchlist_add, None, plex_command.replace('WATCHLIST_ADD?', '')) + elif plex_command.startswith('WATCHLIST_REMOVE?'): + task = backgroundthread.FunctionAsTask( + self.watchlist_remove, None, plex_command.replace('WATCHLIST_REMOVE?', '')) elif plex_command == 'choose_pms_server': task = backgroundthread.FunctionAsTask( self.choose_pms_server, None) diff --git a/resources/lib/skip_plex_markers.py b/resources/lib/skip_plex_markers.py index 8531e10cc..b42cd721c 100644 --- a/resources/lib/skip_plex_markers.py +++ b/resources/lib/skip_plex_markers.py @@ -12,44 +12,65 @@ 'commercial': (utils.lang(30530), 'enableSkipCommercials', 'enableAutoSkipCommercials'), # Skip commercial } -def skip_markers(markers): +def skip_markers(markers, markers_hidden): try: progress = app.APP.player.getTime() except RuntimeError: # XBMC is not playing any media file yet return - within_marker = False + within_marker = None marker_definition = None for start, end, typus, _ in markers: marker_definition = MARKERS[typus] if utils.settings(marker_definition[1]) == "true" and start <= progress < end: - within_marker = True + within_marker = typus break - if within_marker and app.APP.skip_markers_dialog is None: - # WARNING: This Dialog only seems to work if called from the main - # thread. Otherwise, onClick and onAction won't work - app.APP.skip_markers_dialog = SkipMarkerDialog( - 'script-plex-skip_marker.xml', - v.ADDON_PATH, - 'default', - '1080i', - marker_message=marker_definition[0], - marker_end=end) - if utils.settings(marker_definition[2]) == "true": - app.APP.skip_markers_dialog.seekTimeToEnd() - else: - app.APP.skip_markers_dialog.show() - elif not within_marker and app.APP.skip_markers_dialog is not None: + elif typus in markers_hidden: + # reset the marker when escaping its time window + # this allows the skip button to show again if you rewind + del markers_hidden[typus] + if within_marker is not None: + if within_marker in markers_hidden: + # the user did not click the button within the enableAutoHideSkipTime time + # so it was hidden. don't show this marker + return + + if app.APP.skip_markers_dialog is None: + # WARNING: This Dialog only seems to work if called from the main + # thread. Otherwise, onClick and onAction won't work + app.APP.skip_markers_dialog = SkipMarkerDialog( + 'script-plex-skip_marker.xml', + v.ADDON_PATH, + 'default', + '1080i', + marker_message=marker_definition[0], + marker_end=end, + creation_time=progress) + if utils.settings(marker_definition[2]) == "true": + app.APP.skip_markers_dialog.seekTimeToEnd() + else: + app.APP.skip_markers_dialog.show() + + elif utils.settings("enableAutoHideSkip") == "true" and \ + app.APP.skip_markers_dialog.creation_time is not None and \ + (progress - app.APP.skip_markers_dialog.creation_time) > int(utils.settings("enableAutoHideSkipTime")): + # the dialog has been open for more than X seconds, so close it and + # mark it as hidden so it won't show up again within the start/end window + markers_hidden[within_marker] = True + app.APP.skip_markers_dialog.close() + app.APP.skip_markers_dialog = None + + elif app.APP.skip_markers_dialog is not None: app.APP.skip_markers_dialog.close() app.APP.skip_markers_dialog = None - def check(): with app.APP.lock_playqueues: if len(app.PLAYSTATE.active_players) != 1: return playerid = list(app.PLAYSTATE.active_players)[0] markers = app.PLAYSTATE.player_states[playerid]['markers'] + markers_hidden = app.PLAYSTATE.player_states[playerid]['markers_hidden'] if not markers: return - skip_markers(markers) + skip_markers(markers, markers_hidden) diff --git a/resources/lib/sync.py b/resources/lib/sync.py index 9eb5c5193..fdb0e9e3b 100644 --- a/resources/lib/sync.py +++ b/resources/lib/sync.py @@ -229,7 +229,8 @@ def _run_internal(self): # this once a while (otherwise, potentially many screen # refreshes lead to flickering) if (library_sync.WEBSOCKET_MESSAGES and - now - last_websocket_processing > 5): + now - last_websocket_processing > 5 and + (utils.settings('delayBackgroundSyncWhilePlaying') == "false" or not app.APP.is_playing_video)): last_websocket_processing = now library_sync.process_websocket_messages() # See if there is a PMS message we need to handle diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 4978e44e2..2e2d2a9dd 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -475,7 +475,7 @@ def wipe_database(reboot=True): from .playlists import remove_synced_playlists remove_synced_playlists() try: - with plex_db.PlexDB() as plexdb: + with plex_db.PlexDB(lock=False) as plexdb: if plexdb.songs_have_been_synced(): LOG.info('Detected that music has also been synced - wiping music') music = True diff --git a/resources/lib/variables.py b/resources/lib/variables.py index c1fb72862..d18985917 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -104,7 +104,7 @@ PKC_MACHINE_IDENTIFIER = None # Minimal PKC version needed for the Kodi database - otherwise need to recreate -MIN_DB_VERSION = '3.8.8' +MIN_DB_VERSION = '3.9.4' DB_VIDEO_VERSION = None DB_VIDEO_PATH = None diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 9999b7641..6dc48dd76 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -25,6 +25,10 @@ # Need to chain the PMS keys KEY = None +# use getVideoInfoTag to set some list item properties +USE_TAGS = v.KODIVERSION >= 20 +# properties that should be set by tag methods +TAG_PROPERTIES = ("resumetime", "totaltime") def get_clean_image(image): ''' @@ -172,7 +176,7 @@ def _generate_content(api): 'season': api.season_number(), 'sorttitle': api.sorttitle(), # 'Titans (2018)' 'studio': api.studios(), - 'tag': [], # List of tags this item belongs to + 'tag': api.labels(), # List of tags this item belongs to 'tagline': api.tagline(), 'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv' 'title': api.title(), # 'Titans (2018)' @@ -245,7 +249,7 @@ def _generate_content(api): return item -def prepare_listitem(item): +def prepare_listitem(item, listing_key = None): """helper to convert kodi output from json api to compatible format for listitems""" try: @@ -312,6 +316,9 @@ def prepare_listitem(item): properties["type"] = item["type"] properties["path"] = item.get("file") + if listing_key is not None: + properties["LISTINGKEY"] = listing_key + # cast list_cast = [] list_castandrole = [] @@ -468,6 +475,9 @@ def create_listitem(item, as_tuple=True, offscreen=True, listitem=xbmcgui.ListItem): """helper to create a kodi listitem from kodi compatible dict with mediainfo""" try: + # PKCListItem does not implement getVideoInfoTag + use_tags_for_item = USE_TAGS and listitem == xbmcgui.ListItem + liz = listitem( label=item.get("label", ""), label2=item.get("label2", ""), @@ -488,7 +498,9 @@ def create_listitem(item, as_tuple=True, offscreen=True, # extra properties for key, value in item["extraproperties"].items(): - liz.setProperty(key, value) + # some Video properties should be set via tags on newer kodi versions + if nodetype != "Video" or not use_tags_for_item or key not in TAG_PROPERTIES: + liz.setProperty(key, value) # video infolabels if nodetype == "Video": @@ -513,6 +525,7 @@ def create_listitem(item, as_tuple=True, offscreen=True, "sorttitle": item.get("sorttitle"), "duration": item.get("duration"), "studio": item.get("studio"), + "tag": item.get("tag"), "tagline": item.get("tagline"), "writer": item.get("writer"), "tvshowtitle": item.get("tvshowtitle"), @@ -533,15 +546,26 @@ def create_listitem(item, as_tuple=True, offscreen=True, # streamdetails if item.get("streamdetails"): - liz.addStreamInfo("video", item["streamdetails"].get("video", {})) - liz.addStreamInfo("audio", item["streamdetails"].get("audio", {})) - liz.addStreamInfo("subtitle", item["streamdetails"].get("subtitle", {})) + if use_tags_for_item: + tags = liz.getVideoInfoTag() + tags.addVideoStream(_create_VideoStreamDetail(item["streamdetails"].get("video", {}))) + tags.addAudioStream(_create_AudioStreamDetail(item["streamdetails"].get("audio", {}))) + tags.addSubtitleStream(_create_SubtitleStreamDetail(item["streamdetails"].get("subtitle", {}))) + + else: + liz.addStreamInfo("video", item["streamdetails"].get("video", {})) + liz.addStreamInfo("audio", item["streamdetails"].get("audio", {})) + liz.addStreamInfo("subtitle", item["streamdetails"].get("subtitle", {})) if "dateadded" in item: infolabels["dateadded"] = item["dateadded"] if "date" in item: infolabels["date"] = item["date"] + if use_tags_for_item and "resumetime" in item["extraproperties"] and "totaltime" in item["extraproperties"]: + tags = liz.getVideoInfoTag() + tags.setResumePoint(float(item["extraproperties"].get("resumetime")), float(item["extraproperties"].get("totaltime"))); + # music infolabels elif nodetype == 'Music': infolabels = { @@ -581,7 +605,107 @@ def create_listitem(item, as_tuple=True, offscreen=True, infolabels["lastplayed"] = item["lastplayed"] # assign the infolabels - liz.setInfo(type=nodetype, infoLabels=infolabels) + if use_tags_for_item and nodetype == "Video": + # filter out None valued properties + infolabels = {k: v for k, v in infolabels.items() if v is not None} + + tags = liz.getVideoInfoTag() # type: xbmc.InfoTagVideo + + if "dbid" in infolabels: + tags.setDbId(int(infolabels["dbid"])) + if "year" in infolabels: + tags.setYear(int(infolabels["year"])) + if "episode" in infolabels: + tags.setEpisode(int(infolabels["episode"])) + if "season" in infolabels: + tags.setSeason(int(infolabels["season"])) + if "top250" in infolabels: + tags.setTop250(int(infolabels["top250"])) + if "tracknumber" in infolabels: + tags.setTrackNumber(int(infolabels["tracknumber"])) + if "rating" in infolabels: + tags.setRating(float(infolabels["rating"])) + if "playcount" in infolabels: + tags.setPlaycount(int(infolabels["playcount"])) + if "cast" in infolabels: + actors = [] + + for actor_name in infolabels["cast"]: + actors.append(xbmc.Actor(actor_name)) + + tags.setCast(actors) + if "castandrole" in infolabels: + actors = [] + + for actor in infolabels["castandrole"]: + actors.append(xbmc.Actor(actor[0], actor[1])) + + tags.setCast(actors) + if "artist" in infolabels: + tags.setArtists(infolabels["artist"]) + if "genre" in infolabels: + tags.setGenres(infolabels["genre"].split(" / ")) + if "country" in infolabels: + tags.setCountries(infolabels["country"]) + if "director" in infolabels: + tags.setDirectors(infolabels["director"].split(" / ")) + if "mpaa" in infolabels: + tags.setMpaa(str(infolabels["mpaa"])) + if "plot" in infolabels: + tags.setPlot(str(infolabels["plot"])) + if "plotoutline" in infolabels: + tags.setPlotOutline(str(infolabels["plotoutline"])) + if "title" in infolabels: + tags.setTitle(str(infolabels["title"])) + if "originaltitle" in infolabels: + tags.setOriginalTitle(str(infolabels["originaltitle"])) + if "sorttitle" in infolabels: + tags.setSortTitle(str(infolabels["sorttitle"])) + if "duration" in infolabels: + tags.setDuration(int(infolabels["duration"])) + if "studio" in infolabels: + tags.setStudios(infolabels["studio"].split(" / ")) + if "tagline" in infolabels: + tags.setTagLine(str(infolabels["tagline"])) + if "writer" in infolabels: + tags.setWriters(infolabels["writer"].split(" / ")) + if "tvshowtitle" in infolabels: + tags.setTvShowTitle(str(infolabels["tvshowtitle"])) + if "premiered" in infolabels: + tags.setPremiered(str(infolabels["premiered"])) + if "status" in infolabels: + tags.setTvShowStatus(str(infolabels["status"])) + if "set" in infolabels: + tags.setSet(str(infolabels["set"])) + if "setoverview" in infolabels: + tags.setSetOverview(str(infolabels["setoverview"])) + if "tag" in infolabels: + tags.setTags(infolabels["tag"]) + if "imdbnumber" in infolabels: + tags.setIMDBNumber(str(infolabels["imdbnumber"])) + if "code" in infolabels: + tags.setProductionCode(str(infolabels["code"])) + if "aired" in infolabels: + tags.setFirstAired(str(infolabels["aired"])) + if "lastplayed" in infolabels: + tags.setLastPlayed(str(infolabels["lastplayed"])) + if "album" in infolabels: + tags.setAlbum(str(infolabels["album"])) + if "votes" in infolabels: + tags.setVotes(int(infolabels["votes"])) + if "trailer" in infolabels: + tags.setTrailer(str(infolabels["trailer"])) + if "path" in infolabels: + tags.setPath(str(infolabels["path"])) + if "filenameandpath" in infolabels: + tags.setFilenameAndPath(str(infolabels["filenameandpath"])) + if "dateadded" in infolabels: + tags.setDateAdded(str(infolabels["dateadded"])) + if "mediatype" in infolabels: + tags.setMediaType(str(infolabels["mediatype"])) + + else: + liz.setInfo(type=nodetype, infoLabels=infolabels) # artwork liz.setArt(item.get("art", {})) @@ -626,3 +750,51 @@ def create_main_entry(item): 'type': '', 'IsPlayable': 'false' } + + +def _create_VideoStreamDetail(stream): + '''Creates a VideoStreamDetail object from a video stream''' + stream_detail = xbmc.VideoStreamDetail() + + if "codec" in stream: + stream_detail.setCodec(str(stream["codec"])) + if "aspect" in stream: + stream_detail.setAspect(round(stream["aspect"], 2)) + if "width" in stream: + stream_detail.setWidth(int(stream["width"])) + if "height" in stream: + stream_detail.setHeight(int(stream["height"])) + if "duration" in stream: + stream_detail.setDuration(int(stream["duration"])) + if "stereomode" in stream: + stream_detail.setStereoMode(str(stream["stereomode"])) + if "language" in stream: + stream_detail.setLanguage(str(stream["language"])) + if "hdrtype" in stream: + stream_detail.setHDRType(str(stream["hdrtype"])) + + return stream_detail + + +def _create_AudioStreamDetail(stream): + '''Creates a AudioStreamDetail object from an audio stream''' + stream_detail = xbmc.AudioStreamDetail() + + if "channels" in stream: + stream_detail.setChannels(int(stream["channels"])) + if "codec" in stream: + stream_detail.setCodec(str(stream["codec"])) + if "language" in stream: + stream_detail.setLanguage(str(stream["language"])) + + return stream_detail + + +def _create_SubtitleStreamDetail(stream): + '''Creates a SubtitleStreamDetail object from a subtitle stream''' + stream_detail = xbmc.SubtitleStreamDetail() + + if "language" in stream: + stream_detail.setLanguage(str(stream["language"])) + + return stream_detail diff --git a/resources/lib/windows/skip_marker.py b/resources/lib/windows/skip_marker.py index 1e6cb01cd..b67a64fff 100644 --- a/resources/lib/windows/skip_marker.py +++ b/resources/lib/windows/skip_marker.py @@ -20,6 +20,7 @@ def __init__(self, *args, **kwargs): self.marker_message = kwargs.pop('marker_message') self.setProperty('marker_message', self.marker_message) self.marker_end = kwargs.pop('marker_end', None) + self.creation_time = kwargs.pop('creation_time', None) log.debug('SkipMarkerDialog with message %s, ends at %s', self.marker_message, self.marker_end) diff --git a/resources/settings.xml b/resources/settings.xml index ce9735091..e2c8c217c 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -78,6 +78,7 @@ + @@ -119,6 +120,8 @@ + +