diff --git a/addon.xml b/addon.xml index d3820df35..88a7c2ac4 100644 --- a/addon.xml +++ b/addon.xml @@ -1,70 +1,64 @@ - + - - - - - video - - + [String.IsEqual(Window(10000).Property(EmbyRecording),True) + String.StartsWith(ListItem.Path,"pvr://guide/") + !String.IsEmpty(ListItem.EPGEventIcon)] - + [String.IsEqual(Window(10000).Property(EmbySpecials),True) + String.Contains(ListItem.FileName,"-s-")] - + [String.IsEqual(Window(10000).Property(EmbyGoto),True) + String.IsEqual(ListItem.DBTYPE,episode)] - + [String.IsEqual(Window(10000).Property(EmbyGoto),True) + String.IsEqual(ListItem.DBTYPE,episode)] - + String.IsEqual(Window(10000).Property(EmbyDownload),True) + [[String.StartsWith(ListItem.FileName,"e-") | String.StartsWith(ListItem.FileName,"m-") | String.StartsWith(ListItem.FileName,"M-") | [[[String.IsEqual(ListItem.DBTYPE,"tvshow") | String.IsEqual(ListItem.DBTYPE,"season")] + !String.EndsWith(ListItem.Label," (download)")] + [String.Contains(ListItem.Path,"/emby_addon_mode/") | String.Contains(ListItem.Path,"http://127.0.0.1:57342/")]]] + !String.Contains(ListItem.Path,"/EMBY-offline-content/") + !String.Contains(ListItem.Path,"/dynamic/") + !String.IsEmpty(ListItem.DBID)] - + String.IsEqual(Window(10000).Property(EmbyDownload),True) + [String.Contains(ListItem.Path,"/EMBY-offline-content/") | [[String.IsEqual(ListItem.DBTYPE,"tvshow") | String.IsEqual(ListItem.DBTYPE,"season")] + String.EndsWith(ListItem.Label," (download)")]] - + [String.IsEqual(Window(10000).Property(EmbyFavourite),True) + String.IsEqual(Window(10000).Property(EmbyFavourite),True) + !String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"plugin://") + !String.StartsWith(ListItem.FolderPath,"favourites:") + !String.StartsWith(ListItem.Path,"pvr://")] - + [String.IsEqual(Window(10000).Property(EmbyRemote),True) + !String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"favourites:") + !String.IsEmpty(ListItem.Filename) + !String.IsEqual(ListItem.FileExtension,"m3u") + !String.StartsWith(ListItem.Path,"pvr://")] - + [String.IsEqual(Window(10000).Property(EmbyRemote),True) + !String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"favourites:") + !String.IsEmpty(ListItem.Filename) + !String.IsEqual(ListItem.FileExtension,"m3u") + !String.StartsWith(ListItem.Path,"pvr://")] - + [String.IsEqual(Window(10000).Property(EmbyRemote),True) + !String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"plugin://")] - + [String.IsEqual(Window(10000).Property(EmbyRemote),True) + String.IsEqual(Window(10000).Property(EmbyRemoteclient),True)] - + [String.IsEqual(Window(10000).Property(EmbyRefresh),True) + !String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"plugin://") + !String.StartsWith(ListItem.FolderPath,"favourites:") + !String.IsEmpty(ListItem.Filename) + !String.IsEqual(ListItem.FileExtension,"m3u") + !String.StartsWith(ListItem.Path,"pvr://")] - + [!String.StartsWith(ListItem.FolderPath,"library://") + !String.StartsWith(ListItem.FolderPath,"addons://") + !String.StartsWith(ListItem.FolderPath,"plugin://") + String.IsEqual(Window(10000).Property(EmbyDelete),True)] diff --git a/changelog.txt b/changelog.txt index 9012fdb26..f5118e561 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,55 @@ +10.0.30 +============= +fix transcoding playback + + + +10.0.29 +============= +intercept pillow library issue + + + +10.0.28 +============= +minor changes in Player session ops +fix websocket messages delay +fix session logout + + + +10.0.27 +============= +fix several minor http issues +faster https socket connection +support http redirects (including websocket connections) +fix Emby session stop +sync pause only for Emby scan tasks + + + +10.0.26 +============= +rewrite http communication with Emby server (remove urllib3 dependency, http comminicaion is now on low level socket-basis) +fix context menu +rewrite emby login handshakes +fix textoverlay position +fix minor artwork issues +remove song artwork from synced content (let Kodi handle it) +fix menu options in content helper plugins +fix library Id filters for dynamic nodes +fix dynamic node Genre for Audiobooks and Podcasts + + + +10.0.25 +============= +fix sort orders for dynamic nodes +improve performance for dynamic nodes +change plugin structure (fixes content detection issues for dynamic nodes) + + + 10.0.24 ============= fix server busy progress bar when server restarted or shutdown diff --git a/core/common.py b/core/common.py index f721b64aa..3b0e8c6e6 100644 --- a/core/common.py +++ b/core/common.py @@ -25,6 +25,7 @@ "Person": (('Primary', 'poster'), ("Art", 'clearart'), ("Banner", 'banner'), ("Thumb", 'thumb'), ("Thumb", 'landscape'), ("Backdrop", 'fanart'), ('Primary', 'thumb'), ("Primary", 'landscape')) } + def load_ExistingItem(Item, EmbyServer, emby_db, EmbyType): if Item['LibraryId'] not in EmbyServer.library.WhitelistUnique: xbmc.log(f"EMBY.core.common: Library not synced: {Item['LibraryId']}", 3) # LOGERROR @@ -593,7 +594,7 @@ def set_common(Item, ServerId, DynamicNode): if 'PrimaryImageTag' in ArtistItem: ArtistItem['imageurl'] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{ArtistItem['Id']}-0-p-{ArtistItem['PrimaryImageTag']}|redirect-limit=1000" else: - ArtistItem['imageurl'] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{ArtistItem['Id']}-0-p-0|redirect-limit=1000" + ArtistItem['imageurl'] = "" def set_Dates(Item): if 'ProductionYear' in Item: @@ -665,7 +666,7 @@ def set_chapters(Item, ServerId): else: ChapterImage = f"http://127.0.0.1:57342/picture/{ServerId}/p-{Item['Id']}-{index}-c-noimage-{quote(Chapter['Name'])}|redirect-limit=1000" - if not Chapter["StartPositionTicks"] in Chapters: + if Chapter["StartPositionTicks"] not in Chapters: Chapters[Chapter["StartPositionTicks"]] = ChapterImage else: # replace existing chapter label with marker label @@ -691,6 +692,12 @@ def set_KodiArtwork(Item, ServerId, DynamicNode): Item['SeriesPrimaryImageTag'] = Item.get('SeriesPrimaryImageTag', None) Item['KodiArtwork'] = {'clearart': None, 'clearlogo': None, 'discart': None, 'landscape': None, 'thumb': None, 'banner': None, 'poster': None, 'fanart': {}, 'favourite': None} + if not DynamicNode and Item['Type'] == "Audio": # no artwork for synced song content (Kodi handels that based on Albumart etc.) + if Item["AlbumPrimaryImageTag"] and "AlbumId" in Item: + Item['KodiArtwork']['favourite'] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{Item['AlbumId']}-0-p-{Item['AlbumPrimaryImageTag']}|redirect-limit=1000" + + return + if Item['Type'] in ImageTagsMappings: for ImageTagsMapping in ImageTagsMappings[Item['Type']]: EmbyArtworkId = None @@ -717,17 +724,20 @@ def set_KodiArtwork(Item, ServerId, DynamicNode): elif ImageTagsMapping[0] == "AlbumPrimary": if "AlbumId" in Item: EmbyArtworkId = Item["AlbumId"] - elif ImageTagsMapping[0] == "ParentBanner": - if "SeriesId" in Item: - EmbyArtworkId = Item["SeriesId"] + + if DynamicNode: + if ImageTagsMapping[0] == "ParentBanner": + if "SeriesId" in Item: + EmbyArtworkId = Item["SeriesId"] + EmbyArtworkTag = "" + elif ImageTagsMapping[0] == "AlbumArtists" and "AlbumArtists" in Item and Item["AlbumArtists"] and Item["AlbumArtists"] != "None": + EmbyArtworkId = Item["AlbumArtists"][0]['Id'] + EmbyArtworkTag = "" + elif ImageTagsMapping[0] == "ArtistItems" and "ArtistItems" in Item and Item["ArtistItems"] and Item["ArtistItems"] != "None": + EmbyArtworkId = Item["ArtistItems"][0]['Id'] EmbyArtworkTag = "" - elif ImageTagsMapping[0] == "AlbumArtists" and "AlbumArtists" in Item and Item["AlbumArtists"] and Item["AlbumArtists"] != "None": - EmbyArtworkId = Item["AlbumArtists"][0]['Id'] - EmbyArtworkTag = "" - elif ImageTagsMapping[0] == "ArtistItems" and "ArtistItems" in Item and Item["ArtistItems"] and Item["ArtistItems"] != "None": - EmbyArtworkId = Item["ArtistItems"][0]['Id'] - EmbyArtworkTag = "" - elif f"{ImageTagsMapping[0]}ImageTags" in Item: + + if f"{ImageTagsMapping[0]}ImageTags" in Item: BackDropsKey = f"{ImageTagsMapping[0]}ImageTags" if BackDropsKey == "ParentBackdropImageTags": @@ -738,11 +748,11 @@ def set_KodiArtwork(Item, ServerId, DynamicNode): if EmbyBackDropsId: if Item[BackDropsKey] and Item[BackDropsKey] != "None": if ImageTagsMapping[1] == "fanart": - if not "fanart" in Item['KodiArtwork']["fanart"]: + if "fanart" not in Item['KodiArtwork']["fanart"]: Item['KodiArtwork']["fanart"]["fanart"] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{EmbyBackDropsId}-0-B-{Item[BackDropsKey][0]}|redirect-limit=1000" for index, EmbyArtworkTag in enumerate(Item[BackDropsKey][1:], 1): - if not f"fanart{index}" in Item['KodiArtwork']["fanart"]: + if f"fanart{index}" not in Item['KodiArtwork']["fanart"]: Item['KodiArtwork']["fanart"][f"fanart{index}"] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{EmbyBackDropsId}-{index}-B-{EmbyArtworkTag}|redirect-limit=1000" else: if not Item['KodiArtwork'][ImageTagsMapping[1]]: @@ -750,7 +760,7 @@ def set_KodiArtwork(Item, ServerId, DynamicNode): if EmbyArtworkId: if ImageTagsMapping[1] == "fanart": - if not "fanart" in Item['KodiArtwork']["fanart"]: + if "fanart" not in Item['KodiArtwork']["fanart"]: Item['KodiArtwork']["fanart"]["fanart"] = f"http://127.0.0.1:57342/picture/{ServerId}/p-{EmbyArtworkId}-0-{EmbyArtworkIdShort[ImageTagsMapping[0]]}-{EmbyArtworkTag}|redirect-limit=1000" else: if not Item['KodiArtwork'][ImageTagsMapping[1]]: @@ -1169,7 +1179,7 @@ def update_multiversion(EmbyDB, Item, EmbyType): def update_boxsets(StartSync, ParentId, LibraryId, SQLs, EmbyServer): if not StartSync: - for BoxSet in EmbyServer.API.get_Items(ParentId, ["BoxSet"], True, True, {'GroupItemsIntoCollections': True}): + for BoxSet in EmbyServer.API.get_Items(ParentId, ["BoxSet"], True, True, {'GroupItemsIntoCollections': True}, "", False): SQLs["emby"].add_UpdateItem(BoxSet['Id'], "BoxSet", LibraryId) def set_Favorites_Artwork(Item, ServerId): diff --git a/core/musicartist.py b/core/musicartist.py index 56fbc2fb3..749ef6c7f 100644 --- a/core/musicartist.py +++ b/core/musicartist.py @@ -17,7 +17,6 @@ def change(self, Item): xbmc.log(f"EMBY.core.musicartist: Process item: {Item['Name']}", 0) # DEBUG common.set_MetaItems(Item, self.SQLs, self.MusicGenreObject, self.EmbyServer, "MusicGenre", 'GenreItems') common.set_common(Item, self.EmbyServer.ServerData['ServerId'], False) - common.set_KodiArtwork(Item, self.EmbyServer.ServerData['ServerId'], False) isFavorite = common.set_Favorite(Item) _, KodiDB = self.EmbyServer.library.WhitelistUnique[str(Item['LibraryId'])] NewItem = False diff --git a/core/playlist.py b/core/playlist.py index e41d972ae..b3819763a 100644 --- a/core/playlist.py +++ b/core/playlist.py @@ -11,7 +11,7 @@ def change(self, Item): common.load_ExistingItem(Item, self.EmbyServer, self.SQLs["emby"], "Playlist") IsFavorite = common.set_Favorite(Item) xbmc.log(f"EMBY.core.playlist: Process item: {Item['Name']}", 0) # DEBUG - PlaylistItems = self.EmbyServer.API.get_Items(Item['Id'], ["Episode", "Movie", "Trailer", "MusicVideo", "Audio", "Video", "Photo"], True, True, {},"") + PlaylistItems = self.EmbyServer.API.get_Items(Item['Id'], ["Episode", "Movie", "Trailer", "MusicVideo", "Audio", "Video", "Photo"], True, True, {}, "", True) KodiItemId = utils.valid_Filename(Item['Name']) PlaylistFilename = f"{utils.PlaylistPath}{KodiItemId}.m3u" isFavorite = common.set_Favorite(Item) diff --git a/database/emby_db.py b/database/emby_db.py index c08c60885..f15e7bb65 100644 --- a/database/emby_db.py +++ b/database/emby_db.py @@ -1046,7 +1046,7 @@ def add_multiversion(self, item, EmbyType, API, SQLs): SQLs['video'].delete_musicvideos(ItemReferenced['KodiItemId'], ItemReferenced['KodiFileId']) # Add references - if not "ParentId" in item: + if "ParentId" not in item: item['ParentId'] = None if EmbyType == "Episode": diff --git a/database/library.py b/database/library.py index edf02b167..78d3a4116 100644 --- a/database/library.py +++ b/database/library.py @@ -205,6 +205,13 @@ def KodiStartSync(self, Firstrun): # Threaded by caller -> emby.py ProgressBarIndex = 0 for LibraryIdWhitelist, LibraryNameWhitelist, EmbyTypeWhitelist, _, _ in self.Whitelist: + if utils.SystemShutdown: + xbmc.log("EMBY.database.library: THREAD: ---<[ retrieve changes ] shutdown 2", 0) # LOGDEBUG + ProgressBar.close() + del ProgressBar + self.KodiStartSyncRunning = False + return + xbmc.log(f"EMBY.database.library: [ retrieve changes ] {LibraryNameWhitelist} / {EmbyTypeWhitelist}", 1) # LOGINFO LibraryName = "" ProgressBarIndex += 1 @@ -220,12 +227,12 @@ def KodiStartSync(self, Firstrun): # Threaded by caller -> emby.py ItemIndex = 0 UpdateDataTemp = 10000 * [()] # pre allocate memory - for Item in self.EmbyServer.API.get_Items(LibraryIdWhitelist, [EmbyTypeWhitelist], True, True, {'MinDateLastSavedForUser': self.LastSyncTime}): + for Item in self.EmbyServer.API.get_Items(LibraryIdWhitelist, [EmbyTypeWhitelist], True, True, {'MinDateLastSavedForUser': self.LastSyncTime}, "", True): if utils.SystemShutdown: ProgressBar.close() del ProgressBar self.KodiStartSyncRunning = False - xbmc.log("EMBY.database.library: THREAD: ---<[ retrieve changes ] shutdown 2", 0) # LOGDEBUG + xbmc.log("EMBY.database.library: THREAD: ---<[ retrieve changes ] shutdown 3", 0) # LOGDEBUG return if ItemIndex >= 10000: @@ -242,6 +249,11 @@ def KodiStartSync(self, Firstrun): # Threaded by caller -> emby.py ProgressBar.close() del ProgressBar + if utils.SystemShutdown: + xbmc.log("EMBY.database.library: THREAD: ---<[ retrieve changes ] shutdown 4", 0) # LOGDEBUG + self.KodiStartSyncRunning = False + return + # Run jobs if UpdateData: UpdateData = list(dict.fromkeys(UpdateData)) # filter doubles @@ -421,7 +433,7 @@ def worker_update_generator(self, _SQLs, UpdateItems, _RecordsPercent, _): UpdateItemsIdsTemp = UpdateItemsIds.copy() self.EmbyServer.API.ProcessProgress["worker_update"] = 0 - for ItemIndex, Item in enumerate(self.EmbyServer.API.get_Items_Ids(UpdateItemsIds, ContentType, False, False, "worker_update", LibraryId), 1): + for ItemIndex, Item in enumerate(self.EmbyServer.API.get_Items_Ids(UpdateItemsIds, ContentType, False, False, "worker_update", LibraryId, {}), 1): self.EmbyServer.API.ProcessProgress["worker_update"] = ItemIndex if Item['Id'] in UpdateItemsIds: @@ -590,7 +602,7 @@ def worker_library(self): del TagObject # Sync Content - for ItemIndex, Item in enumerate(self.EmbyServer.API.get_Items(LibraryId, [EmbyType], False, True, {}, "worker_library"), 1): + for ItemIndex, Item in enumerate(self.EmbyServer.API.get_Items(LibraryId, [EmbyType], False, True, {}, "worker_library", True), 1): Item["LibraryId"] = LibraryId Continue, SQLs = self.ItemOps(SyncItemProgress, ItemIndex, Item, SQLs, WorkerName, KodiDBs, ProgressBar, True) self.EmbyServer.API.ProcessProgress["worker_library"] = ItemIndex @@ -615,7 +627,7 @@ def worker_library(self): pluginmenu.reset_querycache(None) self.close_Worker(WorkerName, True, True, ProgressBar) # utils.sleep(2) # give Kodi time to catch up (otherwise could cause crashes) -# xbmc.executebuiltin('ReloadSkin()') +# xbmc.executebuiltin('ReloadSkin()') # Skin reload broken in Kodi 21 xbmc.log("EMBY.database.library: --<[ worker library completed ]", 1) # LOGINFO self.RunJobs() @@ -685,6 +697,7 @@ def ItemOps(self, ProgressValue, ItemIndex, Item, SQLs, WorkerName, KodiDBs, Pro xbmc.log("EMBY.database.library: [ worker exit (shutdown 2) ]", 1) # LOGINFO return False, {} + del Item return True, SQLs def load_libraryObject(self, MediaType, SQLs): @@ -1150,14 +1163,12 @@ def SyncThemes(self): for ViewId in views: if UseVideoThemes: - for item in self.EmbyServer.API.get_Items(ViewId, ["Movie", "Series"], True, True, {'HasThemeVideo': "True"}): - query = normalize_string(item['Name']) - items[item['Id']] = query + for item in self.EmbyServer.API.get_Items(ViewId, ["Movie", "Series"], True, True, {'HasThemeVideo': "True"}, "", False): + items[item['Id']] = normalize_string(item['Name']) if UseAudioThemes: - for item in self.EmbyServer.API.get_Items(ViewId, ["Movie", "Series"], True, True, {'HasThemeSong': "True"}): - query = normalize_string(item['Name']) - items[item['Id']] = query + for item in self.EmbyServer.API.get_Items(ViewId, ["Movie", "Series"], True, True, {'HasThemeSong': "True"}, "", False): + items[item['Id']] = normalize_string(item['Name']) Index = 1 TotalItems = len(items) / 100 @@ -1212,7 +1223,7 @@ def SyncThemes(self): utils.translatePath(FilePath).decode('utf-8') if not utils.checkFileExists(FilePath): - self.EmbyServer.API.download_file({"Id": ThemeItem['Id'], "FileSize": ThemeItem['Size'], "Name": Name, "FilePath": FilePath, "Path": NfoPath}) + utils.EmbyServer.API.download_file(ThemeItem['Id'], "", NfoPath, FilePath, ThemeItem['Size'], Name, "", "", "", "") XMLData += f" {utils.encode_XML(FilePath)}\n".encode("utf-8") @@ -1300,7 +1311,7 @@ def SyncLiveTV(self): utils.writeFileString(PlaylistFile, PlaylistM3U) self.SyncLiveTVEPG(False) - SimpleIptvSettings = utils.readFileString("special://home/addons/plugin.video.emby-next-gen/resources/iptvsimple.xml") + SimpleIptvSettings = utils.readFileString("special://home/addons/plugin.service.emby-next-gen/resources/iptvsimple.xml") SimpleIptvSettings = SimpleIptvSettings.replace("SERVERID", self.EmbyServer.ServerData['ServerId']) utils.SendJson('{"jsonrpc":"2.0","id":1,"method":"Addons.SetAddonEnabled","params":{"addonid":"pvr.iptvsimple","enabled":false}}') utils.writeFileBinary(f"special://profile/addon_data/pvr.iptvsimple/instance-settings-{str(int(self.EmbyServer.ServerData['ServerId'], 16))[:4]}.xml", SimpleIptvSettings.encode("utf-8")) @@ -1468,7 +1479,7 @@ def refresh_dynamic_nodes(): pluginmenu.reset_querycache(None) MenuPath = xbmc.getInfoLabel('Container.FolderPath') - if MenuPath.startswith("plugin://plugin.video.emby-next-gen/") and "mode=browse" in MenuPath.lower(): + if MenuPath.startswith("plugin://plugin.service.emby-next-gen/") and "mode=browse" in MenuPath.lower(): xbmc.log("Emby.hooks.websocket: [ UserDataChanged refresh dynamic nodes ]", 1) # LOGINFO xbmc.executebuiltin('Container.Refresh') else: diff --git a/database/music_db.py b/database/music_db.py index 76a455eae..3096f3fc1 100644 --- a/database/music_db.py +++ b/database/music_db.py @@ -377,7 +377,7 @@ def get_artwork(self, KodiId, ContentType): # Favorite for content def get_favoriteData(self, KodiId, ContentType): - self.cursor.execute("SELECT idPath, strTitle, strFilename FROM song WHERE idSong = ?", (KodiId,)) + self.cursor.execute("SELECT idPath, strTitle, strFilename, idAlbum FROM song WHERE idSong = ?", (KodiId,)) ItemData = self.cursor.fetchone() Thumbnail = "" @@ -386,7 +386,7 @@ def get_favoriteData(self, KodiId, ContentType): DataPath = self.cursor.fetchone() if DataPath: - self.cursor.execute("SELECT url FROM art WHERE media_id = ? AND media_type = ? AND type = ?", (KodiId, ContentType, "thumb")) + self.cursor.execute("SELECT url FROM art WHERE media_id = ? AND media_type = ? AND type = ?", (ItemData[3], "album", "thumb")) ArtworkData = self.cursor.fetchone() if ArtworkData: diff --git a/database/video_db.py b/database/video_db.py index 6b20a37ba..1abde67d3 100644 --- a/database/video_db.py +++ b/database/video_db.py @@ -1177,7 +1177,7 @@ def add_countries_and_links(self, ProductionLocations, media_id, media_type): # artwork def get_artwork(self, KodiItemId, ContentType, PrefixKey): Artwork = {} - self.cursor.execute("SELECT * FROM art WHERE media_id = ? and media_type = ?", (KodiItemId, ContentType)) + self.cursor.execute("SELECT * FROM art WHERE media_id = ? AND media_type = ?", (KodiItemId, ContentType)) ArtworksData = self.cursor.fetchall() for ArtworkData in ArtworksData: diff --git a/dialogs/loginconnect.py b/dialogs/loginconnect.py index 0bcabefc3..ae5856d1f 100644 --- a/dialogs/loginconnect.py +++ b/dialogs/loginconnect.py @@ -21,7 +21,7 @@ def __init__(self, *args, **kwargs): self.remind_button = None self.error_toggle = None self.error_msg = None - self.EmbyServer = None + self.Login = "", "" xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def onInit(self): @@ -50,7 +50,8 @@ def onClick(self, controlId): # Display error self._error(ERROR['Empty'], utils.Translate(30608)) xbmc.log("EMBY.dialogs.loginconnect: Username or password cannot be null", 3) # LOGERROR - elif self._login(user, password): + else: + self.Login = user, password self.close() elif controlId == CANCEL: # Remind me later @@ -75,16 +76,6 @@ def _add_editcontrol(self, x, y, height, width, password): return control - def _login(self, username, password): - result = self.EmbyServer.login_to_connect(username, password) - - if not result: - self._error(ERROR['Invalid'], utils.Translate(33009)) - return False - - utils.Dialog.notification(heading=utils.addon_name, message=f"{utils.Translate(33000)} {result['User']['Name']}", icon=result['User'].get('ImageUrl') or utils.icon, time=utils.displayMessage, sound=False) - return True - def _error(self, state, message): self.error = state self.error_msg.setLabel(message) diff --git a/dialogs/loginmanual.py b/dialogs/loginmanual.py index e0bae892e..ea75c9704 100644 --- a/dialogs/loginmanual.py +++ b/dialogs/loginmanual.py @@ -14,7 +14,7 @@ class LoginManual(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): - self.SelectedUser = {} + self.SelectedUser = "", "" self.error = None self.username = None self.user_field = None @@ -23,7 +23,6 @@ def __init__(self, *args, **kwargs): self.error_toggle = None self.error_msg = None self.cancel_button = None - self.EmbyServer = None xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def onInit(self): @@ -58,7 +57,8 @@ def onClick(self, controlId): # Display error self._error(ERROR['Empty'], utils.Translate(30613)) xbmc.log("EMBY.dialogs.loginmanual: Username cannot be null", 3) # LOGERROR - elif self._login(user, password): + else: + self.SelectedUser = user, password self.close() elif controlId == CANCEL: # Remind me later @@ -83,17 +83,6 @@ def _add_editcontrol(self, x, y, height, width, password): return control - def _login(self, username, password): - server = self.EmbyServer.ServerData[self.EmbyServer.ServerData['LastConnectionMode']] - result = self.EmbyServer.ServerLogin(server, username, password) - - if not result: - self._error(ERROR['Invalid'], utils.Translate(33009)) - return False - - self.SelectedUser = result - return True - def _error(self, state, message): self.error = state self.error_msg.setLabel(message) diff --git a/dialogs/serverconnect.py b/dialogs/serverconnect.py index 74f0b8414..35ac265e0 100644 --- a/dialogs/serverconnect.py +++ b/dialogs/serverconnect.py @@ -26,7 +26,9 @@ def __init__(self, *args, **kwargs): self.message_box = None self.busy = None self.list_ = None - self.EmbyServer = None + self.ServerSelected = {} + self.Servers = [] + self.ConnectionMode = "" self.emby_connect = None xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) @@ -36,7 +38,7 @@ def onInit(self): self.busy = self.getControl(BUSY) self.list_ = self.getControl(LIST) - for server in self.EmbyServer.Found_Servers: + for server in self.Servers: if 'Name' not in server: continue @@ -53,7 +55,7 @@ def onInit(self): if not self.emby_connect: # Change connect user self.getControl(EMBY_CONNECT).setLabel(f"[B]{utils.Translate(30618)}[/B]") - if self.EmbyServer.Found_Servers: + if self.Servers: self.setFocus(self.list_) def onAction(self, action): @@ -67,36 +69,24 @@ def onAction(self, action): Server_Selected_Id = server.getProperty('id') Server_Selected_Name = server.getProperty('Name') - for server in self.EmbyServer.Found_Servers: + for server in self.Servers: if server['Id'] == Server_Selected_Id and server['Name'] == Server_Selected_Name: - self.EmbyServer.ServerData.update({'ServerId': server['Id'], 'ServerName': server['Name']}) - - # EmbyConnect - if server.get('ExchangeToken', ""): - self.EmbyServer.ServerData.update({'EmbyConnectLocalAddress': server.get('LocalAddress', ""), 'EmbyConnectRemoteAddress': server.get('RemoteAddress', ""), 'EmbyConnectExchangeToken': server.get('ExchangeToken', ""), 'LocalAddress': "", 'RemoteAddress': "", 'ManualAddress': ""}) - else: #regular - self.EmbyServer.ServerData.update({'LocalAddress': server.get('LocalAddress', ""), 'RemoteAddress': server.get('RemoteAddress', ""), 'ManualAddress': server.get('ManualAddress', ""), 'EmbyConnectLocalAddress': "", 'EmbyConnectRemoteAddress': "", 'EmbyConnectExchangeToken': ""}) - + self.ServerSelected = {'ServerId': server['Id'], 'ServerName': server['Name']} + self.ServerSelected.update({'LocalAddress': server.get('LocalAddress', ""), 'RemoteAddress': server.get('RemoteAddress', ""), 'EmbyConnectExchangeToken': server.get('ExchangeToken', "")}) self.message.setLabel(f"{utils.Translate(30610)} {server['Name']}...") self.message_box.setVisibleCondition('true') self.busy.setVisibleCondition('true') - result = self.EmbyServer.connect_to_server() - - if not result: # Unavailable - self.busy.setVisibleCondition('false') - self.message.setLabel(utils.Translate(30609)) - break - self.message_box.setVisibleCondition('false') + self.ConnectionMode = "ListSelection" self.close() break def onClick(self, controlId): if controlId == EMBY_CONNECT: - self.EmbyServer.ServerData['LastConnectionMode'] = "EmbyConnect" + self.ConnectionMode = "EmbyConnect" elif controlId == MANUAL_SERVER: - self.EmbyServer.ServerData['LastConnectionMode'] = "ManualAddress" + self.ConnectionMode = "ManualAddress" elif controlId == CANCEL: - self.EmbyServer.ServerData['LastConnectionMode'] = "" + self.ConnectionMode = "" self.close() diff --git a/dialogs/servermanual.py b/dialogs/servermanual.py index 704423780..298bfba40 100644 --- a/dialogs/servermanual.py +++ b/dialogs/servermanual.py @@ -21,7 +21,7 @@ def __init__(self, *args): self.error_msg = None self.host_field = None self.port_field = None - self.connect_to_address = None + self.ManualAddress = "" xbmcgui.WindowXMLDialog.__init__(self, *args) def onInit(self): @@ -51,7 +51,8 @@ def onClick(self, controlId): # Display error self._error(ERROR['Empty'], utils.Translate(30617)) xbmc.log("EMBY.dialogs.servermanual: Server cannot be null", 3) # LOGERROR - elif self._connect_to_server(server, port): + else: + self.ManualAddress = f"{server}:{port}" self.close() # Remind me later elif controlId == CANCEL: @@ -72,16 +73,6 @@ def _add_editcontrol(self, x, y, height, width): self.addControl(control) return control - def _connect_to_server(self, server, port): - server_address = f"{server}:{port}" - self._message(f"{utils.Translate(30610)} {server_address}...") - - if not self.connect_to_address(server_address): # Unavailable - self._message(utils.Translate(30609)) - return False - - return True - def _message(self, message): self.error_msg.setLabel(message) self.error_toggle.setVisibleCondition('true') diff --git a/dialogs/usersconnect.py b/dialogs/usersconnect.py index 5536a9bef..1d48913ac 100644 --- a/dialogs/usersconnect.py +++ b/dialogs/usersconnect.py @@ -1,6 +1,5 @@ import xbmc import xbmcgui -from helper import utils ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -15,10 +14,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): self.SelectedUser = {} - self.ManualLogin = False self.list_ = None - self.ServerData = {} - self.API = None self.users = [] xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) @@ -26,18 +22,6 @@ def onInit(self): self.list_ = self.getControl(LIST) for user in self.users: - user['UserImageUrl'] = utils.icon - - # Download user picture - BinaryData, _, FileExtension = self.API.get_Image_Binary(user['Id'], "Primary", 0, 0, True) - - if BinaryData: - Filename = utils.valid_Filename(f"{self.ServerData['ServerName']}_{user['Name']}_{user['Id']}.{FileExtension}") - iconpath = f"{utils.FolderEmbyTemp}{Filename}" - utils.delFile(iconpath) - utils.writeFileBinary(iconpath, BinaryData) - user['UserImageUrl'] = iconpath - item = xbmcgui.ListItem(user['Name']) item.setProperty('id', user['Id']) item.setArt({'Icon': user['UserImageUrl']}) @@ -58,15 +42,13 @@ def onAction(self, action): for user in self.users: if user['Id'] == selected_id: self.SelectedUser = user - self.ServerData['UserImageUrl'] = user['UserImageUrl'] - self.ServerData['UserName'] = user['Name'] break self.close() def onClick(self, controlId): if controlId == MANUAL: - self.ManualLogin = True + self.SelectedUser = "MANUAL" self.close() elif controlId == CANCEL: self.close() diff --git a/emby/api.py b/emby/api.py index 70ea34336..d0cc79b53 100644 --- a/emby/api.py +++ b/emby/api.py @@ -64,18 +64,18 @@ def update_settings(self): self.DynamicListsRemoveFields += ("People",) def open_livestream(self, Id): - PlaybackInfoData = self.EmbyServer.http.request({'params': {'UserId': self.EmbyServer.ServerData['UserId'], "IsPlayback": "true", "AutoOpenLiveStream": "true"}, 'type': "POST", 'handler': f"Items/{Id}/PlaybackInfo"}, True, False) + _, _, Payload = self.EmbyServer.http.request("POST", f"Items/{Id}/PlaybackInfo", {'UserId': self.EmbyServer.ServerData['UserId'], "IsPlayback": "true", "AutoOpenLiveStream": "true"}, {}, False, "", False) - if 'MediaSources' in PlaybackInfoData and PlaybackInfoData['MediaSources']: - MediaSourceId = PlaybackInfoData['MediaSources'][0]['Id'] - LiveStreamId = PlaybackInfoData['MediaSources'][0].get('LiveStreamId', None) - Container = PlaybackInfoData['MediaSources'][0].get('Container', "") + if 'MediaSources' in Payload and Payload['MediaSources']: + MediaSourceId = Payload['MediaSources'][0]['Id'] + LiveStreamId = Payload['MediaSources'][0].get('LiveStreamId', None) + Container = Payload['MediaSources'][0].get('Container', "") else: MediaSourceId = None LiveStreamId = None Container = None - return MediaSourceId, LiveStreamId, PlaybackInfoData['PlaySessionId'], Container + return MediaSourceId, LiveStreamId, Payload['PlaySessionId'], Container def get_Items_dynamic(self, ParentId, MediaTypes, Recursive, Extra, Resume, LibraryId): CustomLimit = False @@ -83,8 +83,9 @@ def get_Items_dynamic(self, ParentId, MediaTypes, Recursive, Extra, Resume, Libr if Resume: Request = f"Users/{self.EmbyServer.ServerData['UserId']}/Items/Resume" else: - Request = f"Users/{self.EmbyServer.ServerData['UserId']}/Items" + Request = f"Users/{self.EmbyServer.ServerData['UserId']}/Items" # Userdata must be always queried, otherwise ParentId parameter is not respected by Emby server + ItemsQueue = queue.Queue() ItemsFullQuery = 10000 * [()] # pre allocate memory ItemIndex = 0 @@ -105,8 +106,12 @@ def get_Items_dynamic(self, ParentId, MediaTypes, Recursive, Extra, Resume, Libr embydb = None videodb = None musicdb = None + start_new_thread(self.async_get_Items, (Request, ItemsQueue, Params, "", CustomLimit)) + + for BasicItem in ItemsQueue.getall(): + if BasicItem == "QUIT": + break - for BasicItem in self.get_Items_Custom(Request, Params, CustomLimit): KodiItem = ({}, "") KodiDB = "" @@ -177,15 +182,20 @@ def get_Items_dynamic(self, ParentId, MediaTypes, Recursive, Extra, Resume, Libr SortItems[ItemFullQuery[0]] += ((ItemFullQuery[1], ItemFullQuery[2]),) + # request extended item data for Type, ItemData in list(SortItems.items()): if ItemData: - if EmbyFields[Type.lower()]: - yield from self.get_Items_Ids(list(dict(ItemData).keys()), [Type], True, False, False, "") - else: + Fields = EmbyFields[Type.lower()] + + if Fields and Fields != ("Path",): + yield from self.get_Items_Ids(list(dict(ItemData).keys()), [Type], True, False, False, "", Extra) + else: # no extended information required for Item in ItemData: yield Item[1] - def get_Items_Ids(self, Ids, MediaTypes, Dynamic, Basic, ProcessProgressId, LibraryId, UserData=True): + del ItemData + + def get_Items_Ids(self, Ids, MediaTypes, Dynamic, Basic, ProcessProgressId, LibraryId, Extra): ItemsQueue = queue.Queue() Fields = [] @@ -207,20 +217,27 @@ def get_Items_Ids(self, Ids, MediaTypes, Dynamic, Basic, ProcessProgressId, Libr Fields = None if len(MediaTypes) == 1: - Params = {'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'IncludeItemTypes': MediaTypes[0], 'SortBy': "None"} + Params = {'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'IncludeItemTypes': MediaTypes[0]} else: - Params = {'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'SortBy': "None"} + Params = {'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline"} - if UserData: - start_new_thread(self.async_get_Items_Ids, (f"Users/{self.EmbyServer.ServerData['UserId']}/Items", ItemsQueue, Params, Ids, Dynamic, ProcessProgressId, LibraryId)) - else: - start_new_thread(self.async_get_Items_Ids, ("Items", ItemsQueue, Params, Ids, Dynamic, ProcessProgressId, LibraryId)) + if Extra: + Params.update(Extra) + + if 'SortBy' not in Params: + Params['SortBy'] = "None" + + start_new_thread(self.async_get_Items_Ids, (f"Users/{self.EmbyServer.ServerData['UserId']}/Items", ItemsQueue, Params, Ids, Dynamic, ProcessProgressId, LibraryId)) while True: Items = ItemsQueue.getall() + if not Items: + break + if Items[-1] == "QUIT": yield from Items[:-1] + del Items return yield from Items @@ -229,7 +246,8 @@ def get_Items_Ids(self, Ids, MediaTypes, Dynamic, Basic, ProcessProgressId, Libr def async_get_Items_Ids(self, Request, ItemsQueue, Params, Ids, Dynamic, ProcessProgressId, LibraryId): xbmc.log("EMBY.emby.api: THREAD: --->[ load Item by Ids ]", 0) # LOGDEBUG Index = 0 - IncomingData = () + Payload = () + IdsTotal = len(Ids) while Ids: # Uri length limitation @@ -252,10 +270,10 @@ def async_get_Items_Ids(self, Request, ItemsQueue, Params, Ids, Dynamic, Process else: Params.update({'Recursive': True, 'ParentId': LibraryId}) - IncomingData = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': Request}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", Request, Params, {}, False, "", False) - if 'Items' in IncomingData: - for Item in IncomingData['Items']: + if 'Items' in Payload: + for Item in Payload['Items']: Found = True Item['LibraryId'] = LibraryId ItemsQueue.put(Item) @@ -263,83 +281,74 @@ def async_get_Items_Ids(self, Request, ItemsQueue, Params, Ids, Dynamic, Process if not Found or utils.SystemShutdown: ItemsQueue.put("QUIT") - del IncomingData # release memory + del Payload # release memory xbmc.log("EMBY.emby.api: THREAD: ---<[ load Item by Ids ] no items found or shutdown (regular query)", 0) # LOGDEBUG return elif not Dynamic: # realtime updates via websocket for WhitelistLibraryId in self.EmbyServer.library.WhitelistUnique: Params.update({'Recursive': True, 'ParentId': WhitelistLibraryId}) - IncomingData = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': Request}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", Request, Params, {}, False, "", False) - if 'Items' in IncomingData: - for Item in IncomingData['Items']: + if 'Items' in Payload: + for Item in Payload['Items']: Item['LibraryId'] = WhitelistLibraryId ItemsQueue.put(Item) Index += 1 - if len(IncomingData['Items']) == len(Params['Ids'].split(",")): # All data received, no need to check additional libraries + if len(Payload['Items']) == len(Params['Ids'].split(",")): # All data received, no need to check additional libraries break if utils.SystemShutdown: ItemsQueue.put("QUIT") - del IncomingData # release memory + del Payload # release memory xbmc.log("EMBY.emby.api: THREAD: ---<[ load Item by Ids ] shutdown (websocket query)", 0) # LOGDEBUG return else: # dynamic node query - IncomingData = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': Request}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", Request, Params, {}, False, "", False) if utils.SystemShutdown: ItemsQueue.put("QUIT") - del IncomingData # release memory + del Payload # release memory xbmc.log("EMBY.emby.api: THREAD: ---<[ load Item by Ids ] shutdown (dynamic)", 0) # LOGDEBUG return - if 'Items' in IncomingData and IncomingData['Items']: - ItemsQueue.put(IncomingData['Items']) - Index += len(IncomingData['Items']) + if 'Items' in Payload and Payload['Items']: + ItemsQueue.put(Payload['Items']) + Index += len(Payload['Items']) - if not self.async_throttle_queries(Index, 10000, ProcessProgressId): + if IdsTotal == Index: # all requested items received break - del IncomingData # release memory + if not self.async_throttle_queries(Index, ProcessProgressId): + break + + del Payload # release memory ItemsQueue.put("QUIT") xbmc.log("EMBY.emby.api: THREAD: ---<[ load Item by Ids ]", 0) # LOGDEBUG - def async_throttle_queries(self, Index, Limit, ProcessProgressId): + def async_throttle_queries(self, Index, ProcessProgressId): # Throttle queries -> give Kodi time to catch up if ProcessProgressId and ProcessProgressId in self.ProcessProgress: - ProcessLimit = Index - 2 * Limit + while Index > self.ProcessProgress[ProcessProgressId]: + xbmc.log(f"EMBY.emby.api: Throttle queries {Index} / {ProcessProgressId} / {self.ProcessProgress[ProcessProgressId]}", 1) # LOGINFO - while ProcessLimit > self.ProcessProgress[ProcessProgressId]: if utils.sleep(2) or self.ProcessProgress[ProcessProgressId] == -1: # Cancel return False - xbmc.log(f"EMBY.emby.api: Throttle queries {ProcessLimit} / {ProcessProgressId} / {self.ProcessProgress[ProcessProgressId]}", 1) # LOGINFO - return True - def get_Items_Custom(self, Request, Params, CustomLimit): - ItemsQueue = queue.Queue() - start_new_thread(self.async_get_Items, (Request, ItemsQueue, CustomLimit, Params)) - - while True: - Items = ItemsQueue.getall() - - if Items[-1] == "QUIT": - yield from Items[:-1] - return - - yield from Items - del Items - - def get_Items(self, ParentId, MediaTypes, Basic, Recursive, Extra, ProcessProgressId="", UserData=True): + def get_Items(self, ParentId, MediaTypes, Basic, Recursive, Extra, ProcessProgressId, UserData): CustomLimit = False ItemsQueue = queue.Queue() for MediaType in MediaTypes: Limit = get_Limit(MediaType) Fields = self.get_Fields(MediaType, Basic, False) - Params = {'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'Recursive': Recursive, 'Limit': Limit, 'Fields': Fields} + + if Fields: + Params = {'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'Recursive': Recursive, 'Limit': Limit, 'Fields': Fields} + else: + Params = {'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'Recursive': Recursive, 'Limit': Limit} if MediaType != "All": Params['IncludeItemTypes'] = MediaType @@ -354,10 +363,16 @@ def get_Items(self, ParentId, MediaTypes, Basic, Recursive, Extra, ProcessProgre if 'SortBy' not in Params: Params['SortBy'] = "None" - if UserData: - start_new_thread(self.async_get_Items, (f"Users/{self.EmbyServer.ServerData['UserId']}/Items", ItemsQueue, CustomLimit, Params, ProcessProgressId)) - else: - start_new_thread(self.async_get_Items, ("Items", ItemsQueue, CustomLimit, Params, ProcessProgressId)) + start_new_thread(self.async_get_Items, (f"Users/{self.EmbyServer.ServerData['UserId']}/Items", ItemsQueue, Params, ProcessProgressId, CustomLimit)) # Userdata must always queried, otherwise ParentId parameter is not respected by Emby server. -> Server issue + +# if UserData and MediaType != "Folder": +# start_new_thread(self.async_get_Items, (f"Users/{self.EmbyServer.ServerData['UserId']}/Items", ItemsQueue, Params, ProcessProgressId, CustomLimit)) +# else: # Skip userdata query +# if "MinDateLastSavedForUser" in Params: +# Params["MinDateLastSaved"] = Params["MinDateLastSavedForUser"] +# del Params["MinDateLastSavedForUser"] + +# start_new_thread(self.async_get_Items, ("Items", ItemsQueue, Params, ProcessProgressId, CustomLimit)) while True: Items = ItemsQueue.getall() @@ -365,8 +380,12 @@ def get_Items(self, ParentId, MediaTypes, Basic, Recursive, Extra, ProcessProgre if utils.SystemShutdown: return + if not Items: + break + if Items[-1] == "QUIT": yield from Items[:-1] + del Items # release memory break yield from Items @@ -376,11 +395,14 @@ def get_channelprogram(self): Limit = get_Limit("livetv") Params = {'UserId': self.EmbyServer.ServerData['UserId'], 'Fields': "Overview", 'EnableTotalRecordCount': False, 'Limit': Limit} ItemsQueue = queue.Queue() - start_new_thread(self.async_get_Items, ("LiveTv/Programs", ItemsQueue, False, Params)) + start_new_thread(self.async_get_Items, ("LiveTv/Programs", ItemsQueue, Params, "", False)) while True: Items = ItemsQueue.getall() + if not Items: + break + if Items[-1] == "QUIT": yield from Items[:-1] del Items @@ -392,16 +414,17 @@ def get_channelprogram(self): def get_recommendations(self, ParentId): Fields = self.get_Fields("movie", False, True) Params = {'ParentId': ParentId, 'UserId': self.EmbyServer.ServerData['UserId'], 'Fields': Fields, 'EnableTotalRecordCount': False, 'Recursive': True} - IncomingData = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': "Movies/Recommendations"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Movies/Recommendations", Params, {}, False, "", False) + Items = [] - for Data in IncomingData: + for Data in Payload: if 'Items' in Data: Items += Data['Items'] return Items - def async_get_Items(self, Request, ItemsQueue, CustomLimit, Params, ProcessProgressId=""): + def async_get_Items(self, Request, ItemsQueue, Params, ProcessProgressId, CustomLimit): xbmc.log("EMBY.emby.api: THREAD: --->[ load Items ]", 0) # LOGDEBUG Index = 0 ItemCounter = 0 @@ -409,46 +432,46 @@ def async_get_Items(self, Request, ItemsQueue, CustomLimit, Params, ProcessProgr while True: Params['StartIndex'] = Index - IncomingData = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': Request}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", Request, Params, {}, False, "", False) DirectItems = Request.lower().find("latest") != -1 if DirectItems: - if not IncomingData or utils.SystemShutdown: + if utils.SystemShutdown or not Payload: ItemsQueue.put("QUIT") - del IncomingData # release memory + del Payload # release memory xbmc.log("EMBY.emby.api: THREAD: ---<[ load Items ] (latest / no items found or shutdown)", 0) # LOGDEBUG return - ItemsQueue.put(IncomingData) - ReceivedItems = len(IncomingData) + ItemsQueue.put(Payload) + ReceivedItems = len(Payload) ItemCounter += ReceivedItems else: - if 'Items' not in IncomingData or not IncomingData['Items'] or utils.SystemShutdown: + if utils.SystemShutdown or 'Items' not in Payload or not Payload['Items']: ItemsQueue.put("QUIT") - del IncomingData # release memory + del Payload # release memory xbmc.log("EMBY.emby.api: THREAD: ---<[ load Items ] (no items found or shutdown)", 0) # LOGDEBUG return - ItemsQueue.put(IncomingData['Items']) - ReceivedItems = len(IncomingData['Items']) + ItemsQueue.put(Payload['Items']) + ReceivedItems = len(Payload['Items']) ItemCounter += ReceivedItems - del IncomingData # release memory + del Payload # release memory + + if ReceivedItems < Limit: + ItemsQueue.put("QUIT") + xbmc.log(f"EMBY.emby.api: THREAD: ---<[ load Items ] Limit: {Limit} / ReceivedItems: {ReceivedItems}", 0) # LOGDEBUG + return if CustomLimit: ItemsQueue.put("QUIT") xbmc.log("EMBY.emby.api: THREAD: ---<[ load Items ] (limit reached)", 0) # LOGDEBUG return - if not self.async_throttle_queries(Index, Limit, ProcessProgressId): + if not self.async_throttle_queries(Index, ProcessProgressId): ItemsQueue.put("QUIT") xbmc.log("EMBY.emby.api: THREAD: ---<[ load Items ] (throttle)", 0) # LOGDEBUG - break - - if ReceivedItems < Limit: - ItemsQueue.put("QUIT") - xbmc.log(f"EMBY.emby.api: THREAD: ---<[ load Items ] Limit: {Limit} / ReceivedItems: {ReceivedItems}", 0) # LOGDEBUG - break + return Index += Limit @@ -459,13 +482,13 @@ def get_Item(self, Ids, MediaTypes, Dynamic, Basic, Specials=False): Fields = self.get_Fields(MediaType, Basic, Dynamic) if Specials: # Bugfix workaround - Data = self.EmbyServer.http.request({'params': {'Ids': Ids, 'Fields': Fields, 'IncludeItemTypes': 'Workaround', 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'SortBy': "None"}, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items", {'Ids': Ids, 'Fields': Fields, 'IncludeItemTypes': 'Workaround', 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'SortBy': "None"}, {}, False, "", False) else: - Data = self.EmbyServer.http.request({'params': {'Ids': Ids, 'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'SortBy': "None"}, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items", {'Ids': Ids, 'Fields': Fields, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'SortBy': "None"}, {}, False, "", False) - if 'Items' in Data: - if Data['Items']: - return Data['Items'][0] + if 'Items' in Payload: + if Payload['Items']: + return Payload['Items'][0] return {} @@ -475,47 +498,55 @@ def get_TotalRecords(self, parent_id, item_type, Extra): if Extra: Params.update(Extra) - Data = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items", Params, {}, False, "", False) - if 'TotalRecordCount' in Data: - return int(Data['TotalRecordCount']) + if 'TotalRecordCount' in Payload: + return int(Payload['TotalRecordCount']) return 0 def get_timer(self, ProgramId): - Data = self.EmbyServer.http.request({'params': {'programId': ProgramId}, 'type': "GET", 'handler': "LiveTv/Timers"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "LiveTv/Timers", {'programId': ProgramId}, {}, False, "", False) - if 'Items' in Data: - return Data['Items'] + if 'Items' in Payload: + return Payload['Items'] return [] def set_timer(self, ProgramId): - return self.EmbyServer.http.request({'params': {'programId': ProgramId}, 'type': "POST", 'handler': "LiveTv/Timers"}, False, False) + _, _, Payload = self.EmbyServer.http.request("POST", "LiveTv/Timers", {'programId': ProgramId}, {}, False, "", False) + return Payload def delete_timer(self, TimerId): - return self.EmbyServer.http.request({'type': "POST", 'handler': f"LiveTv/Timers/{TimerId}/Delete"}, False, False) + _, _, Payload = self.EmbyServer.http.request("POST", f"LiveTv/Timers/{TimerId}/Delete", {}, {}, False, "", False) + return Payload def get_users(self, disabled, hidden): - return self.EmbyServer.http.request({'params': {'IsDisabled': disabled, 'IsHidden': hidden}, 'type': "GET", 'handler': "Users"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Users", {'IsDisabled': disabled, 'IsHidden': hidden}, {}, False, "", False) + return Payload def get_public_users(self): - return self.EmbyServer.http.request({'type': "GET", 'handler': "Users/Public"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Users/Public", {}, {}, False, "", False) + return Payload def get_user(self, user_id): if not user_id: - return self.EmbyServer.http.request({'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}", {}, {}, False, "", False) + return Payload - return self.EmbyServer.http.request({'type': "GET", 'handler': f"Users/{user_id}"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{user_id}", {}, {}, False, "", False) + return Payload def get_libraries(self): - return self.EmbyServer.http.request({'type': "GET", 'handler': "Library/VirtualFolders/Query"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Library/VirtualFolders/Query", {}, {}, False, "", False) + return Payload def get_views(self): - return self.EmbyServer.http.request({'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Views"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Views", {}, {}, False, "", False) + return Payload - def download_file(self, Download): - self.EmbyServer.http.request({'type': "GET", 'handler': f"Items/{Download['Id']}/Download"}, False, True, False, False, False, Download) + def download_file(self, EmbyId, ParentPath, Path, FilePath, FileSize, Name, KodiType, KodiPathIdBeforeDownload, KodiFileId, KodiId): + self.EmbyServer.http.Queues["DOWNLOAD"].put(((EmbyId, ParentPath, Path, FilePath, FileSize, Name, KodiType, KodiPathIdBeforeDownload, KodiFileId, KodiId),)) def get_Image_Binary(self, Id, ImageType, ImageIndex, ImageTag, UserImage=False): Params = {"EnableImageEnhancers": utils.enableCoverArt} @@ -557,15 +588,15 @@ def get_Image_Binary(self, Id, ImageType, ImageIndex, ImageTag, UserImage=False) if UserImage: Params["Format"] = "original" - BinaryData, Headers = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': f"Users/{Id}/Images/{ImageType}"}, False, True, True) + _, Header, Payload = self.EmbyServer.http.request("GET", f"Users/{Id}/Images/{ImageType}", Params, {}, True, "", True) else: if ImageTag: Params["tag"] = ImageTag - BinaryData, Headers = self.EmbyServer.http.request({'params': Params, 'type': "GET", 'handler': f"Items/{Id}/Images/{ImageType}/{ImageIndex}"}, False, True, True) + _, Header, Payload = self.EmbyServer.http.request("GET", f"Items/{Id}/Images/{ImageType}/{ImageIndex}", Params, {}, True, "", True) - if 'Content-Type' in Headers: - ContentType = Headers['Content-Type'] + if 'Content-Type' in Header: + ContentType = Header['content-type'] if ContentType == "image/jpeg": FileExtension = "jpg" @@ -587,66 +618,72 @@ def get_Image_Binary(self, Id, ImageType, ImageIndex, ImageTag, UserImage=False) FileExtension = "ukn" ContentType = "image/unknown" - return BinaryData, ContentType, FileExtension + return Payload, ContentType, FileExtension def get_device(self): - return self.EmbyServer.http.request({'params': {'DeviceId': self.EmbyServer.ServerData['DeviceId']}, 'type': "GET", 'handler': "Sessions"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Sessions", {'DeviceId': self.EmbyServer.ServerData['DeviceId']}, {}, False, "", False) + return Payload def get_active_sessions(self): - return self.EmbyServer.http.request({'type': "GET", 'handler': "Sessions"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Sessions", {}, {}, False, "", False) + return Payload - def send_text_msg(self, SessionId, Header, Text, Priority=False, LastWill=False): - self.EmbyServer.http.request({'params': {'Header': f"{Header}", 'Text': f"{Text}"}, 'type': "POST", 'handler': f"Sessions/{SessionId}/Message"}, Priority, False, True, LastWill, Priority) + def send_text_msg(self, SessionId, Header, Text, Priority=False): + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Message", {'Header': f"{Header}", 'Text': f"{Text}"}, Priority),)) def send_play(self, SessionId, ItemId, PlayCommand, StartPositionTicks, Priority=False): - self.EmbyServer.http.request({'params': {'ItemIds': f"{ItemId}", 'StartPositionTicks': f"{StartPositionTicks}", 'PlayCommand': f"{PlayCommand}"}, 'type': "POST", 'handler': f"Sessions/{SessionId}/Playing"}, Priority, False, True, False, Priority) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Playing", {'ItemIds': f"{ItemId}", 'StartPositionTicks': f"{StartPositionTicks}", 'PlayCommand': f"{PlayCommand}"}, Priority),)) def send_pause(self, SessionId, Priority=False): - self.EmbyServer.http.request({'type': "POST", 'handler': f"Sessions/{SessionId}/Playing/Pause"}, Priority, False, True, False, Priority) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Playing/Pause", {}, Priority),)) def send_unpause(self, SessionId, Priority=False): - self.EmbyServer.http.request({'type': "POST", 'handler': f"Sessions/{SessionId}/Playing/Unpause"}, Priority, False, True, False, Priority) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Playing/Unpause", {}, Priority),)) def send_seek(self, SessionId, Position, Priority=False): - self.EmbyServer.http.request({'params': {'SeekPositionTicks': Position}, 'type': "POST", 'handler': f"Sessions/{SessionId}/Playing/Seek"}, Priority, False, True, False, Priority) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Playing/Seek", {'SeekPositionTicks': Position}, Priority),)) def send_stop(self, SessionId, Priority=False): - self.EmbyServer.http.request({'type': "POST", 'handler': f"Sessions/{SessionId}/Playing/Stop"}, Priority, False, True, False, Priority) - - def ping(self): - self.EmbyServer.http.request({'type': "POST", 'handler': "System/Ping"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{SessionId}/Playing/Stop", {}, Priority),)) def get_channels(self): - Data = self.EmbyServer.http.request({'params': {'UserId': self.EmbyServer.ServerData['UserId'], 'EnableImages': True, 'EnableUserData': True, 'Fields': ",".join(EmbyFields['tvchannel'])}, 'type': "GET", 'handler': "LiveTv/Channels"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "LiveTv/Channels", {'UserId': self.EmbyServer.ServerData['UserId'], 'EnableImages': True, 'EnableUserData': True, 'Fields': ",".join(EmbyFields['tvchannel'])}, {}, False, "", False) - if 'Items' in Data: - return Data['Items'] + if 'Items' in Payload: + return Payload['Items'] return [] def get_specialfeatures(self, Id): - return self.EmbyServer.http.request({'params': {'Fields': "Path,MediaSources,PresentationUniqueKey", 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline"}, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/SpecialFeatures"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/SpecialFeatures", {'Fields': "Path,MediaSources,PresentationUniqueKey", 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline"}, {}, False, "", False) + return Payload def get_intros(self, Id): - return self.EmbyServer.http.request({'params': {'Fields': ",".join(EmbyFields["trailer"]), 'EnableTotalRecordCount': False}, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/Intros"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/Intros", {'Fields': ",".join(EmbyFields["trailer"]), 'EnableTotalRecordCount': False}, {}, False, "", False) + return Payload def get_additional_parts(self, Id): - return self.EmbyServer.http.request({'params': {'Fields': "Path,MediaSources"}, 'type': "GET", 'handler': f"Videos/{Id}/AdditionalParts"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Videos/{Id}/AdditionalParts", {'Fields': "Path,MediaSources"}, {}, False, "", False) + return Payload def get_local_trailers(self, Id): - return self.EmbyServer.http.request({'params': {'Fields': ",".join(EmbyFields["trailer"]), 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline"}, 'type': "GET", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/LocalTrailers"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/LocalTrailers", {'Fields': ",".join(EmbyFields["trailer"]), 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline"}, {}, False, "", False) + return Payload def get_themes(self, Id, Songs, Videos): - return self.EmbyServer.http.request({'params': {'Fields': "Path,MediaSources", 'UserId': self.EmbyServer.ServerData['UserId'], 'InheritFromParent': True, 'EnableThemeSongs': Songs, 'EnableThemeVideos': Videos, 'EnableTotalRecordCount': False}, 'type': "GET", 'handler': f"Items/{Id}/ThemeMedia"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Items/{Id}/ThemeMedia", {'Fields': "Path,MediaSources", 'UserId': self.EmbyServer.ServerData['UserId'], 'InheritFromParent': True, 'EnableThemeSongs': Songs, 'EnableThemeVideos': Videos, 'EnableTotalRecordCount': False}, {}, False, "", False) + return Payload def get_sync_queue(self, date): - return self.EmbyServer.http.request({'params': {'LastUpdateDT': date}, 'type': "GET", 'handler': f"Emby.Kodi.SyncQueue/{self.EmbyServer.ServerData['UserId']}/GetItems"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"Emby.Kodi.SyncQueue/{self.EmbyServer.ServerData['UserId']}/GetItems", {'LastUpdateDT': date}, {}, False, "", False) + return Payload def get_system_info(self): - return self.EmbyServer.http.request({'type': "GET", 'handler': "System/Configuration"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "System/Configuration", {}, {}, False, "", False) + return Payload def set_progress(self, Id, Progress, PlayCount): - self.EmbyServer.http.request({'params': {"PlaybackPositionTicks": Progress, "PlayCount": PlayCount, "Played": bool(PlayCount)}, 'type': "POST", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/UserData"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/UserData", {"PlaybackPositionTicks": Progress, "PlayCount": PlayCount, "Played": bool(PlayCount)}, False),)) def set_progress_upsync(self, Id, PlaybackPositionTicks, PlayCount, LastPlayedDate): Params = {"PlaybackPositionTicks": PlaybackPositionTicks, "LastPlayedDate": LastPlayedDate} @@ -654,55 +691,76 @@ def set_progress_upsync(self, Id, PlaybackPositionTicks, PlayCount, LastPlayedDa if PlayCount and PlayCount != -1: Params.update({"PlayCount": PlayCount, "Played": bool(PlayCount)}) - self.EmbyServer.http.request({'params': Params, 'type': "POST", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/UserData"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Users/{self.EmbyServer.ServerData['UserId']}/Items/{Id}/UserData", Params, False),)) def set_played(self, Id, PlayCount): if PlayCount: - self.EmbyServer.http.request({'type': "POST", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/PlayedItems/{Id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Users/{self.EmbyServer.ServerData['UserId']}/PlayedItems/{Id}", {}, False),)) else: - self.EmbyServer.http.request({'type': "DELETE", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/PlayedItems/{Id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("DELETE", f"Users/{self.EmbyServer.ServerData['UserId']}/PlayedItems/{Id}", {}, False),)) def refresh_item(self, Id): - self.EmbyServer.http.request({'params': {'Recursive': True, 'ImageRefreshMode': "FullRefresh", 'MetadataRefreshMode': "FullRefresh", 'ReplaceAllImages': False, 'ReplaceAllMetadata': True}, 'type': "POST", 'handler': f"Items/{Id}/Refresh"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Items/{Id}/Refresh", {'Recursive': True, 'ImageRefreshMode': "FullRefresh", 'MetadataRefreshMode': "FullRefresh", 'ReplaceAllImages': False, 'ReplaceAllMetadata': True}, False),)) def favorite(self, Id, Add): if Add: - self.EmbyServer.http.request({'type': "POST", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/FavoriteItems/{Id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Users/{self.EmbyServer.ServerData['UserId']}/FavoriteItems/{Id}", {}, False),)) else: - self.EmbyServer.http.request({'type': "DELETE", 'handler': f"Users/{self.EmbyServer.ServerData['UserId']}/FavoriteItems/{Id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("DELETE", f"Users/{self.EmbyServer.ServerData['UserId']}/FavoriteItems/{Id}", {}, False),)) - def post_capabilities(self, params): - self.EmbyServer.http.request({'params': params, 'type': "POST", 'handler': "Sessions/Capabilities/Full"}, False, False) + def post_capabilities(self): + self.EmbyServer.http.request("POST", "Sessions/Capabilities/Full", {'Id': self.EmbyServer.EmbySession[0]['Id'], 'SupportsRemoteControl': True, 'PlayableMediaTypes': ["Audio", "Video", "Photo"], 'SupportsMediaControl': True, 'SupportsSync': True, 'SupportedCommands': ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "Select", "Back", "ToggleContextMenu", "ToggleFullscreen", "ToggleOsdMenu", "GoHome", "PageUp", "NextLetter", "GoToSearch", "GoToSettings", "PageDown", "PreviousLetter", "TakeScreenshot", "VolumeUp", "VolumeDown", "ToggleMute", "SendString", "DisplayMessage", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "SetRepeatMode", "Mute", "Unmute", "SetVolume", "Pause", "Unpause", "Play", "Playstate", "PlayNext", "PlayMediaSource"], 'IconUrl': "https://raw.githubusercontent.com/MediaBrowser/plugin.video.emby/master/kodi_icon.png"}, {}, False, "", False) def session_add_user(self, session_id, user_id, option): if option: - self.EmbyServer.http.request({'type': "POST", 'handler': f"Sessions/{session_id}/Users/{user_id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", f"Sessions/{session_id}/Users/{user_id}", {}, False),)) else: - self.EmbyServer.http.request({'type': "DELETE", 'handler': f"Sessions/{session_id}/Users/{user_id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("DELETE", f"Sessions/{session_id}/Users/{user_id}", {}, False),)) def session_playing(self, PlayingItem): - PlayingItemLocal = session_filter_data(PlayingItem) - self.EmbyServer.http.request({'params': PlayingItemLocal, 'type': "POST", 'handler': "Sessions/Playing"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", "Sessions/Playing", session_filter_data(PlayingItem), False),)) - def session_progress(self, PlayingItem): - PlayingItemLocal = session_filter_data(PlayingItem) - self.EmbyServer.http.request({'params': PlayingItemLocal, 'type': "POST", 'handler': "Sessions/Playing/Progress"}, False, False) + def session_progress(self, PlayingItem, EventName): + PlayingItemEvent = {"EventName": EventName} + PlayingItemEvent.update(PlayingItem) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", "Sessions/Playing/Progress", session_filter_data(PlayingItemEvent), False),)) def session_stop(self, PlayingItem): - PlayingItemLocal = session_filter_data(PlayingItem) - self.EmbyServer.http.request({'params': PlayingItemLocal, 'type': "POST", 'handler': "Sessions/Playing/Stopped"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("POST", "Sessions/Playing/Stopped", session_filter_data(PlayingItem), False),)) def session_logout(self): - self.EmbyServer.http.request({'type': "POST", 'handler': "Sessions/Logout"}, False, False) + self.EmbyServer.http.request("POST", "Sessions/Logout", {}, {}, False, "", False) def delete_item(self, Id): - self.EmbyServer.http.request({'type': "DELETE", 'handler': f"Items/{Id}"}, False, False) + self.EmbyServer.http.Queues["ASYNC"].put((("DELETE", f"Items/{Id}", {}, False),)) + + def get_publicinfo(self): + _, _, Payload = self.EmbyServer.http.request("GET", "system/info/public", {}, {}, False, "", False) + return Payload + + def get_exchange(self): + _, _, Payload = self.EmbyServer.http.request("GET", "Connect/Exchange", {'ConnectUserId': self.EmbyServer.ServerData['EmbyConnectUserId']}, {'X-Emby-Token': self.EmbyServer.ServerData['EmbyConnectExchangeToken']}, {}, False, "", False) + return Payload + + def get_authbyname(self, Username, Password): + _, _, Payload = self.EmbyServer.http.request("POST", "Users/AuthenticateByName", {'username': Username, 'pw': Password or ""}, {}, False, "", False) + return Payload def get_stream_statuscode(self, Id, MediasourceID): - return self.EmbyServer.http.request({'params': {'static': True, 'MediaSourceId': MediasourceID, 'DeviceId': self.EmbyServer.ServerData['DeviceId']}, 'type': "HEAD", 'handler': f"videos/{Id}/stream"}, False, False) + StatusCode, _, _ = self.EmbyServer.http.request("HEAD", f"videos/{Id}/stream", {'static': True, 'MediaSourceId': MediasourceID, 'DeviceId': self.EmbyServer.ServerData['DeviceId']}, {}, False, "", False) + return StatusCode def get_Subtitle_Binary(self, Id, MediasourceID, SubtitleId, SubtitleFormat): - return self.EmbyServer.http.request({'type': "GET", 'handler': f"/videos/{Id}/{MediasourceID}/Subtitles/{SubtitleId}/stream.{SubtitleFormat}"}, False, True, False) + _, _, Payload = self.EmbyServer.http.request("GET", f"videos/{Id}/{MediasourceID}/Subtitles/{SubtitleId}/stream.{SubtitleFormat}", {}, {}, True, "", False) + return Payload + + def get_embyconnect_authenticate(self, Username, Password): + _, _, Payload = self.EmbyServer.http.request("POST", "service/user/authenticate", {'nameOrEmail': Username, 'rawpw': Password}, {'X-Application': f"{utils.addon_name}/{utils.addon_version}"}, False, "https://connect.emby.media:443", True) + return Payload + + def get_embyconnect_servers(self): + _, _, Payload = self.EmbyServer.http.request("GET", f"service/servers?userId={self.EmbyServer.ServerData['EmbyConnectUserId']}", {}, {'X-Connect-UserToken': self.EmbyServer.ServerData['EmbyConnectAccessToken'], 'X-Application': f"{utils.addon_name}/{utils.addon_version}"}, False, "https://connect.emby.media:443", True) + return Payload def get_Fields(self, MediaType, Basic, Dynamic): if not Basic: @@ -724,18 +782,18 @@ def get_Fields(self, MediaType, Basic, Dynamic): return Fields def get_upcoming(self, ParentId): - Data = self.EmbyServer.http.request({'params': {'UserId': self.EmbyServer.ServerData['UserId'], 'ParentId': ParentId, 'Fields': ",".join(EmbyFields["episode"]), 'EnableImages': True, 'EnableUserData': True}, 'type': "GET", 'handler': "Shows/Upcoming"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Shows/Upcoming", {'UserId': self.EmbyServer.ServerData['UserId'], 'ParentId': ParentId, 'Fields': ",".join(EmbyFields["episode"]), 'EnableImages': True, 'EnableUserData': True}, {}, False, "", False) - if 'Items' in Data: - return Data['Items'] + if 'Items' in Payload: + return Payload['Items'] return [] def get_NextUp(self, ParentId): - Data = self.EmbyServer.http.request({'params': {'UserId': self.EmbyServer.ServerData['UserId'], 'ParentId': ParentId, 'Fields': ",".join(EmbyFields["episode"]), 'EnableImages': True, 'EnableUserData': True, 'LegacyNextUp': True}, 'type': "GET", 'handler': "Shows/NextUp"}, False, False) + _, _, Payload = self.EmbyServer.http.request("GET", "Shows/NextUp", {'UserId': self.EmbyServer.ServerData['UserId'], 'ParentId': ParentId, 'Fields': ",".join(EmbyFields["episode"]), 'EnableImages': True, 'EnableUserData': True, 'LegacyNextUp': True}, {}, False, "", False) - if 'Items' in Data: - return Data['Items'] + if 'Items' in Payload: + return Payload['Items'] return [] diff --git a/emby/emby.py b/emby/emby.py index 8a88ddbcc..fa361949c 100644 --- a/emby/emby.py +++ b/emby/emby.py @@ -6,7 +6,6 @@ from dialogs import serverconnect, usersconnect, loginconnect, loginmanual, servermanual from helper import utils, playerops from database import library -from hooks import websocket from . import views, api, http @@ -14,39 +13,46 @@ class EmbyServer: def __init__(self, ServerSettings): self.ShutdownInProgress = False self.EmbySession = [] - self.Websocket = None self.Found_Servers = [] self.ServerSettings = ServerSettings self.Firstrun = not bool(self.ServerSettings) - self.ServerData = {'AccessToken': "", 'UserId': "", 'UserName': "", 'UserImageUrl': "", 'ServerName': "", 'ServerId': "", 'ServerUrl': "", 'EmbyConnectExchangeToken': "", 'EmbyConnectUserId': "", 'EmbyConnectUserName': "", 'EmbyConnectAccessToken': "", 'LastConnectionMode': "", 'ManualAddress': "", 'RemoteAddress': "", 'LocalAddress': "" ,'EmbyConnectRemoteAddress': "", 'EmbyConnectLocalAddress': "", 'AdditionalUsers': {}, "DeviceId": "", "Online": False} + self.ServerData = {'AccessToken': "", 'UserId': "", 'UserName': "", 'UserImageUrl': "", 'ServerName': "", 'ServerId': "", 'ServerUrl': "", 'EmbyConnectExchangeToken': "", 'EmbyConnectUserId': "", 'EmbyConnectUserName': "", 'EmbyConnectAccessToken': "", 'ManualAddress': "", 'RemoteAddress': "", 'LocalAddress': "" ,'AdditionalUsers': {}, "DeviceId": ""} self.ServerReconnecting = False self.http = http.HTTP(self) self.API = api.API(self) self.Views = views.Views(self) self.library = library.Library(self) + self.Online = False xbmc.log("EMBY.emby.emby: ---[ INIT EMBYCLIENT: ]---", 1) # LOGINFO def ServerReconnect(self): + if self.Firstrun: + return + if not self.ServerReconnecting: start_new_thread(self.worker_ServerReconnect, ()) def worker_ServerReconnect(self): xbmc.log(f"EMBY.emby.emby: THREAD: --->[ Reconnecting ] {self.ServerData['ServerName']} / {self.ServerData['ServerId']}", 0) # LOGDEBUG - utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message=utils.Translate(33575), time=utils.displayMessage, sound=True) if not self.ServerReconnecting: + utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message=utils.Translate(33575), time=utils.displayMessage, sound=False) utils.SyncPause.update({f"server_reconnecting_{self.ServerData['ServerId']}": True, f"server_busy_{self.ServerData['ServerId']}": False}) self.ServerReconnecting = True - self.stop() while True: + self.stop() + if utils.sleep(1): - self.http.stop_session() + break + + if not self.ServerData['ServerUrl']: + xbmc.log("EMBY.emby.emby: Reconnect exit by empty ServerUrl", 1) # LOGINFO break xbmc.log(f"EMBY.emby.emby: Reconnect try again: {self.ServerData['ServerName']} / {self.ServerData['ServerId']}", 1) # LOGINFO - if self.ServerConnect(True): + if self.ServerHandshake(): self.start() break @@ -56,20 +62,15 @@ def worker_ServerReconnect(self): xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Reconnecting ] {self.ServerData['ServerName']} / {self.ServerData['ServerId']}", 0) # LOGDEBUG def start(self): - xbmc.log(f"EMBY.emby.emby: ---[ START EMBYCLIENT: {self.ServerData['ServerName']} / {self.ServerData['ServerId']} / {self.ServerData['LastConnectionMode']}]---", 1) # LOGINFO + xbmc.log(f"EMBY.emby.emby: ---[ START EMBYCLIENT: {self.ServerData['ServerName']} / {self.ServerData['ServerId']}]---", 1) # LOGINFO utils.SyncPause[f"server_starting_{self.ServerData['ServerId']}"] = True - self.ServerData['Online'] = True + self.Online = True self.library.load_settings() playerops.init_RemoteClient(self.ServerData['ServerId']) self.Views.update_views() self.Views.update_nodes() - - if utils.websocketenabled and not self.Websocket: - self.Websocket = websocket.WSClient(self) - self.Websocket.start() - + self.http.start() start_new_thread(self.library.KodiStartSync, (self.Firstrun,)) # start initial sync - start_new_thread(self.Ping, ()) self.Firstrun = False if utils.connectMsg: @@ -79,23 +80,21 @@ def start(self): xbmc.log("EMBY.emby.emby: [ Server Online ]", 1) # LOGINFO def stop(self): + xbmc.log(f"EMBY.emby.emby: --->[ STOP EMBYCLIENT: {self.ServerData['ServerId']} ]---", 1) # LOGINFO + if self.EmbySession and not self.ShutdownInProgress: + xbmc.log("EMBY.emby.emby: Emby client stop", 0) # LOGDEBUG self.ShutdownInProgress = True - xbmc.log(f"EMBY.emby.emby: ---[ STOP EMBYCLIENT: {self.ServerData['ServerId']} ]---", 1) # LOGINFO utils.SyncPause.update({f"server_starting_{self.ServerData['ServerId']}": True, f"server_busy_{self.ServerData['ServerId']}": False}) - playerops.delete_RemoteClient(self.ServerData['ServerId'], [self.EmbySession[0]['Id']], True, True) - - if self.Websocket: - self.Websocket.close() - self.Websocket = None - + playerops.delete_RemoteClient(self.ServerData['ServerId'], [self.EmbySession[0]['Id']], True) self.EmbySession = [] - self.ServerData['Online'] = False + self.Online = False self.ShutdownInProgress = False else: - xbmc.log("EMBY.emby.emby: Emby client already closed", 1) # LOGINFO + xbmc.log("EMBY.emby.emby: Emby client already closed", 0) # LOGDEBUG - self.http.stop_session() + self.http.stop() + xbmc.log(f"EMBY.emby.emby: ---<[ STOP EMBYCLIENT: {self.ServerData['ServerId']} ]---", 1) # LOGINFO def add_AdditionalUser(self, UserId, UserName): self.ServerData['AdditionalUsers'][UserId] = UserName @@ -114,14 +113,14 @@ def remove_AdditionalUser(self, UserId): def ServerInitConnection(self): xbmc.log("EMBY.emby.emby: --[ server/DEFAULT ]", 1) # LOGINFO - # load credentials froom file + # load credentials from file if self.ServerSettings: FileData = utils.readFileString(self.ServerSettings) if FileData: LoadedServerSettings = json.loads(FileData) - if 'ServerId' in LoadedServerSettings and LoadedServerSettings['ServerId']: # file content is not valid + if 'ServerId' in LoadedServerSettings and LoadedServerSettings['ServerId']: # file content is valid self.ServerData = LoadedServerSettings utils.DatabaseFiles[self.ServerData['ServerId']] = utils.translatePath(f"special://profile/Database/emby_{self.ServerData['ServerId']}.db") @@ -129,15 +128,15 @@ def ServerInitConnection(self): self.ServerData["DeviceId"] = str(uuid.uuid4()) # Refresh EmbyConnect Emby server addresses (dynamic IP) - if self.ServerData["LastConnectionMode"] in ("EmbyConnectLocalAddress", "EmbyConnectRemoteAddress"): + if self.ServerData["EmbyConnectAccessToken"]: xbmc.log("EMBY.emby.emby: Refresh Emby server urls from EmbyConnect", 1) # LOGINFO - result = self.request_url({'type': "GET", 'url': f"https://connect.emby.media/service/servers?userId={self.ServerData['EmbyConnectUserId']}", 'headers': {'X-Connect-UserToken': self.ServerData['EmbyConnectAccessToken']}}) + EmbyConnectServers = self.API.get_embyconnect_servers() - if result: - for server in result: - if server['SystemId'] == self.ServerData['ServerId']: - if self.ServerData['EmbyConnectRemoteAddress'] != server['Url'] or self.ServerData['EmbyConnectLocalAddress'] != server['LocalAddress']: # update server settings - self.ServerData.update({'EmbyConnectRemoteAddress': server['Url'], 'EmbyConnectLocalAddress': server['LocalAddress']}) + if EmbyConnectServers: + for EmbyConnectServer in EmbyConnectServers: + if EmbyConnectServer['SystemId'] == self.ServerData['ServerId']: + if self.ServerData['RemoteAddress'] != EmbyConnectServer['Url'] or self.ServerData['LocalAddress'] != EmbyConnectServer['LocalAddress']: # update server settings + self.ServerData.update({'RemoteAddress': EmbyConnectServer['Url'], 'LocalAddress': EmbyConnectServer['LocalAddress']}) self.save_credentials() xbmc.log("EMBY.emby.emby: Update Emby server urls from EmbyConnect", 1) # LOGINFO @@ -155,43 +154,45 @@ def ServerInitConnection(self): break Dialog = serverconnect.ServerConnect("script-emby-connect-server.xml", *utils.CustomDialogParameters) - Dialog.EmbyServer = self + Dialog.Servers = self.Found_Servers Dialog.UserImageUrl = self.ServerData['UserImageUrl'] Dialog.emby_connect = not self.ServerData['UserId'] Dialog.doModal() + ConnectionMode = Dialog.ConnectionMode + self.ServerData.update(Dialog.ServerSelected) del Dialog - if self.ServerData['LastConnectionMode'] in ("LocalAddress", "RemoteAddress",): - if self.UserLogin(): - if self.ServerConnect(): # SignedIn - break + if ConnectionMode == "ListSelection": + if self.TestConnections(): + Password = self.UserSelection() - self.ServerData['LastConnectionMode'] = "" - elif self.ServerData['LastConnectionMode'] == "ManualAddress": + if self.ServerLogin(Password): + if self.ServerHandshake(): + break + elif ConnectionMode == "ManualAddress": xbmc.log("EMBY.emby.emby: Adding manual server", 0) # LOGDEBUG Dialog = servermanual.ServerManual("script-emby-connect-server-manual.xml", *utils.CustomDialogParameters) - Dialog.connect_to_address = self.connect_to_address Dialog.doModal() + self.ServerData['ManualAddress'] = Dialog.ManualAddress del Dialog if self.ServerData['ManualAddress']: - if self.UserLogin(): - if self.ServerConnect(): # SignedIn - break - - self.ServerData['LastConnectionMode'] = "" - elif self.ServerData['LastConnectionMode'] in ("EmbyConnectLocalAddress", "EmbyConnectRemoteAddress"): - if self.ServerConnect(): # SignedIn - break + if self.TestConnections(): + Password = self.UserSelection() - self.ServerData['LastConnectionMode'] = "" - elif self.ServerData['LastConnectionMode'] == "EmbyConnect": - self.ServerData['LastConnectionMode'] = "" + if self.ServerLogin(Password): + if self.ServerHandshake(): + break + elif ConnectionMode == "EmbyConnect": Dialog = loginconnect.LoginConnect("script-emby-connect-login.xml", *utils.CustomDialogParameters) - Dialog.EmbyServer = self Dialog.doModal() + Username, Password = Dialog.Login del Dialog - else: + + if Username and Password: + self.EmbyConnectServers(Username, Password) + continue + else: # cancel SignedIn = False break @@ -203,15 +204,16 @@ def ServerInitConnection(self): # re-establish connection utils.EmbyServers[self.ServerData['ServerId']] = self - start_new_thread(self.establish_existing_connection, ()) + start_new_thread(self.EstablishExistingConnection, ()) - def establish_existing_connection(self): - xbmc.log("EMBY.emby.emby: THREAD: --->[ establish_existing_connection ]", 0) # LOGDEBUG + def EstablishExistingConnection(self): + xbmc.log("EMBY.emby.emby: THREAD: --->[ EstablishExistingConnection ]", 0) # LOGDEBUG - if self.ServerConnect(): - self.start() + if self.TestConnections(): + if self.ServerHandshake(): + self.start() - xbmc.log("EMBY.emby.emby: THREAD: ---<[ establish_existing_connection ]", 0) # LOGDEBUG + xbmc.log("EMBY.emby.emby: THREAD: ---<[ EstablishExistingConnection ]", 0) # LOGDEBUG def save_credentials(self): if not self.ServerSettings: @@ -225,92 +227,102 @@ def ServerDisconnect(self): utils.EmbyServers[self.ServerData['ServerId']].stop() utils.delFile(f"{utils.FolderAddonUserdata}servers_{self.ServerData['ServerId']}.json") self.EmbySession = [] - self.ServerData['Online'] = False + self.Online = False - def ServerConnect(self, Reconnect=False): - # Connect to server verification - if self.ServerData["AccessToken"]: - if self._try_connect(self.ServerData["ServerUrl"]): - return True + def ServerHandshake(self): + self.EmbySession = self.API.get_device() - if not Reconnect and self.connect_to_server(): - return True + if not self.EmbySession: + xbmc.log(f"EMBY.emby.emby: ---[ SESSION ERROR EMBYCLIENT: {self.ServerData['ServerId']} ] {self.EmbySession} ---", 3) # LOGERROR + self.http.stop() + return False - return False + if not self.ServerData['UserName']: + self.ServerData['UserName'] = self.EmbySession[0]['UserName'] - def UserLogin(self): - users = self.API.get_public_users() + self.API.post_capabilities() - if not users: - return self.login_manual(None) + for AdditionalUserId in self.ServerData['AdditionalUsers']: + AddUser = True - Dialog = usersconnect.UsersConnect("script-emby-connect-users.xml", *utils.CustomDialogParameters) - Dialog.API = self.API - Dialog.ServerData = self.ServerData - Dialog.users = users - Dialog.doModal() - SelectedUser = Dialog.SelectedUser - ManualLogin = Dialog.ManualLogin - del Dialog + for SessionAdditionalUser in self.EmbySession[0]['AdditionalUsers']: + if SessionAdditionalUser['UserId'] == AdditionalUserId: + AddUser = False + break - if SelectedUser: - if SelectedUser['HasPassword']: - xbmc.log("EMBY.emby.emby: User has password, present manual login", 0) # LOGDEBUG - Result = self.login_manual(SelectedUser['Name']) + if AddUser: + if utils.connectMsg: + utils.Dialog.notification(heading=utils.addon_name, message=f"{utils.Translate(33067)} {self.ServerData['AdditionalUsers'][AdditionalUserId]}", icon=utils.icon, time=utils.displayMessage, sound=False) + self.API.session_add_user(self.EmbySession[0]['Id'], AdditionalUserId, True) - if Result: - return Result - else: - return self.ServerLogin(self.ServerData['ServerUrl'], SelectedUser['Name'], None) - elif ManualLogin: - Result = self.login_manual("") + return True - if Result: - return Result - else: - return False # No user selected + def UserSelection(self): + Username = "" + Users = self.API.get_public_users() + + if Users: + UsersInfo = [] + + for User in Users: + UserData = User.copy() + UserData['UserImageUrl'] = utils.icon + + # Download user picture + BinaryData, _, FileExtension = self.API.get_Image_Binary(UserData['Id'], "Primary", 0, 0, True) + + if BinaryData: + Filename = utils.valid_Filename(f"{self.ServerData['ServerName']}_{UserData['Name']}_{UserData['Id']}.{FileExtension}") + iconpath = f"{utils.FolderEmbyTemp}{Filename}" + utils.delFile(iconpath) + utils.writeFileBinary(iconpath, BinaryData) + UserData['UserImageUrl'] = iconpath + + UsersInfo.append(UserData) - return self.UserLogin() + Dialog = usersconnect.UsersConnect("script-emby-connect-users.xml", *utils.CustomDialogParameters) + Dialog.users = UsersInfo + Dialog.doModal() + SelectedUser = Dialog.SelectedUser + del Dialog + + if SelectedUser and SelectedUser != "MANUAL": + self.ServerData.update({'UserImageUrl': SelectedUser['UserImageUrl'], 'UserName': SelectedUser['Name']}) + + + if SelectedUser['HasPassword']: + xbmc.log("EMBY.emby.emby: User has password, present manual login", 0) # LOGDEBUG + Username = SelectedUser['Name'] + else: + self.ServerData["UserName"] = SelectedUser['Name'] + return "" - # Return manual login user authenticated - def login_manual(self, UserName): Dialog = loginmanual.LoginManual("script-emby-connect-login-manual.xml", *utils.CustomDialogParameters) - Dialog.username = UserName - Dialog.EmbyServer = self + Dialog.username = Username Dialog.doModal() SelectedUser = Dialog.SelectedUser del Dialog - return SelectedUser - - def login_to_connect(self, username, password): - if not username: - return {} # username cannot be empty - - if not password: - return {} # password cannot be empty + self.ServerData["UserName"], Password = SelectedUser + return Password - result = self.request_url({'type': "POST", 'url': "https://connect.emby.media/service/user/authenticate", 'params': {'nameOrEmail': username, 'rawpw': password}}) + def EmbyConnectServers(self, Username, Password): + Data = self.API.get_embyconnect_authenticate(Username, Password) - if not result: # Failed to login - return {} + if not Data: # Failed to login + return - # Signed in with EmbyConnect user - self.ServerData.update({'EmbyConnectUserId': result['User']['Id'], 'EmbyConnectUserName': result['User']['Name'], 'EmbyConnectAccessToken': result['AccessToken']}) + self.ServerData.update({'EmbyConnectUserId': Data['User']['Id'], 'EmbyConnectUserName': Data['User']['Name'], 'EmbyConnectAccessToken': Data['AccessToken']}) xbmc.log("EMBY.emby.emby: Begin getConnectServers", 0) # LOGDEBUG + EmbyConnectServers = self.API.get_embyconnect_servers() - if self.ServerData['EmbyConnectAccessToken'] and self.ServerData['EmbyConnectUserId']: - EmbyConnectServers = self.request_url({'type': "GET", 'url': f"https://connect.emby.media/service/servers?userId={self.ServerData['EmbyConnectUserId']}", 'headers': {'X-Connect-UserToken': self.ServerData['EmbyConnectAccessToken']}}) + if EmbyConnectServers: + for EmbyConnectServer in EmbyConnectServers: + self.Found_Servers.append({'ExchangeToken': EmbyConnectServer['AccessKey'], 'ConnectServerId': EmbyConnectServer['Id'], 'Id': EmbyConnectServer['SystemId'], 'Name': f"Emby Connect: {EmbyConnectServer['Name']}", 'RemoteAddress': EmbyConnectServer['Url'], 'LocalAddress': EmbyConnectServer['LocalAddress'], 'UserLinkType': "Guest" if EmbyConnectServer['UserType'].lower() == "guest" else "LinkedUser"}) - if EmbyConnectServers: - for EmbyConnectServer in EmbyConnectServers: - self.Found_Servers.append({'ExchangeToken': EmbyConnectServer['AccessKey'], 'ConnectServerId': EmbyConnectServer['Id'], 'Id': EmbyConnectServer['SystemId'], 'Name': f"Emby Connect: {EmbyConnectServer['Name']}", 'RemoteAddress': EmbyConnectServer['Url'], 'LocalAddress': EmbyConnectServer['LocalAddress'], 'UserLinkType': "Guest" if EmbyConnectServer['UserType'].lower() == "guest" else "LinkedUser"}) - - return result - - def ServerLogin(self, ServerUrl, username, password): + def ServerLogin(self, password): xbmc.log("EMBY.emby.emby: Login to server", 1) # LOGINFO - if not username: + if not self.ServerData["UserName"]: xbmc.log("EMBY.emby.emby: Username cannot be empty", 3) # LOGERROR return False @@ -318,85 +330,14 @@ def ServerLogin(self, ServerUrl, username, password): if self.ServerData['ServerId'] in utils.EmbyServers: self.ServerDisconnect() - result = self.http.request({'type': "POST", 'url': f"{ServerUrl}/emby/Users/AuthenticateByName", 'params': {'username': username, 'pw': password or ""}}, True, False) - - if not result: - return False - - self.ServerData.update({'UserId': result['User']['Id'], 'AccessToken': result['AccessToken']}) - return result - - def get_PublicInfo(self, address): - PublicInfoUrl = f"{address}/emby/system/info/public" - xbmc.log(f"EMBY.emby.emby: tryConnect url: {address}", 1) # LOGINFO - return self.request_url({'type': "GET", 'url': PublicInfoUrl}) - - def connect_to_address(self, address): - if not address: - return False - - address = normalize_address(address) - PublicInfo = self.get_PublicInfo(address) + Data = self.API.get_authbyname(self.ServerData["UserName"], password) - if not PublicInfo: + if not Data: return False - self.ServerData.update({'ManualAddress': address, 'LastConnectionMode': "ManualAddress", 'ServerName': PublicInfo['ServerName'], 'ServerId': PublicInfo['Id'], 'ServerUrl': address}) - xbmc.log(f"EMBY.emby.emby: ConnectToAddress {address} succeeded", 1) # LOGINFO + self.ServerData.update({'UserId': Data['User']['Id'], 'AccessToken': Data['AccessToken']}) return True - def connect_to_server(self): - xbmc.log("EMBY.emby.emby: Begin connectToServer", 0) # LOGDEBUG - Connections = [] - - # Try local connections first - if self.ServerData['LastConnectionMode']: - Connections.append(self.ServerData['LastConnectionMode'].replace("RemoteAddress", "LocalAddress")) # Local connection priority - - if "EmbyConnectLocalAddress" not in Connections: - Connections.append("EmbyConnectLocalAddress") - - if "EmbyConnectRemoteAddress" not in Connections: - Connections.append("EmbyConnectRemoteAddress") - - if "ManualAddress" not in Connections: - Connections.append("ManualAddress") - - if "LocalAddress" not in Connections: - Connections.append("LocalAddress") - - if "RemoteAddress" not in Connections: - Connections.append("RemoteAddress") - - for Connection in Connections: - if utils.SystemShutdown: - return False - - ConnectUrl = self.ServerData.get(Connection) - - if not ConnectUrl: - xbmc.log(f"EMBY.emby.emby: Skip Emby server connection test: {Connection}", 1) # LOGINFO - continue - - # Emby Connect - if self.ServerData['EmbyConnectExchangeToken'] and self.ServerData['EmbyConnectUserId']: - auth = self.request_url({'url': f"{ConnectUrl}/emby/Connect/Exchange", 'type': "GET", 'params': {'ConnectUserId': self.ServerData['EmbyConnectUserId']}, 'headers': {'X-Emby-Token': self.ServerData['EmbyConnectExchangeToken'], 'Authorization': f"Emby Client={utils.addon_name},Device={utils.device_name},DeviceId={self.ServerData['DeviceId']},Version={utils.addon_version}"}}) - - if auth: - self.ServerData.update({'UserId': auth['LocalUserId'], 'AccessToken': auth['AccessToken']}) - else: - self.ServerData.update({'UserId': "", 'AccessToken': ""}) - - self.ServerData.update({'LastConnectionMode': Connection, 'ServerUrl': ConnectUrl}) - - if not self._try_connect(ConnectUrl): - continue - - return True - - xbmc.log("EMBY.emby.emby: Tested all connection modes. Failing server connection", 1) # LOGINFO - return False - def ServerDetect(self): xbmc.log("EMBY.emby.emby: Begin getAvailableServers", 0) # LOGDEBUG MULTI_GROUP = ("", 7359) @@ -408,16 +349,17 @@ def ServerDetect(self): xbmc.log(f"EMBY.emby.emby: Sending UDP Data: {MESSAGE}", 0) # LOGDEBUG found_servers = [] + # get severs via broadcast try: sock.sendto(MESSAGE, MULTI_GROUP) while True: try: data, _ = sock.recvfrom(1024) # buffer size - IncommingData = json.loads(data) + IncomingData = json.loads(data) - if IncommingData not in found_servers: - found_servers.append(IncommingData) + if IncomingData not in found_servers: + found_servers.append(IncomingData) except _socket.timeout: xbmc.log(f"EMBY.emby.emby: Found Servers: {found_servers}", 1) # LOGINFO break @@ -433,14 +375,17 @@ def ServerDetect(self): server = "" if found_server.get('Address') and found_server.get('EndpointAddress'): - address = found_server['EndpointAddress'].split(':')[0] - # Determine the port, if any + server = found_server['EndpointAddress'].split(':')[0] parts = found_server['Address'].split(':') if len(parts) > 1: port_string = parts[len(parts) - 1] - address += f":{port_string}" - server = normalize_address(address) + server += f":{port_string}" + server = server.strip() + server = server.lower() + + if 'http' not in server: + server = f"http://{server}" if not server and not found_server.get('Address'): xbmc.log(f"EMBY.emby.emby: Server {found_server} has no address", 2) # LOGWARNING @@ -448,72 +393,34 @@ def ServerDetect(self): self.Found_Servers.append({'Id': found_server['Id'], 'LocalAddress': server or found_server['Address'], 'Name': found_server['Name']}) - def request_url(self, request): - request.setdefault('headers', {}) - request['headers'].update({'Accept': "application/json", 'Accept-Charset': "UTF-8,*", 'Accept-encoding': "gzip", 'X-Application': f"{utils.addon_name}/{utils.addon_version}", 'Content-type': 'application/json'}) - return self.http.request(request, True, False) - - def _try_connect(self, address): - PublicInfo = self.get_PublicInfo(address) - - if PublicInfo: - xbmc.log("EMBY.emby.emby: User is authenticated", 1) # LOGINFO - self.ServerData.update({'RemoteAddress': PublicInfo.get('WanAddress', self.ServerData['RemoteAddress']), 'LocalAddress': PublicInfo.get('LocalAddress', self.ServerData['LocalAddress']), 'ServerName': PublicInfo.get('ServerName'), 'ServerId': PublicInfo.get('Id'), 'ServerUrl': address}) - utils.DatabaseFiles[self.ServerData['ServerId']] = utils.translatePath(f"special://profile/Database/emby_{self.ServerData['ServerId']}.db") - - if not self.ServerData.get('AccessToken', ""): - return False - - self.EmbySession = self.API.get_device() + def TestConnections(self): + xbmc.log("EMBY.emby.emby: Begin connectToServer", 0) # LOGDEBUG - if not self.EmbySession: - xbmc.log(f"EMBY.emby.emby: ---[ SESSION ERROR EMBYCLIENT: {self.ServerData['ServerId']} ] {self.EmbySession} ---", 3) # LOGERROR - self.http.stop_session() + for Connection in ("ManualAddress", "LocalAddress", "RemoteAddress"): + if utils.SystemShutdown: return False - if not self.ServerData['UserName']: - self.ServerData['UserName'] = self.EmbySession[0]['UserName'] - - self.API.post_capabilities({'Id': self.EmbySession[0]['Id'], 'SupportsRemoteControl': "true",'PlayableMediaTypes': "Audio,Video,Photo", 'SupportsMediaControl': True, 'SupportsSync': True, 'SupportedCommands': "MoveUp,MoveDown,MoveLeft,MoveRight,Select,Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,GoHome,PageUp,NextLetter,GoToSearch,GoToSettings,PageDown,PreviousLetter,TakeScreenshot,VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,SetAudioStreamIndex,SetSubtitleStreamIndex,SetRepeatMode,Mute,Unmute,SetVolume,Pause,Unpause,Play,Playstate,PlayNext,PlayMediaSource", 'IconUrl': "https://raw.githubusercontent.com/MediaBrowser/plugin.video.emby/master/kodi_icon.png"}) - - for AdditionalUserId in self.ServerData['AdditionalUsers']: - AddUser = True - - for SessionAdditionalUser in self.EmbySession[0]['AdditionalUsers']: - if SessionAdditionalUser['UserId'] == AdditionalUserId: - AddUser = False - break + if not self.ServerData[Connection]: + xbmc.log(f"EMBY.emby.emby: Skip Emby server connection test: {Connection}", 1) # LOGINFO + continue - if AddUser: - if utils.connectMsg: - utils.Dialog.notification(heading=utils.addon_name, message=f"{utils.Translate(33067)} {self.ServerData['AdditionalUsers'][AdditionalUserId]}", icon=utils.icon, time=utils.displayMessage, sound=False) - self.API.session_add_user(self.EmbySession[0]['Id'], AdditionalUserId, True) + self.ServerData['ServerUrl'] = self.ServerData[Connection] - if not self.ServerData['UserImageUrl']: - self.ServerData['UserImageUrl'] = utils.icon + if not self.UpdateServerInfo(): + self.ServerData['ServerUrl'] = "" + continue return True + xbmc.log("EMBY.emby.emby: Tested all connection modes. Failing server connection", 1) # LOGINFO return False - # Ping server -> keep http session open - def Ping(self): - xbmc.log(f"EMBY.emby.emby: THREAD: --->[ Ping {self.ServerData['ServerId']} ]", 0) # LOGDEBUG - - while True: - for _ in range(30): - if utils.sleep(1) or not self.ServerData['Online']: - xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Ping {self.ServerData['ServerId']} ]", 0) # LOGDEBUG - return + def UpdateServerInfo(self): + PublicInfo = self.API.get_publicinfo() - self.API.ping() - -def normalize_address(address): - # Attempt to correct bad input - address = address.strip() - address = address.lower() - - if 'http' not in address: - address = f"http://{address}" + if PublicInfo: + self.ServerData.update({'RemoteAddress': PublicInfo.get('WanAddress', self.ServerData['RemoteAddress']), 'LocalAddress': PublicInfo.get('LocalAddress', self.ServerData['LocalAddress']), 'ServerName': PublicInfo.get('ServerName'), 'ServerId': PublicInfo.get('Id')}) + utils.DatabaseFiles[self.ServerData['ServerId']] = utils.translatePath(f"special://profile/Database/emby_{self.ServerData['ServerId']}.db") + return True - return address + return False diff --git a/emby/http.py b/emby/http.py index 300e51d2c..9adf92952 100644 --- a/emby/http.py +++ b/emby/http.py @@ -1,378 +1,882 @@ -from _thread import start_new_thread +from _thread import start_new_thread, allocate_lock +import base64 +import os +import array +import struct +import hashlib +import json +import zlib +import ssl +import uuid import _socket -import urllib3 import xbmc import xbmcgui from helper import utils, queue, artworkcache from database import dbio from core import common +from hooks import websocket -TimeoutPriority = urllib3.util.timeout.Timeout(connect=1, read=0.5) -TimeoutRegular = urllib3.util.timeout.Timeout(connect=15, read=300) -TimeoutAsync = urllib3.util.timeout.Timeout(connect=5, read=2) -urllib3v1 = False - -if urllib3.__version__[:1] == "1": - import json - urllib3v1 = True class HTTP: def __init__(self, EmbyServer): - self.session = None self.EmbyServer = EmbyServer self.Intros = [] - self.HeaderCache = {} - self.AsyncCommandQueue = queue.Queue() - self.FileDownloadQueue = queue.Queue() - self.Priority = False + self.Queues = {"ASYNC": queue.Queue(), "DOWNLOAD": queue.Queue()} + self.Connection = {} + self.SocketBusy = {"MAIN": allocate_lock()} + self.Connecting = allocate_lock() + self.Running = False + self.inProgressWebSocket = False + self.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS) + self.SSLContext.load_default_certs() + self.Websocket = websocket.WebSocket(EmbyServer) + self.WebsocketBuffer = b"" + + if utils.sslverify: + self.SSLContext.verify_mode = ssl.CERT_REQUIRED + else: + self.SSLContext.verify_mode = ssl.CERT_NONE + + def start(self): + with self.Connecting: + if not self.Running: + self.Running = True + xbmc.log("EMBY.emby.http: --->[ HTTP ]", 1) # LOGINFO + self.Queues["ASYNC"].clear() + self.Queues["DOWNLOAD"].clear() + start_new_thread(self.Ping, ()) + start_new_thread(self.async_commands, ()) + start_new_thread(self.download_file, ()) - def download_file(self): - xbmc.log("EMBY.emby.http: THREAD: --->[ async file download ]", 0) # LOGDEBUG + if utils.websocketenabled: + start_new_thread(self.Websocket.Message, ()) + start_new_thread(self.websocket_listen, ()) - while True: - Command = self.FileDownloadQueue.get() + def stop(self): + with self.Connecting: + if self.Running: + self.Running = False + xbmc.log("EMBY.emby.http: ---<[ HTTP ]", 1) # LOGINFO + self.Queues["ASYNC"].put("QUIT") + self.Queues["DOWNLOAD"].put("QUIT") - if Command == "QUIT": - xbmc.log("EMBY.emby.http: Download Queue closed", 1) # LOGINFO - self.FileDownloadQueue.clear() + if utils.websocketenabled: + self.Websocket.MessageQueue.put("QUIT") + + for ConnectionId in list(self.Connection.keys()): + self.socket_close(ConnectionId) + + def socket_open(self, ConnectionString, ConnectionId): + NewHeader = False + + if ConnectionId not in self.Connection: + self.Connection[ConnectionId] = {} + + if "ConnectionString" not in self.Connection[ConnectionId]: + self.Connection[ConnectionId]["ConnectionString"] = ConnectionString + NewHeader = True + else: + if self.Connection[ConnectionId]["ConnectionString"] != ConnectionString: + self.Connection[ConnectionId]["ConnectionString"] = ConnectionString + NewHeader = True + + if NewHeader: + try: + Scheme, self.Connection[ConnectionId]["Hostname"], self.Connection[ConnectionId]["Port"] = utils.get_url_info(ConnectionString) + except Exception as error: + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Wrong ConnectionString: {ConnectionString} / {error}", 2) # LOGWARNING + + if ConnectionId == "MAIN": + utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message="Invalid server address", time=utils.displayMessage, sound=False) + + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + return 611 + + self.Connection[ConnectionId]["SSL"] = bool(Scheme == "https") + self.Connection[ConnectionId]["RequestHeader"] = {"Host": f"{self.Connection[ConnectionId]['Hostname']}:{self.Connection[ConnectionId]['Port']}", 'Content-type': 'application/json; charset=utf-8', 'Accept-Charset': 'utf-8', 'Accept-encoding': 'gzip', 'User-Agent': f"{utils.addon_name}/{utils.addon_version}", 'Connection': 'keep-alive', 'Authorization': f'Emby Client="{utils.addon_name}", Device="{utils.device_name}", DeviceId="{self.EmbyServer.ServerData["DeviceId"]}", Version="{utils.addon_version}"'} + + if ConnectionId == "DOWNLOAD": + self.Connection[ConnectionId]["RequestHeader"]['Accept-encoding'] = "identity" + + try: + self.Connection[ConnectionId]["AddressFamily"] = _socket.getaddrinfo(self.Connection[ConnectionId]['Hostname'], None)[0][0] + except Exception as error: + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Wrong Hostname: {error}", 2) # LOGWARNING + + if ConnectionId == "MAIN": + utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message="Invalid server address", time=utils.displayMessage, sound=False) + + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + return 609 + + TimeoutCounter = 0 + + while True: + try: + self.Connection[ConnectionId]["Socket"] = _socket.socket(self.Connection[ConnectionId]["AddressFamily"], _socket.SOCK_STREAM) + self.Connection[ConnectionId]["Socket"].setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1) + self.Connection[ConnectionId]["Socket"].settimeout(1) # set timeout + self.Connection[ConnectionId]["Socket"].connect((self.Connection[ConnectionId]['Hostname'], self.Connection[ConnectionId]['Port'])) break + except TimeoutError: + TimeoutCounter += 1 - self.wait_for_priority_request() + if TimeoutCounter < 10: + continue - if utils.getFreeSpace(Command[1]["Path"]) < (2097152 + Command[1]["FileSize"] / 1024): # check if free space below 2GB - utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33429), icon=utils.icon, time=utils.displayMessage, sound=True) - xbmc.log("EMBY.emby.http: THREAD: ---<[ async file download ] terminated by filesize", 2) # LOGWARNING - return + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Timeout", 2) # LOGWARNING + + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + return 606 + except ConnectionRefusedError: + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + xbmc.log(f"EMBY.emby.http: [ ServerUnreachable ] {ConnectionId}", 2) # LOGWARNING + xbmc.log(f"EMBY.emby.http: [ ServerUnreachable ] {ConnectionString}", 0) # LOGDEBUG + return 607 + except Exception as error: + if str(error) == "timed out": # workaround when TimeoutError not raised + TimeoutCounter += 1 + + if TimeoutCounter < 10: + continue + + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Timeout", 2) # LOGWARNING + + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + return 606 + + if str(error).lower().find("errno 22") != -1 or str(error).lower().find("invalid argument") != -1: # [Errno 22] Invalid argument + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Invalid argument", 2) # LOGWARNING + + if ConnectionId == "MAIN": + utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message="Invalid argument", time=utils.displayMessage, sound=False) + + return 610 + + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Undefined error: {error}", 2) # LOGWARNING + xbmc.log(f"EMBY.emby.http: Socket open {ConnectionId}: Undefined error type {type(error)}", 2) # LOGWARNING - ProgressBar = xbmcgui.DialogProgressBG() - ProgressBar.create("Download", Command[1]["Name"]) - ProgressBarTotal = Command[1]["FileSize"] / 100 - ProgressBarCounter = 0 - Terminate = False + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] + return 699 + + if self.Connection[ConnectionId]["SSL"]: try: - if urllib3v1: - r = self.session.request("GET", Command[0]['url'], body=json.dumps(Command[1].get("params", {})).encode('utf-8'), preload_content=False) - else: - r = self.session.request("GET", Command[0]['url'], json=Command[1].get("params", {}), preload_content=False) + self.Connection[ConnectionId]["Socket"] = self.SSLContext.wrap_socket(self.Connection[ConnectionId]["Socket"], do_handshake_on_connect=True, suppress_ragged_eofs=True, server_hostname=self.Connection[ConnectionId]["Hostname"]) + except ssl.CertificateError: + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] - with open(Command[1]["FilePath"], 'wb') as outfile: - for chunk in r.stream(4194304): # 4 MB chunks - outfile.write(chunk) - ProgressBarCounter += 4194304 + xbmc.log("EMBY.emby.http: socket_open ssl certificate error", 3) # LOGERROR - if ProgressBarCounter > Command[1]["FileSize"]: - ProgressBarCounter = Command[1]["FileSize"] + if ConnectionId == "MAIN": + utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33428), time=utils.displayMessage) - ProgressBar.update(int(ProgressBarCounter / ProgressBarTotal), "Download", Command[1]["Name"]) + return 608 + except Exception as error: + if ConnectionId in self.Connection: + del self.Connection[ConnectionId] - if utils.SystemShutdown: - r.close() - Terminate = True - break + xbmc.log(f"EMBY.emby.http: socket_open ssl undefined error: {error}", 2) # LOGWARNING + return 699 + + xbmc.log(f"EMBY.emby.http: Socket {ConnectionId} opened", 0) # LOGDEBUG + return 0 + + def socket_close(self, ConnectionId): + if ConnectionId in self.Connection: + try: + self.Connection[ConnectionId]["Socket"].close() + except Exception as error: + xbmc.log(f"EMBY.emby.http: Socket {ConnectionId} close error: {error}", 2) # LOGWARNING + + try: + del self.Connection[ConnectionId] + except Exception as error: + xbmc.log(f"EMBY.emby.http: Socket {ConnectionId} reset error: {error}", 2) # LOGWARNING + + if ConnectionId != "MAIN" and ConnectionId in self.SocketBusy: + del self.SocketBusy[ConnectionId] + else: + xbmc.log(f"EMBY.emby.http: Socket {ConnectionId} already closed", 0) # LOGDEBUG + return + + xbmc.log(f"EMBY.emby.http: Socket {ConnectionId} closed", 0) # LOGDEBUG - r.release_conn() + def socket_io(self, Request, ConnectionId, Timeout): + IncomingData = b"" + StatusCode = 0 + TimeoutCounter = 0 + BytesSend = 0 + BytesSendTotal = len(Request) - if Terminate: - utils.delFile(Command[1]["FilePath"]) + while True: + try: + self.Connection[ConnectionId]["Socket"].settimeout(1) # set timeout + + if Request: + while BytesSend < BytesSendTotal: + BytesSend += self.Connection[ConnectionId]["Socket"].send(Request[BytesSend:]) else: - if "KodiId" in Command[1]: - SQLs = dbio.DBOpenRW(self.EmbyServer.ServerData['ServerId'], "download_item", {}) - SQLs['emby'].add_DownloadItem(Command[1]["Id"], Command[1]["KodiPathIdBeforeDownload"], Command[1]["KodiFileId"], Command[1]["KodiId"], Command[1]["KodiType"]) - dbio.DBCloseRW(self.EmbyServer.ServerData['ServerId'], "download_item", {}) - SQLs = dbio.DBOpenRW("video", "download_item_replace", {}) - Artworks = () - ArtworksData = SQLs['video'].get_artworks(Command[1]["KodiId"], Command[1]["KodiType"]) - - for ArtworkData in ArtworksData: - if ArtworkData[3] in ("poster", "thumb", "landscape"): - UrlMod = ArtworkData[4].split("|") - UrlMod = f"{UrlMod[0].replace('-download', '')}-download|redirect-limit=1000" - SQLs['video'].update_artwork(ArtworkData[0], UrlMod) - Artworks += ((UrlMod,),) - - SQLs['video'].update_Name(Command[1]["KodiId"], Command[1]["KodiType"], True) - SQLs['video'].replace_Path_ContentItem(Command[1]["KodiId"], Command[1]["KodiType"], Command[1]["Path"]) - - if Command[1]["KodiType"] == "episode": - KodiPathId = SQLs['video'].get_add_path(Command[1]["Path"], None, Command[1]["ParentPath"]) - Artworks = SQLs['video'].set_Subcontent_download_tags(Command[1]["KodiId"], True) - - if Artworks: - artworkcache.CacheAllEntries(Artworks, None) - elif Command[1]["KodiType"] == "movie": - KodiPathId = SQLs['video'].get_add_path(Command[1]["Path"], "movie", None) - elif Command[1]["KodiType"] == "musicvideo": - KodiPathId = SQLs['video'].get_add_path(Command[1]["Path"], "musicvideos", None) - else: - KodiPathId = None - xbmc.log(f"EMBY.emby.http: Download invalid: KodiPathId: {Command[1]['Path']} / {Command[1]['KodiType']}", 2) # LOGWARNING - - if KodiPathId: - SQLs['video'].replace_PathId(Command[1]["KodiFileId"], KodiPathId) - - dbio.DBCloseRW("video", "download_item_replace", {}) - artworkcache.CacheAllEntries(Artworks, ProgressBar) + IncomingData = self.Connection[ConnectionId]["Socket"].recv(1048576) - ProgressBar.close() - del ProgressBar + if not IncomingData: # No Data received -> Socket closed by Emby server + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Empty data", 0) # LOGDEBUG + StatusCode = 600 + + break + except TimeoutError: + if not Timeout or (ConnectionId != "MAIN" and self.SocketBusy["MAIN"].locked()): # Websocket or binary -> wait longer for e.g. images. MAIN queries could block IO + continue + + TimeoutCounter += 1 - if self.FileDownloadQueue.isEmpty(): - utils.refresh_widgets(True) + if TimeoutCounter < Timeout: + continue + + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Timeout", 2) # LOGWARNING + StatusCode = 603 + break + except BrokenPipeError: + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Pipe error", 2) # LOGWARNING + StatusCode = 605 + break except Exception as error: - xbmc.log(f"EMBY.emby.http: Download Emby server did not respond: error: {error}", 2) # LOGWARNING + if str(error) == "timed out": # workaround when TimeoutError not raised + if not Timeout or (ConnectionId != "MAIN" and self.SocketBusy["MAIN"].locked()): # Websocket or binary -> wait longer for e.g. images. MAIN queries could block IO + continue + + TimeoutCounter += 1 + + if TimeoutCounter < Timeout: + continue + + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Timeout (workaround)", 2) # LOGWARNING + StatusCode = 603 + break + + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Undefined error {error}", 3) # LOGERROR + xbmc.log(f"EMBY.emby.http: Socket IO {ConnectionId}: ({bool(Request)}): Undefined error type {type(error)}", 3) # LOGERROR + StatusCode = 699 + break + + return StatusCode, IncomingData + + def socket_request(self, Method, Handler, Params, Binary, TimeoutSend, TimeoutRecv, ConnectionId, DownloadPath, DownloadName): + if ConnectionId not in self.Connection: + return 601, {}, {} + + PayloadTotal = b"" + PayloadTotalPosition = 0 + + # Prepare HTTP Header + HeaderString = "" + + for Key, Values in list(self.Connection[ConnectionId]['RequestHeader'].items()): + HeaderString += f"{Key}: {Values}\r\n" + + # Prepare HTTP Payload + if Method == "GET": + ParamsString = "" + + for Query, Param in list(Params.items()): + if Param not in ([], None): + ParamsString += f"{Query}={Param}&" + + if ParamsString: + ParamsString = f"?{ParamsString[:-1]}" + + StatusCodeSocket, _ = self.socket_io(f"{Method} /{Handler}{ParamsString} HTTP/1.1\r\n{HeaderString}Content-Length: 0\r\n\r\n".encode("utf-8"), ConnectionId, TimeoutSend) + else: + if Params: + ParamsString = json.dumps(Params) + else: + ParamsString = "" + + StatusCodeSocket, _ = self.socket_io(f"{Method} /{Handler} HTTP/1.1\r\n{HeaderString}Content-Length: {len(ParamsString)}\r\n\r\n{ParamsString}".encode("utf-8"), ConnectionId, TimeoutSend) + + if StatusCodeSocket: + return StatusCodeSocket, {}, "" + + IncomingData = b"" + + # Receive Data + while True: + StatusCodeSocket, IncomingData = self.socket_io("", ConnectionId, TimeoutRecv) + + if StatusCodeSocket: + return StatusCodeSocket, {}, "" + + IncomingData = IncomingData.split(b'\x0d\x0a\x0d\x0a', 1) # Receive initial package 1MB + IncomingDataHeadArray = IncomingData[0].decode("utf-8").split("\r\n") + StatusCode = int(IncomingDataHeadArray[0].split(" ")[1]) + + if StatusCode not in (200, 206, 301, 302, 307, 308, 101) or StatusCode == 204: # 200 = OK, 206 = Partial content, 204 = OK but no content, 3XX redirects, 101 wesocket + return StatusCode, {}, "" + + IncomingDataHeaderArray = IncomingDataHeadArray[1:] + IncomingDataHeader = {} + + for IncomingDataHeaderArrayData in IncomingDataHeaderArray: + Temp = IncomingDataHeaderArrayData.split(": ") + IncomingDataHeader[Temp[0].lower()] = Temp[1] + + # Redirects 3XX and websocket 1XX + if StatusCode in (301, 302, 307, 308, 101): + return StatusCode, IncomingDataHeader, "" + + try: + # Decompress flags + isGzip = IncomingDataHeader.get("content-encoding", "") == "gzip" + isDeflate = IncomingDataHeader.get("content-encoding", "") == "deflate" + + # Recv frame + PayloadLenghtTotal = int(IncomingDataHeader["content-length"]) + except Exception as error: # Can happen on Emby server hard reboot + xbmc.log(f"EMBY.emby.http: Header error {ConnectionId}: Undefined error {error}", 3) # LOGERROR + return 612, {}, "" + + # Recv frame + PayloadTotalPosition += PayloadLenghtTotal + Payload = IncomingData[1] + del IncomingData + PayloadLenght = len(Payload) + + if DownloadPath: + ProgressBar = xbmcgui.DialogProgressBG() + ProgressBar.create("Download", DownloadName) + ProgressBarTotal = PayloadLenghtTotal / 100 + OutFile = open(DownloadPath, 'wb') + + while PayloadLenght < PayloadLenghtTotal: + self.wait_for_main(ConnectionId) + StatusCodeSocket, PayloadRecv = self.socket_io("", ConnectionId, TimeoutRecv) + + if StatusCodeSocket: + if DownloadPath: + OutFile.close() + ProgressBar.close() + del ProgressBar + + return StatusCodeSocket, {}, "" + + Payload += PayloadRecv + PayloadLenght += len(PayloadRecv) + del PayloadRecv + + if DownloadPath: + OutFile.write(Payload) + Payload = b"" + ProgressBar.update(int(PayloadLenght / ProgressBarTotal), "Download", DownloadName) + + if DownloadPath: + OutFile.close() ProgressBar.close() del ProgressBar - xbmc.log("EMBY.emby.http: THREAD: ---<[ async file download ]", 0) # LOGDEBUG + PayloadTotal += Payload + del Payload - def async_commands(self): - xbmc.log("EMBY.emby.http: THREAD: --->[ async queue ]", 0) # LOGDEBUG - PingTimeoutCounter = 0 + # request additional data + if StatusCode == 206: # partial content + xbmc.log(f"EMBY.emby.http: Partial content {ConnectionId} closed", 1) # LOGINFO + + if Method == "GET": + StatusCodeSocket, _ = self.socket_io(f"{Method} /{Handler}{ParamsString} HTTP/1.1\r\n{HeaderString}Range: bytes={PayloadTotalPosition}-\r\nContent-Length: 0\r\n\r\n".encode("utf-8"), ConnectionId, TimeoutSend) + else: + StatusCodeSocket, _ = self.socket_io(f"{Method} /{Handler} HTTP/1.1\r\n{HeaderString}Content-Length: {len(ParamsString)}\r\n\r\n{ParamsString}".encode("utf-8"), ConnectionId, TimeoutSend) + + if StatusCodeSocket: + return 601, {}, "" + else: + break + + # Decompress data + if isDeflate: + PayloadTotal = zlib.decompress(PayloadTotal, -zlib.MAX_WBITS) + elif isGzip: + PayloadTotal = zlib.decompress(PayloadTotal, zlib.MAX_WBITS|32) + + if Binary: + if DownloadPath and PayloadTotal: + with open(DownloadPath, 'wb') as OutFile: + OutFile.write(PayloadTotal) + + PayloadTotal = b"" + + return StatusCode, IncomingDataHeader, PayloadTotal + + try: + return StatusCode, IncomingDataHeader, json.loads(PayloadTotal) + except: + xbmc.log(f"EMBY.emby.emby: Invalid content {ConnectionId}: {IncomingDataHeader}", 0) # LOGDEBUG + return 601, {}, "" + + def download_file(self): + xbmc.log("EMBY.emby.http: THREAD: --->[ file download ]", 0) # LOGDEBUG while True: - Command = self.AsyncCommandQueue.get() + Command = self.Queues["DOWNLOAD"].get() # EmbyId, ParentPath, Path, FilePath, FileSize, Name, KodiType, KodiPathIdBeforeDownload, KodiFileId, KodiId - try: - if Command == "QUIT": - xbmc.log("EMBY.emby.http: Queue closed", 1) # LOGINFO - self.AsyncCommandQueue.clear() - break + if Command == "QUIT": + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Download {self.EmbyServer.ServerData['ServerId']} ] shutdown 1", 0) # LOGDEBUG + self.Queues["DOWNLOAD"].clear() + utils.delFile(Command[3]) + self.socket_close("DOWNLOAD") + return - self.wait_for_priority_request() + # check if free space below 2GB + if utils.getFreeSpace(Command[2]) < (2097152 + Command[4] / 1024): + utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33429), icon=utils.icon, time=utils.displayMessage, sound=True) + xbmc.log("EMBY.emby.http: THREAD: ---<[ file download ] terminated by filesize", 2) # LOGWARNING + return - if Command['type'] in ("POST", "DELETE"): - if urllib3v1: - r = self.session.request(Command['type'], Command['url'], body=json.dumps(Command.get("params", {})).encode('utf-8'), timeout=TimeoutAsync) + while True: + if self.socket_open(self.EmbyServer.ServerData['ServerUrl'], "DOWNLOAD"): + if utils.sleep(10): + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Download {self.EmbyServer.ServerData['ServerId']} shutdown ]", 0) # LOGDEBUG + return + + continue + + self.update_header("DOWNLOAD") + StatusCode, _, _ = self.socket_request("GET", f"Items/{Command[0]}/Download", {}, True, 10, 300, "DOWNLOAD", Command[3], Command[5]) + + if StatusCode == 601: # quit + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Download {self.EmbyServer.ServerData['ServerId']} ] shutdown 2", 0) # LOGDEBUG + self.Queues["DOWNLOAD"].clear() + utils.delFile(Command[3]) + self.socket_close("DOWNLOAD") + return + + if StatusCode in (600, 602, 603, 604, 605, 612): + xbmc.log(f"EMBY.emby.http: Download retry {StatusCode}", 2) # LOGWARNING + utils.delFile(Command[3]) + self.socket_close("DOWNLOAD") + continue + + try: + if StatusCode != 200: + utils.delFile(Command[3]) else: - r = self.session.request(Command['type'], Command['url'], json=Command.get("params", {}), timeout=TimeoutAsync) + if Command[9]: # KodiId + SQLs = dbio.DBOpenRW(self.EmbyServer.ServerData['ServerId'], "download_item", {}) + SQLs['emby'].add_DownloadItem(Command[0], Command[7], Command[8], Command[9], Command[6]) + dbio.DBCloseRW(self.EmbyServer.ServerData['ServerId'], "download_item", {}) + SQLs = dbio.DBOpenRW("video", "download_item_replace", {}) + Artworks = () + ArtworksData = SQLs['video'].get_artworks(Command[9], Command[6]) + + for ArtworkData in ArtworksData: + if ArtworkData[3] in ("poster", "thumb", "landscape"): + UrlMod = ArtworkData[4].split("|") + UrlMod = f"{UrlMod[0].replace('-download', '')}-download|redirect-limit=1000" + SQLs['video'].update_artwork(ArtworkData[0], UrlMod) + Artworks += ((UrlMod,),) + + SQLs['video'].update_Name(Command[9], Command[6], True) + SQLs['video'].replace_Path_ContentItem(Command[9], Command[6], Command[2]) + + if Command[6] == "episode": + KodiPathId = SQLs['video'].get_add_path(Command[2], None, Command[1]) + Artworks = SQLs['video'].set_Subcontent_download_tags(Command[9], True) + + if Artworks: + artworkcache.CacheAllEntries(Artworks, None) + elif Command[6] == "movie": + KodiPathId = SQLs['video'].get_add_path(Command[2], "movie", None) + elif Command[6] == "musicvideo": + KodiPathId = SQLs['video'].get_add_path(Command[2], "musicvideos", None) + else: + KodiPathId = None + xbmc.log(f"EMBY.emby.http: Download invalid: KodiPathId: {Command[1]['Path']} / {Command[6]}", 2) # LOGWARNING + + if KodiPathId: + SQLs['video'].replace_PathId(Command[8], KodiPathId) + + dbio.DBCloseRW("video", "download_item_replace", {}) + artworkcache.CacheAllEntries(Artworks, None) + + if self.Queues["DOWNLOAD"].isEmpty(): + utils.refresh_widgets(True) + except Exception as error: + xbmc.log(f"EMBY.emby.http: Download Emby server did not respond: error: {error}", 2) # LOGWARNING + + break - r.close() + xbmc.log("EMBY.emby.http: THREAD: ---<[ file download ]", 0) # LOGDEBUG - if Command['url'].find("System/Ping") != -1: - PingTimeoutCounter = 0 - except Exception as error: - xbmc.log(f"EMBY.emby.http: Async_commands Emby server did not respond: error: {error}", 2) # LOGWARNING + # decide threaded or wait for response + def request(self, Method, Handler, Params, RequestHeader, Binary, ConnectionString, CloseConnection): + xbmc.log(f"EMBY.emby.http: [ http ] Method: {Method} / Handler: {Handler} / Params: {Params} / Binary: {Binary} / ConnectionString: {ConnectionString} / CloseConnection: {CloseConnection} / RequestHeader: {RequestHeader}", 0) # LOGDEBUG - if Command['url'].find("System/Ping") != -1: # ping timeout - if PingTimeoutCounter == 4: - xbmc.log("EMBY.emby.http: Ping re-establish connection", 2) # LOGWARNING - self.EmbyServer.ServerReconnect() - else: - PingTimeoutCounter += 1 - xbmc.log(f"EMBY.emby.http: Ping timeout: {PingTimeoutCounter}", 2) # LOGWARNING + if CloseConnection: + ConnectionId = str(uuid.uuid4()) + else: + ConnectionId = "MAIN" + + if ConnectionId not in self.SocketBusy: + self.SocketBusy[ConnectionId] = allocate_lock() + + with self.SocketBusy[ConnectionId]: + if not ConnectionString: + ConnectionString = self.EmbyServer.ServerData['ServerUrl'] + + # Connectionstring changed + if ConnectionId in self.Connection and (not CloseConnection or ConnectionString.find(self.Connection[ConnectionId]['Hostname']) == -1): + self.socket_close(ConnectionId) + + while True: + StatusCode = 0 + + # Shutdown + if utils.SystemShutdown: + self.socket_close(ConnectionId) + return noData(StatusCode, {}, Binary) + + # open socket + if ConnectionId not in self.Connection: + StatusCode = self.socket_open(ConnectionString, ConnectionId) + + if StatusCode: + if StatusCode not in (608, 609, 610, 611): # wrong Emby server address or SSL issue + self.EmbyServer.ServerReconnect() + + return noData(StatusCode, {}, Binary) + + # Update Header information + if RequestHeader: + self.Connection[ConnectionId]["RequestHeader"] = {"Host": f"{self.Connection[ConnectionId]['Hostname']}:{self.Connection[ConnectionId]['Port']}", 'Accept': "application/json", 'Accept-Charset': "utf-8", 'X-Application': f"{utils.addon_name}/{utils.addon_version}", 'Content-type': 'application/json'} + self.Connection[ConnectionId]["RequestHeader"].update(RequestHeader) + else: + self.update_header(ConnectionId) + + StatusCode, Header, Payload = self.socket_request(Method, Handler, Params, Binary, 10, 300, ConnectionId, "", "") + + # Redirects + if StatusCode in (301, 302, 307, 308): + self.socket_close(ConnectionId) + Location = Header.get("location", "") + Scheme, Hostname, Port = utils.get_url_info(Location) + ConnectionString = f"{Scheme}://{Hostname}:{Port}" + ConnectionStringNoPort = f"{Scheme}://{Hostname}" + Handler = Location.replace(ConnectionString, "").replace(ConnectionStringNoPort, "") + + if Handler.startswith("/"): + Handler = Handler[1:] + + if ConnectionId == "MAIN" and StatusCode in (301, 308): + self.EmbyServer.ServerData['ServerUrl'] = ConnectionString + + continue + + if CloseConnection: + self.socket_close(ConnectionId) + + if StatusCode == 200: # OK + return StatusCode, Header, Payload + + if StatusCode == 204: # OK, no data + return noData(StatusCode, Header, Binary) + + if StatusCode == 401: # Unauthorized + xbmc.log(f"EMBY.emby.http: Request unauthorized {StatusCode} / {ConnectionId}", 3) # LOGERROR + utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33147), time=utils.displayMessage) + return noData(StatusCode, Header, Binary) + + if StatusCode in (600, 604, 605, 612): # not data received, broken pipes, header issue + xbmc.log(f"EMBY.emby.http: Request retry {StatusCode} / {ConnectionId}", 2) # LOGWARNING + self.socket_close(ConnectionId) + continue + + if StatusCode in (602, 603): # timeouts + xbmc.log(f"EMBY.emby.http: Request timeout {StatusCode} / {ConnectionId}", 2) # LOGWARNING + self.socket_close(ConnectionId) + return noData(StatusCode, Header, Binary) - xbmc.log("EMBY.emby.http: THREAD: ---<[ async queue ]", 0) # LOGDEBUG + if StatusCode == 601: # quit + return noData(StatusCode, Header, Binary) - def wait_for_priority_request(self): - LOGDone = False + xbmc.log(f"EMBY.emby.http: [ Statuscode ] {StatusCode}", 3) # LOGERROR + xbmc.log(f"EMBY.emby.http: [ Statuscode ] {Payload}", 0) # LOGDEBUG + return noData(StatusCode, Header, Binary) - while self.Priority: - if not LOGDone: - LOGDone = True - xbmc.log("EMBY.emby.http: Delay queries, priority request in progress", 1) # LOGINFO + def websocket_listen(self): + xbmc.log(f"EMBY.emby.emby: THREAD: --->[ Websocket {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG - if utils.sleep(0.1): + while self.Running: + xbmc.log("EMBY.emby.emby: Websocket connecting", 1) # LOGINFO + self.inProgressWebSocket = False + + if self.socket_open(self.EmbyServer.ServerData['ServerUrl'], "WEBSOCKET"): + if utils.sleep(10): + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Download {self.EmbyServer.ServerData['ServerId']} shutdown ]", 0) # LOGDEBUG + return + + continue + + uid = uuid.uuid4() + EncodingKey = base64.b64encode(uid.bytes).strip().decode('utf-8') + self.Connection["WEBSOCKET"]["RequestHeader"].update({"Upgrade": "websocket", "Connection": "Upgrade", "Sec-WebSocket-Key": EncodingKey, "Sec-WebSocket-Version": "13"}) + StatusCode, Header, _ = self.socket_request("GET", f"embywebsocket?api_key={self.EmbyServer.ServerData['AccessToken']}&deviceId={self.EmbyServer.ServerData['DeviceId']}", {}, True, 10, 30, "WEBSOCKET", "", "") + + if StatusCode == 601: # quit + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Websocket {self.EmbyServer.ServerData['ServerId']} quit ]", 0) # LOGDEBUG return - if LOGDone: - xbmc.log("EMBY.emby.http: Delay queries, continue", 1) # LOGINFO + if StatusCode != 101: + self.inProgressWebSocket = False + self.socket_close("WEBSOCKET") - def stop_session(self): - if not self.session: - xbmc.log("EMBY.emby.http: Session close: No session found", 0) # LOGDEBUG - return + if utils.sleep(1): + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Websocket {self.EmbyServer.ServerData['ServerId']} shutdown ]", 0) # LOGDEBUG + return - try: - self.session.clear() - except Exception as error: - xbmc.log(f"EMBY.emby.http: Session close error: {error}", 2) # LOGWARNING + result = Header.get("sec-websocket-accept", "") - self.session = None - self.AsyncCommandQueue.put("QUIT") - self.FileDownloadQueue.put("QUIT") - xbmc.log("EMBY.emby.http: Session close", 1) # LOGINFO + if not result: + utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message=utils.Translate(33235), sound=True, time=utils.newContentTime) + return - # decide threaded or wait for response - def request(self, data, ForceReceiveData, Binary, GetHeaders=False, LastWill=False, Priority=False, Download=None): - ServerUnreachable = False + value = f"{EncodingKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode("utf-8") + hashed = base64.b64encode(hashlib.sha1(value).digest()).strip().lower().decode('utf-8') - if Priority: - self.Priority = True + if hashed != result.lower(): + return - if 'url' not in data: - data['url'] = f"{self.EmbyServer.ServerData['ServerUrl']}/emby/{data.pop('handler', '')}" + self.inProgressWebSocket = True + self.websocket_send('{"MessageType": "ScheduledTasksInfoStart", "Data": "0,1500"}', 0x1) # subscribe notifications - if 'headers' not in data: - Header = {'Content-type': "application/json", 'Accept-Charset': "UTF-8,*", 'Accept-encoding': "gzip", 'User-Agent': f"{utils.addon_name}/{utils.addon_version}"} - else: - Header = data['headers'] - del data['headers'] + if "WEBSOCKET" not in self.Connection: + continue - if 'Authorization' not in Header: - auth = f"Emby Client={utils.addon_name},Device={utils.device_name},DeviceId={self.EmbyServer.ServerData['DeviceId']},Version={utils.addon_version}" + self.Connection["WEBSOCKET"]["Socket"].settimeout(1) + self.WebsocketBuffer = b"" - if self.EmbyServer.ServerData['AccessToken'] and self.EmbyServer.ServerData['UserId']: - Header.update({'Authorization': f"{auth},UserId={self.EmbyServer.ServerData['UserId']}", 'X-Emby-Token': self.EmbyServer.ServerData['AccessToken']}) - else: - Header.update({'Authorization': auth}) + while self.Running: + ConnectionClosed = False + StatusCodeSocket, PayloadRecv = self.socket_io("", "WEBSOCKET", 0) - if not ForceReceiveData and (Priority or data['type'] in ("POST", "DELETE")): - Timeout = TimeoutPriority - RepeatSend = 20 - else: - Timeout = TimeoutRegular - RepeatSend = 2 + if StatusCodeSocket: + xbmc.log(f"EMBY.emby.emby: Websocket receive interupted {StatusCodeSocket}", 1) # LOGINFO + break - xbmc.log(f"EMBY.emby.http: [ http ] {data}", 0) # LOGDEBUG - for Index in range(RepeatSend): # timeout 10 seconds - if not Priority: - self.wait_for_priority_request() + self.WebsocketBuffer += PayloadRecv - if Index > 0: - xbmc.log(f"EMBY.emby.http: Request no send, retry: {Index}", 2) # LOGWARNING + while True: + if len(self.WebsocketBuffer) < 2: + break - # Shutdown - if utils.SystemShutdown and not LastWill: - self.stop_session() - return self.noData(Binary, GetHeaders) + FrameHeader = self.WebsocketBuffer[:2] + Curser = 2 + fin = FrameHeader[0] >> 7 & 1 - # start session - if not self.session: - self.HeaderCache = {} + if not fin: + xbmc.log("EMBY.emby.emby: Websocket not fin", 0) # LOGDEBUG + break - if utils.sslverify: - self.session = urllib3.PoolManager(10, None, socket_options=[(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1)]) - else: - self.session = urllib3.PoolManager(10, None, cert_reqs='CERT_NONE', assert_hostname=False, socket_options=[(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1)]) + opcode = FrameHeader[0] & 0xf + has_mask = FrameHeader[1] >> 7 & 1 - start_new_thread(self.async_commands, ()) - start_new_thread(self.download_file, ()) + # Frame length + FrameLength = FrameHeader[1] & 0x7f - # Update session headers - if Header != self.HeaderCache: - self.HeaderCache = Header.copy() - self.session.headers = Header + if FrameLength == 0x7e: + length_data = self.WebsocketBuffer[Curser:Curser + 2] + Curser += 2 + FrameLength = struct.unpack("!H", length_data)[0] + elif FrameLength == 0x7f: + length_data = self.WebsocketBuffer[Curser:Curser + 8] + Curser += 8 + FrameLength = struct.unpack("!Q", length_data)[0] - # http request - try: - if data['type'] == "HEAD": - if urllib3v1: - r = self.session.request('HEAD', data['url'], body=json.dumps(data.get("params", {})).encode('utf-8'), timeout=Timeout) - else: - r = self.session.request('HEAD', data['url'], json=data.get("params", {}), timeout=Timeout) + # Mask + if has_mask: + FrameMask = self.WebsocketBuffer[Curser:Curser + 4] + Curser += 4 - r.close() - self.Priority = False - return r.status + # Payload + if FrameLength: + FrameLengthEndPos = Curser + FrameLength - if data['type'] == "GET": - if Download: - self.FileDownloadQueue.put(((data, Download),)) - return None + if len(self.WebsocketBuffer) < FrameLengthEndPos: # Incomplete Frame + xbmc.log("EMBY.emby.emby: Websocket incomplete frame", 0) # LOGDEBUG + break - if urllib3v1: - r = self.session.request('GET', data['url'], body=json.dumps(data.get("params", {})).encode('utf-8'), timeout=Timeout) + payload = self.WebsocketBuffer[Curser:FrameLengthEndPos] + Curser = FrameLengthEndPos + + if has_mask: + payload = maskData(FrameMask, payload) + + if opcode in (0x2, 0x1, 0x0): # 1 textframe, 2 binaryframe, 0 continueframe + if fin: + self.Websocket.MessageQueue.put(payload) + elif opcode == 0x8: # Connection close + xbmc.log("EMBY.emby.emby: Websocket connection closed", 0) # LOGDEBUG + ConnectionClosed = True + elif opcode == 0x9: # Ping + self.websocket_send(payload, 0xa) # Pong + elif opcode == 0xa: # Pong + xbmc.log("EMBY.emby.emby: Websocket Pong received", 0) # LOGDEBUG else: - r = self.session.request('GET', data['url'], json=data.get("params", {}), timeout=Timeout) + xbmc.log(f"EMBY.hooks.websocket: Uncovered Opcode: {opcode} / Payload: {payload} / FrameHeader: {FrameHeader} / FrameLength: {FrameLength} / FrameMask: {FrameMask}", 3) # LOGERROR - r.close() - self.Priority = False + self.WebsocketBuffer = self.WebsocketBuffer[Curser:] + continue - if r.status == 200: - if Binary: - if GetHeaders: - return r.data, r.headers + if ConnectionClosed: + break - return r.data + self.inProgressWebSocket = False + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Websocket {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG - if urllib3v1: - return json.loads(r.data.decode('utf-8')) + def websocket_send(self, payload, opcode): + if opcode == 0x1: + payload = payload.encode("utf-8") - return r.json() + length = len(payload) + frame_header = struct.pack("B", (1 << 7 | 0 << 6 | 0 << 5 | 0 << 4 | opcode)) - if r.status == 401: - utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33147), time=utils.displayMessage) + if length < 0x7d: + frame_header += struct.pack("B", (1 << 7 | length)) + elif length < 1 << 16: # LENGTH_16 + frame_header += struct.pack("B", (1 << 7 | 0x7e)) + frame_header += struct.pack("!H", length) + else: + frame_header += struct.pack("B", (1 << 7 | 0x7f)) + frame_header += struct.pack("!Q", length) - xbmc.log(f"EMBY.emby.http: [ Statuscode ] {r.status}", 3) # LOGERROR - xbmc.log(f"EMBY.emby.http: [ Statuscode ] {data}", 0) # LOGDEBUG - return self.noData(Binary, GetHeaders) + mask_key = os.urandom(4) + data = frame_header + mask_key + maskData(mask_key, payload) + self.socket_io(data, "WEBSOCKET", 300) - if data['type'] == "POST": - if Priority or ForceReceiveData: - if urllib3v1: - r = self.session.request('POST', data['url'], body=json.dumps(data.get("params", {})).encode('utf-8'), timeout=Timeout) - else: - r = self.session.request('POST', data['url'], json=data.get("params", {}), timeout=Timeout) + # Delay low priority tasks + def wait_for_main(self, ConnectionId): + if ConnectionId == "MAIN": + return False - r.close() - self.Priority = False + while self.SocketBusy["MAIN"].locked(): + if utils.sleep(2): + return True - if GetHeaders: - return r.data, r.headers + return False - if urllib3v1: - return json.loads(r.data.decode('utf-8')) + def update_header(self, ConnectionId): + if 'X-Emby-Token' not in self.Connection[ConnectionId]["RequestHeader"] and self.EmbyServer.ServerData['AccessToken'] and self.EmbyServer.ServerData['UserId']: + self.Connection[ConnectionId]["RequestHeader"].update({'Authorization': f'{self.Connection[ConnectionId]["RequestHeader"]["Authorization"]}, Emby UserId="{self.EmbyServer.ServerData["UserId"]}"', 'X-Emby-Token': self.EmbyServer.ServerData['AccessToken']}) - return r.json() + # No return values are expected, usually also lower priority + def async_commands(self): + xbmc.log("EMBY.emby.http: THREAD: --->[ Async ]", 0) # LOGDEBUG + CommandsTotal = () - self.AsyncCommandQueue.put(data) - elif data['type'] == "DELETE": - self.AsyncCommandQueue.put(data) + # Sort tasks: priority tasks first, quit tasks second, others last + while True: + CommandsSorted = () + CommandsPriority = () + CommandsRegular = () - return self.noData(Binary, GetHeaders) - except urllib3.exceptions.SSLError: - xbmc.log("EMBY.emby.http: [ SSL error ]", 3) # LOGERROR - xbmc.log(f"EMBY.emby.http: [ SSL error ] {data}", 0) # LOGDEBUG - utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33428), time=utils.displayMessage) - self.stop_session() - return self.noData(Binary, GetHeaders) - except urllib3.exceptions.ConnectionError: - xbmc.log("EMBY.emby.http: [ ServerUnreachable ]", 3) # LOGERROR - xbmc.log(f"EMBY.emby.http: [ ServerUnreachable ] {data}", 0) # LOGDEBUG - ServerUnreachable = True - continue - except urllib3.exceptions.TimeoutError: - xbmc.log("EMBY.emby.http: [ ServerTimeout ]", 3) # LOGERROR - xbmc.log(f"EMBY.emby.http: [ ServerTimeout ] {data}", 0) # LOGDEBUG + # merge commands + Commands = self.Queues["ASYNC"].getall() # (Method, URL-handler, Parameters, Priority) + CommandsTotal += Commands + + if not self.Queues["ASYNC"].isEmpty(): continue - except Exception as error: - xbmc.log(f"EMBY.emby.http: [ Unknown ] {error}", 3) # LOGERROR - xbmc.log(f"EMBY.emby.http: [ Unknown ] {data} / {error}", 0) # LOGDEBUG - return self.noData(Binary, GetHeaders) - if ServerUnreachable: - self.EmbyServer.ServerReconnect() + # Sort commands + QUIT = False - return self.noData(Binary, GetHeaders) + for CommandTotal in CommandsTotal: + if len(CommandTotal) > 1: + if CommandTotal[3]: + CommandsPriority += (CommandTotal,) + else: + CommandsRegular += (CommandTotal,) + else: + QUIT = True - def load_Trailers(self, EmbyId): - ReceivedIntros = [] - self.Intros = [] + if QUIT: + CommandsSorted = CommandsPriority + ("QUIT",) + else: + CommandsSorted = CommandsPriority + CommandsRegular - if utils.localTrailers: - LocalTrailers = self.EmbyServer.API.get_local_trailers(EmbyId) + CommandsTotal = () - for LocalTrailer in LocalTrailers: - ReceivedIntros.append(LocalTrailer) + # Process commands + for CommandSorted in CommandsSorted: # (Method, URL-handler, Parameters, Priority) + if CommandSorted == "QUIT": + xbmc.log("EMBY.emby.http: Async closed", 1) # LOGINFO + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Async {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG + return - if utils.Trailers: - Intros = self.EmbyServer.API.get_intros(EmbyId) + while True: + if not CommandSorted[3] and self.wait_for_main("ASYNC"): + self.socket_close("ASYNC") + return - if 'Items' in Intros: - for Intro in Intros['Items']: - ReceivedIntros.append(Intro) + if self.socket_open(self.EmbyServer.ServerData['ServerUrl'], "ASYNC"): + self.socket_close("ASYNC") - if ReceivedIntros: - Index = 0 + if utils.sleep(1): + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Async {self.EmbyServer.ServerData['ServerId']} shutdown ]", 0) # LOGDEBUG + return + + continue + + self.update_header("ASYNC") + StatusCode, _, _ = self.socket_request(CommandSorted[0], CommandSorted[1], CommandSorted[2], False, 1, 1, "ASYNC", "", "") + + if StatusCode == 601: # quit + self.Queues["ASYNC"].clear() + self.socket_close("ASYNC") + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Async {self.EmbyServer.ServerData['ServerId']} ] shutdown 2", 0) # LOGDEBUG + return + + if StatusCode in (600, 602, 603, 604, 605, 612): + xbmc.log(f"EMBY.emby.http: Async retry {StatusCode}", 2) # LOGWARNING + self.socket_close("ASYNC") + continue - for Index, Intro in enumerate(ReceivedIntros): - if self.verify_intros(Intro): break - for Intro in ReceivedIntros[Index + 1:]: - start_new_thread(self.verify_intros, (Intro,)) + self.Queues["ASYNC"].clear() + self.socket_close("ASYNC") + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Async {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG + + # Ping server -> keep http session open (timer) + def Ping(self): + xbmc.log(f"EMBY.emby.emby: THREAD: --->[ Ping {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG + + while True: + for Counter in range(20): # ping every 10 seconds + if utils.sleep(0.5) or not self.Running: + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Ping {self.EmbyServer.ServerData['ServerId']} shutdown or quit ]", 0) # LOGDEBUG + return + + if Counter == 9 and self.inProgressWebSocket: + self.websocket_send(b"", 0x9) + + if Counter == 19 and not self.SocketBusy["MAIN"].locked(): + _, _, _ = self.request("POST", "System/Ping", {}, {}, True, "", False) + + xbmc.log(f"EMBY.emby.emby: THREAD: ---<[ Ping {self.EmbyServer.ServerData['ServerId']} ]", 0) # LOGDEBUG + # Intros and Trailers def verify_intros(self, Intro): xbmc.log("EMBY.emby.http: THREAD: --->[ verify intros ]", 0) # LOGDEBUG @@ -394,13 +898,45 @@ def verify_intros(self, Intro): xbmc.log("EMBY.emby.http: THREAD: ---<[ verify intros ] invalid", 0) # LOGDEBUG return False - def noData(self, Binary, GetHeaders): - self.Priority = False + def load_Trailers(self, EmbyId): + ReceivedIntros = [] + self.Intros = [] - if Binary: - if GetHeaders: - return b"", {} + if utils.localTrailers: + LocalTrailers = self.EmbyServer.API.get_local_trailers(EmbyId) + + for LocalTrailer in LocalTrailers: + ReceivedIntros.append(LocalTrailer) + + if utils.Trailers: + Intros = self.EmbyServer.API.get_intros(EmbyId) + + if 'Items' in Intros: + for Intro in Intros['Items']: + ReceivedIntros.append(Intro) + + if ReceivedIntros: + Index = 0 + + for Index, Intro in enumerate(ReceivedIntros): + if self.verify_intros(Intro): + break + + for Intro in ReceivedIntros[Index + 1:]: + start_new_thread(self.verify_intros, (Intro,)) + +# Return empty data +def noData(StatusCode, Header, Binary): + if Binary: + return StatusCode, Header, b"" + + return StatusCode, Header, {} + +def maskData(mask_key, data): + _m = array.array("B", mask_key) + _d = array.array("B", data) - return b"" + for i in range(len(_d)): + _d[i] ^= _m[i % 4] # i xor - return {} + return _d.tobytes() diff --git a/emby/views.py b/emby/views.py index 5d6748253..2d2b15321 100644 --- a/emby/views.py +++ b/emby/views.py @@ -6,14 +6,14 @@ # filename, label, icon, content, [(rule1, Filter, Operator)], [direction, order], useLimit, group, Subfolder SyncNodes = { 'tvshows': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "tvshows", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "tvshows", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultTVShows.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('recentlyadded', utils.Translate(30170), 'DefaultRecentlyAddedEpisodes.png', "tvshows", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('recentlyaddedepisodes', utils.Translate(30175), 'DefaultRecentlyAddedEpisodes.png', "episodes", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('inprogress', utils.Translate(30171), 'DefaultInProgressShows.png', "tvshows", (("tag", "is", "LIBRARYTAG"), ("inprogress", "true")), ("descending", "lastplayed"), False, False), ('inprogressepisodes', utils.Translate(30178), 'DefaultInProgressShows.png', "episodes", (("tag", "is", "LIBRARYTAG"), ("inprogress", "true")), ("descending", "lastplayed"), False, False), ('genres', utils.Translate(33248), 'DefaultGenre.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), - ('random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), ('recommended', utils.Translate(30230), 'DefaultFavourites.png', "tvshows", (("tag", "is", "LIBRARYTAG"), ("inprogress", "false"), ("playcount", "is", "0"), ("rating", "greaterthan", "7")), ("descending", "rating"), True, None), ('years', utils.Translate(33218), 'DefaultYear.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('actors', utils.Translate(33219), 'DefaultActor.png', "tvshows", (("tag", "is", "LIBRARYTAG"),), ("ascending", "title"), False, "actors"), @@ -29,7 +29,7 @@ ('nextepisodesplayed', utils.Translate(33667), 'DefaultInProgressShows.png', "tvshows", (("PLUGIN", "nextepisodesplayed", "episode"),)), ], 'movies': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "movies", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith"),), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "movies", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith"),), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultMovies.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('recentlyadded', utils.Translate(30174), 'DefaultRecentlyAddedMovies.png', "movies", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('recentlyreleased', utils.Translate(33619), 'DefaultRecentlyAddedMovies.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("descending", "year"), True, False), @@ -38,7 +38,7 @@ ('recentlyreleasedunwatched', utils.Translate(33618), 'OverlayUnwatched.png', "movies", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "year"), True, False), ('sets', utils.Translate(30185), 'DefaultSets.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "sets"), ('genres', utils.Translate(33248), 'DefaultGenre.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), - ('random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), ('recommended', utils.Translate(30230), 'DefaultFavourites.png', "movies", (("tag", "is", "LIBRARYTAG"), ("inprogress", "false"), ("playcount", "is", "0"), ("rating", "greaterthan", "7")), ("descending", "rating"), True, False), ('years', utils.Translate(33218), 'DefaultYear.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('actors', utils.Translate(33219), 'DefaultActor.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "title"), False, "actors"), @@ -54,13 +54,13 @@ ('resolution4k', utils.Translate(33361), 'DefaultIconInfo.png', "movies", (("tag", "is", "LIBRARYTAG"), ("videoresolution", "greaterthan", "1080")), ("ascending", "sorttitle"), False, False) ], 'musicvideos': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultMusicVideos.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("ascending", "artist"), False, False), ('recentlyadded', utils.Translate(30256), 'DefaultRecentlyAddedMusicVideos.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('years', utils.Translate(33218), 'DefaultMusicYears.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('genres', utils.Translate(33248), 'DefaultGenre.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), ('inprogress', utils.Translate(30257), 'DefaultInProgressShows.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("inprogress", "true")), ("descending", "lastplayed"), False, False), - ('random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), ('unwatched', utils.Translate(30258), 'OverlayUnwatched.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("inprogress", "false"), ("playcount", "is", "0")), ("random",), True, False), ('artists', utils.Translate(33343), 'DefaultMusicArtists.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("ascending", "artists"), False, "artists"), ('tags', utils.Translate(33220), 'DefaultTags.png', "musicvideos", (("tag", "is", "LIBRARYTAG"),), ("ascending", "title"), False, "tags"), @@ -72,13 +72,13 @@ ('resolution4k', utils.Translate(33361), 'DefaultIconInfo.png', "musicvideos", (("tag", "is", "LIBRARYTAG"), ("videoresolution", "greaterthan", "1080")), ("ascending", "sorttitle"), False, False) ], 'homevideos': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "movies", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "movies", (("tag", "is", "LIBRARYTAG"), ("sorttitle", "startswith")), ("ascending", "sorttitle"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultMusicVideos.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('recentlyadded', utils.Translate(30256), 'DefaultRecentlyAddedMusicVideos.png', "movies", (("tag", "is", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('years', utils.Translate(33218), 'DefaultMusicYears.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('genres', utils.Translate(33248), 'DefaultGenre.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), ('inprogress', utils.Translate(30257), 'DefaultInProgressShows.png', "movies", (("tag", "is", "LIBRARYTAG"), ("inprogress", "true")), ("descending", "lastplayed"), False, False), - ('random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("random",), True, False), ('unwatched', utils.Translate(30258), 'OverlayUnwatched.png', "movies", (("tag", "is", "LIBRARYTAG"), ("inprogress", "false"), ("playcount", "is", "0")), ("random",), True, False), ('tags', utils.Translate(33220), 'DefaultTags.png', "movies", (("tag", "is", "LIBRARYTAG"),), ("ascending", "title"), False, "tags"), ('collections', utils.Translate(33612), 'DefaultSets.png', "movies", (("PLUGIN", "collections", "movie"),)), @@ -89,7 +89,7 @@ ('resolution4k', utils.Translate(33361), 'DefaultIconInfo.png', "movies", (("tag", "is", "LIBRARYTAG"), ("videoresolution", "greaterthan", "1080")), ("ascending", "sorttitle"), False, False) ], 'music': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultAddonMusic.png', "artists", (("disambiguation", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('years', utils.Translate(33218), 'DefaultMusicYears.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('genres', utils.Translate(33248), 'DefaultMusicGenres.png', "artists", (("artists", "disambiguation", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), @@ -100,11 +100,11 @@ ('recentlyaddedalbums', utils.Translate(33388), 'DefaultMusicRecentlyAdded.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "dateadded"), True, False), ('recentlyadded', utils.Translate(33390), 'DefaultMusicRecentlyAdded.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('recentlyplayedmusic', utils.Translate(33350), 'DefaultMusicRecentlyPlayed.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "greaterthan", "0")), ("descending", "lastplayed"), True, False), - ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), - ('random', utils.Translate(33392), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False), + ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(33392), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False), ], 'audiobooks': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultAddonMusic.png', "artists", (("disambiguation", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('years', utils.Translate(33218), 'DefaultMusicYears.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('genres', utils.Translate(33248), 'DefaultMusicGenres.png', "artists", (("artists", "disambiguation", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), @@ -113,11 +113,11 @@ ('recentlyaddedalbums', utils.Translate(33388), 'DefaultMusicRecentlyAdded.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "dateadded"), True, False), ('recentlyadded', utils.Translate(33389), 'DefaultMusicRecentlyAdded.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('recentlyplayedmusic', utils.Translate(33350), 'DefaultMusicRecentlyPlayed.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "greaterthan", "0")), ("descending", "lastplayed"), True, False), - ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), - ('random', utils.Translate(33393), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False) + ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(33393), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False) ], 'podcasts': [ - ('letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), + ('letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "artists", (("disambiguation", "is", "LIBRARYTAG"), ("artist", "startswith")), ("ascending", "artist"), False, False, ("letter", "LETTER")), ('all', "LIBRARYNAME", 'DefaultAddonMusic.png', "artists", (("disambiguation", "is", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, False), ('years', utils.Translate(33218), 'DefaultMusicYears.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "year"), True, "years"), ('genres', utils.Translate(33248), 'DefaultMusicGenres.png', "artists", (("artists", "disambiguation", "LIBRARYTAG"),), ("ascending", "sorttitle"), False, "genres"), @@ -126,8 +126,8 @@ ('recentlyaddedalbums', utils.Translate(33388), 'DefaultMusicRecentlyAdded.png', "albums", (("type", "is", "LIBRARYTAG"),), ("descending", "dateadded"), True, False), ('recentlyadded', utils.Translate(33395), 'DefaultMusicRecentlyAdded.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "is", "0")), ("descending", "dateadded"), True, False), ('recentlyplayedmusic', utils.Translate(33350), 'DefaultMusicRecentlyPlayed.png', "songs", (("comment", "endswith", "LIBRARYTAG"), ("playcount", "greaterthan", "0")), ("descending", "lastplayed"), True, False), - ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), - ('random', utils.Translate(33394), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False) + ('randomalbums', utils.Translate(33391), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "albums", (("type", "is", "LIBRARYTAG"),), ("random",), True, False), + ('random', utils.Translate(33394), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "songs", (("comment", "endswith", "LIBRARYTAG"),), ("random",), True, False) ], 'playlists': [], 'root': [ @@ -150,7 +150,7 @@ } DynamicNodes = { 'tvshows': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Series"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Series"), ('Series', utils.Translate(33349), 'DefaultTVShows.png', "Series"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), ('Recentlyadded', utils.Translate(30170), 'DefaultRecentlyAddedEpisodes.png', "Series"), @@ -167,14 +167,14 @@ ('Upcoming', utils.Translate(33348), 'DefaultSets.png', "Episode"), ('NextUp', utils.Translate(30179), 'DefaultSets.png', "Episode"), ('Resume', utils.Translate(33355), 'DefaultInProgressShows.png', "Episode"), - ('Random', utils.Translate(33339), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Series"), - ('Random', utils.Translate(33338), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Episode") + ('Random', utils.Translate(33339), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Series"), + ('Random', utils.Translate(33338), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Episode") ], 'mixedvideo': [ - ('Letter', utils.Translate(33621), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Movie"), - ('Letter', utils.Translate(33622), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Series"), - ('Letter', utils.Translate(33617), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Video"), - ('Letter', utils.Translate(33620), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "VideoMusicArtist"), + ('Letter', utils.Translate(33621), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Movie"), + ('Letter', utils.Translate(33622), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Series"), + ('Letter', utils.Translate(33617), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Video"), + ('Letter', utils.Translate(33620), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "VideoMusicArtist"), ('Movie', utils.Translate(30302), 'DefaultMovies.png', "Movie"), ('Video', utils.Translate(33367), 'DefaultAddonVideo.png', "Video"), ('Series', utils.Translate(33349), 'DefaultTVShows.png', "Series"), @@ -193,13 +193,13 @@ ('Genre', utils.Translate(135), 'DefaultGenre.png', "Series"), ('Genre', utils.Translate(135), 'DefaultGenre.png', "Movie"), ('MusicGenre', utils.Translate(135), 'DefaultGenre.png', "MusicVideo"), - ('Random', utils.Translate(33339), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Series"), - ('Random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Movie"), - ('Random', utils.Translate(33338), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Episode"), - ('Random', utils.Translate(33365), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "MusicVideo") + ('Random', utils.Translate(33339), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Series"), + ('Random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Movie"), + ('Random', utils.Translate(33338), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Episode"), + ('Random', utils.Translate(33365), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "MusicVideo") ], 'movies': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Movie"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Movie"), ('Movie', utils.Translate(30302), 'DefaultMovies.png', "Movie"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), ('Recentlyadded', utils.Translate(30174), 'DefaultRecentlyAddedMovies.png', "Movie"), @@ -211,13 +211,13 @@ ('Favorite', utils.Translate(33558), 'DefaultFavourites.png', "movies"), ('Favorite', utils.Translate(33614), 'DefaultFavourites.png', "Movie"), ('Genre', utils.Translate(135), 'DefaultGenre.png', "Movie"), - ('Random', utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Movie") + ('Random', utils.Translate(30229), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Movie") ], 'channels': [ ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder") ], 'boxsets': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "BoxSet"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "BoxSet"), ('BoxSet', utils.Translate(30185), 'DefaultSets.png', "BoxSet"), ('Favorite', utils.Translate(33615), 'DefaultFavourites.png', "BoxSet"), ], @@ -225,7 +225,7 @@ ('TvChannel', utils.Translate(33593), 'DefaultAddonPVRClient.png', 'TvChannel') ], 'musicvideos': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "VideoMusicArtist"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "VideoMusicArtist"), ('MusicVideo', utils.Translate(33363), 'DefaultMusicVideos.png', "MusicVideo"), ('VideoMusicArtist', utils.Translate(33343), 'DefaultMusicArtists.png', "VideoMusicArtist"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), @@ -234,14 +234,14 @@ ('Unwatched', utils.Translate(30258), 'OverlayUnwatched.png', "MusicVideo"), ('Tag', utils.Translate(33364), 'DefaultTags.png', "musicvideos"), ('BoxSet', utils.Translate(30185), 'DefaultSets.png', "musicvideos"), - ('Random', utils.Translate(33365), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "MusicVideo"), + ('Random', utils.Translate(33365), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "MusicVideo"), ('Favorite', utils.Translate(33610), 'DefaultFavourites.png', "MusicVideo"), ('Favorite', utils.Translate(33168), 'DefaultFavourites.png', "musicvideos"), ('MusicGenre', utils.Translate(135), 'DefaultGenre.png', "MusicVideo") ], 'homevideos': [ - ('Letter', utils.Translate(33616), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "PhotoAlbum"), - ('Letter', utils.Translate(33617), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Video"), + ('Letter', utils.Translate(33616), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "PhotoAlbum"), + ('Letter', utils.Translate(33617), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Video"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), ('Video', utils.Translate(33367), 'DefaultAddonVideo.png', "Video"), ('Photo', utils.Translate(33368), 'DefaultPicture.png', "Photo"), @@ -257,38 +257,38 @@ ('Recentlyadded', utils.Translate(33375), 'DefaultRecentlyAddedMovies.png', "Video") ], 'playlists': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "Playlist"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "Playlist"), ('Playlists', utils.Translate(33376), 'DefaultPlaylist.png', "Playlist") ], 'audiobooks': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "MusicArtist"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "MusicArtist"), ('MusicArtist', utils.Translate(33343), 'DefaultMusicArtists.png', "MusicArtist"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), ('Audio', utils.Translate(33377), 'DefaultFolder.png', "Audio"), ('Recentlyadded', utils.Translate(33167), 'DefaultRecentlyAddedMovies.png', "Audio"), ('Inprogress', utils.Translate(33169), 'DefaultInProgressShows.png', "Audio"), ('Favorite', utils.Translate(33168), 'DefaultFavourites.png', "Audio"), - ('Random', utils.Translate(33378), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Audio"), - ('Genre', utils.Translate(135), 'DefaultGenre.png', "Audio"), + ('Random', utils.Translate(33378), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Audio"), + ('MusicGenre', utils.Translate(135), 'DefaultGenre.png', "Audio"), ('Unwatched', utils.Translate(33379), 'OverlayUnwatched.png', "Audio") ], 'podcasts': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "MusicArtist"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "MusicArtist"), ('MusicArtist', utils.Translate(33343), 'DefaultMusicArtists.png', "MusicArtist"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), ('Audio', utils.Translate(33382), 'DefaultFolder.png', "Audio"), ('Recentlyadded', utils.Translate(33167), 'DefaultRecentlyAddedMovies.png', "Audio"), ('Inprogress', utils.Translate(33169), 'DefaultInProgressShows.png', "Audio"), ('Favorite', utils.Translate(33168), 'DefaultFavourites.png', "Audio"), - ('Random', utils.Translate(33381), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Audio"), - ('Genre', utils.Translate(135), 'DefaultGenre.png', "Audio"), + ('Random', utils.Translate(33381), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Audio"), + ('MusicGenre', utils.Translate(135), 'DefaultGenre.png', "Audio"), ('Unwatched', utils.Translate(33379), 'OverlayUnwatched.png', "Audio") ], 'music': [ - ('Letter', utils.Translate(33611), 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png', "MusicArtist"), + ('Letter', utils.Translate(33611), 'special://home/addons/plugin.service.emby-next-gen/resources/letter.png', "MusicArtist"), ('MusicArtist', utils.Translate(33343), 'DefaultMusicArtists.png', "MusicArtist"), ('Folder', utils.Translate(33335), 'DefaultFolder.png', "Folder"), - ('Random', utils.Translate(33380), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png', "Audio"), + ('Random', utils.Translate(33380), 'special://home/addons/plugin.service.emby-next-gen/resources/random.png', "Audio"), ('MusicGenre', utils.Translate(135), 'DefaultMusicGenres.png', "Audio"), ('Unwatched', utils.Translate(33379), 'OverlayUnwatched.png', "Audio"), ('Favorite', utils.Translate(33623), 'DefaultFavourites.png', "Audio"), @@ -450,7 +450,7 @@ def add_nodes(self, view, Dynamic): elif view['ContentType'] in ('music', 'audiobooks', 'podcasts'): view['Icon'] = 'DefaultMusicVideos.png' else: - view['Icon'] = "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + view['Icon'] = "special://home/addons/plugin.service.emby-next-gen/resources/icon.png" if view['ContentType'] != "root": if view['ContentType'] in ('music', 'audiobooks', 'podcasts'): @@ -521,7 +521,7 @@ def add_nodes(self, view, Dynamic): Data = '\n' Data += f'\n' Data += f' \n' - Data += f' plugin://plugin.video.emby-next-gen/?mode=browse&id={Letter}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query=Letter\n' + Data += f' plugin://plugin.service.emby-next-gen/?mode=browse&id={Letter}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query=Letter\n' Data += '' utils.writeFileBinary(FilePath, Data.encode("utf-8")) continue @@ -531,7 +531,7 @@ def add_nodes(self, view, Dynamic): if f"{view['ContentType']}_{view['FilteredName']}/" not in self.PictureNodes: self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] = () - self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] += ((node[1], FilePath, f'plugin://plugin.video.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}', node[2]),) + self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] += ((node[1], FilePath, f'plugin://plugin.service.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}', node[2]),) if node[3] == "Photo": continue @@ -539,7 +539,7 @@ def add_nodes(self, view, Dynamic): if f"{view['ContentType']}_{view['FilteredName']}/" not in self.PictureNodes: self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] = () - self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] += ((node[1], FilePath, f'plugin://plugin.video.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}', node[2]),) + self.PictureNodes[f"{view['ContentType']}_{view['FilteredName']}/"] += ((node[1], FilePath, f'plugin://plugin.service.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}', node[2]),) FilePath = f"{folder}{node[0].lower()}_{node[3].lower()}.xml" @@ -548,7 +548,7 @@ def add_nodes(self, view, Dynamic): Data += f'\n' Data += f' \n' Data += f' {node[2]}\n' - Data += f' plugin://plugin.video.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}\n' + Data += f' plugin://plugin.service.emby-next-gen/?mode=browse&id={view["LibraryId"]}&parentid={view["LibraryId"]}&libraryid={view["LibraryId"]}&content={node[3]}&server={view["ServerId"]}&query={node[0]}\n' Data += '' utils.writeFileBinary(FilePath, Data.encode("utf-8")) else: @@ -563,9 +563,9 @@ def add_nodes(self, view, Dynamic): Data += f' {node[2]}\n' if node[0] == "Search": - Data += f' plugin://plugin.video.emby-next-gen/?mode=search&server={self.EmbyServer.ServerData["ServerId"]}\n' + Data += f' plugin://plugin.service.emby-next-gen/?mode=search&server={self.EmbyServer.ServerData["ServerId"]}\n' else: - Data += f' plugin://plugin.video.emby-next-gen/?mode=browse&id=0&parentid=0&libraryid=0&content={node[3]}&server={self.EmbyServer.ServerData["ServerId"]}&query={node[0]}\n' + Data += f' plugin://plugin.service.emby-next-gen/?mode=browse&id=0&parentid=0&libraryid=0&content={node[3]}&server={self.EmbyServer.ServerData["ServerId"]}&query={node[0]}\n' Data += '' utils.writeFileBinary(FilePath, Data.encode("utf-8")) @@ -658,7 +658,7 @@ def set_synced_node(Folder, view, node, NodeIndex, LimitFactor): Data += f'\n' Data += f' \n' Data += f' {node[2]}\n' - Data += f' plugin://plugin.video.emby-next-gen/?mode={node[4][0][1]}&mediatype={node[4][0][2]}&libraryname={quote(view.get("Name", "unknown"))}\n' + Data += f' plugin://plugin.service.emby-next-gen/?mode={node[4][0][1]}&mediatype={node[4][0][2]}&libraryname={quote(view.get("Name", "unknown"))}\n' else: Data += f'\n' Data += f' \n' diff --git a/helper/context.py b/helper/context.py index a19670c32..f39111f22 100644 --- a/helper/context.py +++ b/helper/context.py @@ -119,9 +119,17 @@ def download(): continue Path = utils.PathAddTrailing(f"{utils.DownloadPath}EMBY-offline-content") - utils.mkDir(Path) + + if not utils.mkDir(Path): + utils.Dialog.notification(heading=utils.Translate(33558), message="Download path not found", icon=utils.icon, time=utils.displayMessage) + return + Path = utils.PathAddTrailing(f"{Path}{KodiType}") - utils.mkDir(Path) + + if not utils.mkDir(Path): + utils.Dialog.notification(heading=utils.Translate(33558), message="Download path not found", icon=utils.icon, time=utils.displayMessage) + return + Path = utils.translatePath(Path).decode('utf-8') FilePath = f"{Path}{DownloadItem[4]}" embydb = dbio.DBOpenRO(ServerId, "download_item") @@ -129,7 +137,7 @@ def download(): dbio.DBCloseRO(ServerId, "download_item") if FileSize: - utils.EmbyServers[ServerId].API.download_file({"Id": EmbyId, "ParentPath": DownloadItem[1], "Path": Path, "FilePath": FilePath, "FileSize": FileSize, "Name": DownloadItem[5], "KodiType": KodiType, "KodiPathIdBeforeDownload": DownloadItem[2], "KodiFileId": DownloadItem[3], "KodiId": DownloadItem[0]}) + utils.EmbyServers[ServerId].API.download_file(EmbyId, DownloadItem[1], Path, FilePath, FileSize, DownloadItem[5], KodiType, DownloadItem[2], DownloadItem[3], DownloadItem[0]) def gotoshow(): KodiId = xbmc.getInfoLabel('ListItem.DBID') diff --git a/helper/player.py b/helper/player.py index a88412285..bc2b53436 100644 --- a/helper/player.py +++ b/helper/player.py @@ -49,12 +49,12 @@ def PlayerCommands(): if Commands[0] == "seek": xbmc.log("EMBY.hooks.player: [ onSeek ]", 1) # LOGINFO + EventData = json.loads(Commands[1]) + xbmc.log(f"EMBY.hooks.player: [ onSeek ] {EventData}", 0) # LOGDEBUG if not PlayingItem[0] or 'RunTimeTicks' not in PlayingItem[0]: continue - EventData = json.loads(Commands[1]) - if EventData['player']['playerid'] != -1: playerops.PlayerId = EventData['player']['playerid'] @@ -80,6 +80,7 @@ def PlayerCommands(): elif Commands[0] == "avstart": xbmc.log("EMBY.hooks.player: --> [ onAVStarted ]", 1) # LOGINFO EventData = json.loads(Commands[1]) + xbmc.log(f"EMBY.hooks.player: [ onAVStarted ] {EventData}", 0) # LOGDEBUG if EventData['player']['playerid'] != -1: playerops.PlayerId = EventData['player']['playerid'] @@ -126,7 +127,7 @@ def PlayerCommands(): KodiType = "" # Unsynced content: Update player info for dynamic/downloaded content (played via widget or themes or themes downloaded) - if not 'id' in EventData['item']: + if 'id' not in EventData['item']: isDynamic = FullPath.startswith("http://127.0.0.1:57342/dynamic/") isDynamicPathSubstitution = FullPath.startswith("/emby_addon_mode/dynamic/") isTheme = bool(FullPath.find("/EMBY-themes/") != -1) @@ -202,7 +203,7 @@ def PlayerCommands(): continue if isTheme: - globals()["QueuedPlayingItem"] = [{'CanSeek': True, 'QueueableMediaTypes': "Video,Audio", 'IsPaused': False, 'ItemId': int(EmbyId), 'MediaSourceId': None, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, None, None, None, utils.EmbyServers[ServerId], KodiType] + globals()["QueuedPlayingItem"] = [{'QueueableMediaTypes': ["Audio", "Video", "Photo"], 'CanSeek': True, 'IsPaused': False, 'ItemId': int(EmbyId), 'MediaSourceId': None, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, None, None, None, utils.EmbyServers[ServerId], KodiType] else: KodiId = EventData['item']['id'] KodiType = EventData['item']['type'] @@ -325,7 +326,7 @@ def PlayerCommands(): continue if EmbyId: - globals()["QueuedPlayingItem"] = [{'CanSeek': True, 'QueueableMediaTypes': "Video,Audio", 'IsPaused': False, 'ItemId': int(EmbyId), 'MediaSourceId': MediasourceID, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, IntroStartPosTicks, IntroEndPosTicks, None, EmbyServer, KodiType] + globals()["QueuedPlayingItem"] = [{'QueueableMediaTypes': ["Audio", "Video", "Photo"], 'CanSeek': not bool(KodiType == "channel"), 'IsPaused': False, 'ItemId': int(EmbyId), 'MediaSourceId': MediasourceID, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, IntroStartPosTicks, IntroEndPosTicks, None, EmbyServer, KodiType] else: continue @@ -361,9 +362,11 @@ def PlayerCommands(): PlaylistPosition = playerops.GetPlayerPosition(playerops.PlayerId) globals()["PlayingItem"][0].update({"NowPlayingQueue": NowPlayingQueue[playerops.PlayerId], "PlaylistLength": len(NowPlayingQueue[playerops.PlayerId]), "PlaylistIndex": PlaylistPosition}) - PlayingItem[4].API.session_progress(PlayingItem[0]) + PlayingItem[4].API.session_progress(PlayingItem[0], "PlaylistItemAdd") elif Commands[0] == "play": + xbmc.log("EMBY.hooks.player: [ onPlay ]", 1) # LOGINFO EventData = json.loads(Commands[1]) + xbmc.log(f"EMBY.hooks.player: [ onPlay ] {EventData}", 0) # LOGDEBUG if EventData['player']['playerid'] != -1: playerops.PlayerId = EventData['player']['playerid'] @@ -391,9 +394,7 @@ def PlayerCommands(): if PlayingItem[4] and PlayingItem[4].EmbySession: playerops.RemoteCommand(PlayingItem[4].ServerData['ServerId'], PlayingItem[4].EmbySession[0]['Id'], "pause") - PlayingItemEvent = {"EventName": "pause"} - PlayingItemEvent.update(PlayingItem[0]) - PlayingItem[4].API.session_progress(PlayingItemEvent) + PlayingItem[4].API.session_progress(PlayingItem[0], "Pause") xbmc.log("EMBY.hooks.player: -->[ paused ]", 0) # LOGDEBUG elif Commands[0] == "resume": xbmc.log("EMBY.hooks.player: [ onPlayBackResumed ]", 1) # LOGINFO @@ -406,13 +407,12 @@ def PlayerCommands(): playerops.RemoteCommand(PlayingItem[4].ServerData['ServerId'], PlayingItem[4].EmbySession[0]['Id'], "unpause") globals()["PlayingItem"][0]['IsPaused'] = False - PlayingItemEvent = {"EventName": "unpause"} - PlayingItemEvent.update(PlayingItem[0]) - PlayingItem[4].API.session_progress(PlayingItemEvent) + PlayingItem[4].API.session_progress(PlayingItem[0], "Unpause") xbmc.log("EMBY.hooks.player: --<[ paused ]", 0) # LOGDEBUG elif Commands[0] == "stop": + xbmc.log("EMBY.hooks.player: [ onPlayBackStopped ]", 1) # LOGINFO EventData = json.loads(Commands[1]) - xbmc.log(f"EMBY.hooks.player: [ onPlayBackStopped ] {EventData}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.player: [ onPlayBackStopped ] {EventData}", 0) # LOGDEBUG utils.SyncPause['playing'] = False playerops.AVStarted = False playerops.EmbyIdPlaying = 0 @@ -443,7 +443,7 @@ def PlayerCommands(): continue globals()["PlayingItem"][0].update({'VolumeLevel': Volume, 'IsMuted': Muted}) - PlayingItem[4].API.session_progress(PlayingItem[0]) + PlayingItem[4].API.session_progress(PlayingItem[0], "VolumeChange") xbmc.log("EMBY.hooks.player: THREAD: ---<[ player commands ]", 0) # LOGDEBUG @@ -512,7 +512,7 @@ def stop_playback(delete, PlayTrailer): def play_Trailer(EmbyServer): MediasourceID = EmbyServer.http.Intros[0]['MediaSources'][0]['Id'] - globals()["QueuedPlayingItem"] = [{'CanSeek': True, 'QueueableMediaTypes': "Video,Audio", 'IsPaused': False, 'ItemId': int(EmbyServer.http.Intros[0]['Id']), 'MediaSourceId': MediasourceID, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, None, None, None, EmbyServer, ""] + globals()["QueuedPlayingItem"] = [{'QueueableMediaTypes': ["Audio", "Video", "Photo"], 'CanSeek': True, 'IsPaused': False, 'ItemId': int(EmbyServer.http.Intros[0]['Id']), 'MediaSourceId': MediasourceID, 'PlaySessionId': str(uuid.uuid4()).replace("-", ""), 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': Volume, 'IsMuted': Muted}, None, None, None, EmbyServer, ""] Path = EmbyServer.http.Intros[0]['Path'] li = listitem.set_ListItem(EmbyServer.http.Intros[0], EmbyServer.ServerData['ServerId']) del EmbyServer.http.Intros[0] @@ -573,7 +573,7 @@ def PositionTracker(): if LoopCounter % 10 == 0: # modulo 10 globals()["PlayingItem"][0]['PositionTicks'] = Position xbmc.log(f"EMBY.hooks.player: PositionTracker: Report progress {PlayingItem[0]['PositionTicks']}", 0) # LOGDEBUG - PlayingItem[4].API.session_progress(PlayingItem[0]) + PlayingItem[4].API.session_progress(PlayingItem[0], "TimeUpdate") LoopCounter = 0 LoopCounter += 1 @@ -586,7 +586,7 @@ def jump_Intro(): playerops.Seek(PlayingItem[2]) globals()["PlayingItem"][0]['PositionTicks'] = PlayingItem[2] globals()["SkipIntroJumpDone"] = True - PlayingItem[4].API.session_progress(PlayingItem[0]) + PlayingItem[4].API.session_progress(PlayingItem[0], "TimeUpdate") def jump_Credits(): if PlayingItem[0].get('RunTimeTicks', 0): @@ -690,7 +690,7 @@ def sync_workers(): TasksRunning.append("sync_workers") if not utils.sleep(2): - for _, EmbyServer in list(utils.EmbyServers.items()): + for EmbyServer in utils.EmbyServers.values(): EmbyServer.library.RunJobs() TasksRunning.remove("sync_workers") diff --git a/helper/playerops.py b/helper/playerops.py index 3a1ec0abf..089eccf03 100644 --- a/helper/playerops.py +++ b/helper/playerops.py @@ -23,7 +23,7 @@ def enable_remotemode(ServerId): globals()["RemoteControl"] = True globals()["RemoteMode"] = True - send_RemoteClients(ServerId, [], True, False) + send_RemoteClients(ServerId, [], True) def ClearPlaylist(PlaylistId): utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "Playlist.Clear", "params": {{"playlistid": {PlaylistId}}}}}') @@ -414,7 +414,7 @@ def PlayEmby(ItemIds, PlayCommand, StartIndex, StartPositionTicks, EmbyServer, T #load additional items after playback started if PlayCommand not in ("PlayInit", "PlaySingle"): if DelayedQueryEmbyIds: - for Item in EmbyServer.API.get_Items_Ids(DelayedQueryEmbyIds, ["Episode", "Movie", "Trailer", "MusicVideo", "Audio", "Video", "Photo"], True, False, "", None): + for Item in EmbyServer.API.get_Items_Ids(DelayedQueryEmbyIds, ["Episode", "Movie", "Trailer", "MusicVideo", "Audio", "Video", "Photo"], True, False, "", None, {}): ListItem = listitem.set_ListItem(Item, EmbyServer.ServerData['ServerId']) Path, ShortType = common.get_path_type_from_item(EmbyServer.ServerData['ServerId'], Item) @@ -442,7 +442,7 @@ def PlayEmby(ItemIds, PlayCommand, StartIndex, StartPositionTicks, EmbyServer, T Pictures.append((PlaylistItem[5], PlaylistItem[4])) if PlayerIdPlaylistId == 2: # picture - utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "pictures", "parameters": ["plugin://plugin.video.emby-next-gen/?mode=remotepictures&position={KodiPlaylistIndexStartitem}", "return"]}}}}') + utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "pictures", "parameters": ["plugin://plugin.service.emby-next-gen/?mode=remotepictures&position={KodiPlaylistIndexStartitem}", "return"]}}}}') for Index, Picture in enumerate(Pictures): if Index != 0: @@ -466,12 +466,12 @@ def add_RemoteClientExtendedSupportAck(ServerId, SessionId, DeviceName, UserName if SessionId not in RemoteClientData[ServerId]["ExtendedSupportAck"]: add_RemoteClient(ServerId, SessionId, DeviceName, UserName) globals()['RemoteClientData'][ServerId]["ExtendedSupportAck"].append(SessionId) - send_RemoteClients(ServerId, RemoteClientData[ServerId]["ExtendedSupportAck"], False, False) + send_RemoteClients(ServerId, RemoteClientData[ServerId]["ExtendedSupportAck"], False) def init_RemoteClient(ServerId): globals()['RemoteClientData'][ServerId] = {"SessionIds": [utils.EmbyServers[ServerId].EmbySession[0]['Id']], "Usernames": {utils.EmbyServers[ServerId].EmbySession[0]['Id']: utils.EmbyServers[ServerId].EmbySession[0]['UserName']}, "Devicenames": {utils.EmbyServers[ServerId].EmbySession[0]['Id']: utils.EmbyServers[ServerId].EmbySession[0]['DeviceName']}, "ExtendedSupport": [utils.EmbyServers[ServerId].EmbySession[0]['Id']], "ExtendedSupportAck": [utils.EmbyServers[ServerId].EmbySession[0]['Id']]} -def delete_RemoteClient(ServerId, SessionIds, Force=False, LastWill=False): +def delete_RemoteClient(ServerId, SessionIds, Force=False): if ServerId not in RemoteClientData: xbmc.log(f"EMBY.helper.playerops: ServerId {ServerId} not found in RemoteClientData", 2) # LOGWARNING return @@ -492,7 +492,7 @@ def delete_RemoteClient(ServerId, SessionIds, Force=False, LastWill=False): if SessionId in RemoteCommandQueue: globals()['RemoteCommandQueue'][SessionId].put("QUIT") - send_RemoteClients(ServerId, ClientExtendedSupportAck, Force, LastWill) + send_RemoteClients(ServerId, ClientExtendedSupportAck, Force) # Disable remote mode when self device is the only one left if len(RemoteClientData[ServerId]["SessionIds"]) == 1 and RemoteClientData[ServerId]["SessionIds"][0] == utils.EmbyServers[ServerId].EmbySession[0]['Id']: @@ -542,7 +542,7 @@ def disable_RemoteClients(ServerId): if RemoteMode: for SessionId in RemoteClientData[ServerId]["ExtendedSupportAck"]: if SessionId != utils.EmbyServers[ServerId].EmbySession[0]['Id']: - utils.EmbyServers[ServerId].API.send_text_msg(SessionId, "remotecommand", "clients|||||", True, False) + utils.EmbyServers[ServerId].API.send_text_msg(SessionId, "remotecommand", "clients|||||", True) init_RemoteClient(ServerId) globals().update({"RemoteMode": False, "WatchTogether": False, "RemoteControl": False, "RemoteCommandActive": [0, 0, 0, 0, 0]}) @@ -550,7 +550,7 @@ def disable_RemoteClients(ServerId): if not utils.EmbyServers[ServerId].library.KodiStartSyncRunning: start_new_thread(utils.EmbyServers[ServerId].library.KodiStartSync, (False,)) -def send_RemoteClients(ServerId, SendSessionIds, Force, LastWill): +def send_RemoteClients(ServerId, SendSessionIds, Force): if not utils.remotecontrol_sync_clients: return @@ -573,7 +573,7 @@ def send_RemoteClients(ServerId, SendSessionIds, Force, LastWill): for SessionId in SendSessionIds: if SessionId != utils.EmbyServers[ServerId].EmbySession[0]['Id']: - utils.EmbyServers[ServerId].API.send_text_msg(SessionId, "remotecommand", Data, Force, LastWill) + utils.EmbyServers[ServerId].API.send_text_msg(SessionId, "remotecommand", Data, Force) # Remote control clients def RemoteCommand(ServerId, selfSessionId, Command, EmbyId=-1): diff --git a/helper/pluginmenu.py b/helper/pluginmenu.py index b7aa336fc..917dca9ba 100644 --- a/helper/pluginmenu.py +++ b/helper/pluginmenu.py @@ -23,19 +23,19 @@ def listing(Handle, ContentSupported): for ServerId, EmbyServer in list(utils.EmbyServers.items()): if ContentSupported != "image": - ItemsListings = add_ListItem(ItemsListings, f"{utils.Translate(33386)} ({EmbyServer.ServerData['ServerName']})", f"plugin://plugin.video.emby-next-gen/?mode=browse&query=NodesSynced&server={ServerId}&contentsupported={ContentSupported}", True, "DefaultHardDisk.png", utils.Translate(33383)) + ItemsListings = add_ListItem(ItemsListings, f"{utils.Translate(33386)} ({EmbyServer.ServerData['ServerName']})", f"plugin://plugin.service.emby-next-gen/?mode=browse&query=NodesSynced&server={ServerId}&contentsupported={ContentSupported}", "DefaultHardDisk.png", utils.Translate(33383)) - ItemsListings = add_ListItem(ItemsListings, f"{utils.Translate(33387)} ({EmbyServer.ServerData['ServerName']})", f"plugin://plugin.video.emby-next-gen/?mode=browse&query=NodesDynamic&server={ServerId}&contentsupported={ContentSupported}", True, "DefaultNetwork.png", utils.Translate(33384)) + ItemsListings = add_ListItem(ItemsListings, f"{utils.Translate(33387)} ({EmbyServer.ServerData['ServerName']})", f"plugin://plugin.service.emby-next-gen/?mode=browse&query=NodesDynamic&server={ServerId}&contentsupported={ContentSupported}", "DefaultNetwork.png", utils.Translate(33384)) # Common Items if utils.menuOptions: - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33194), "plugin://plugin.video.emby-next-gen/?mode=managelibsselection", False, "DefaultAddSource.png", utils.Translate(33309)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33059), "plugin://plugin.video.emby-next-gen/?mode=texturecache", False, "DefaultAddonImages.png", utils.Translate(33310)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(5), "plugin://plugin.video.emby-next-gen/?mode=settings", False, "DefaultAddon.png", utils.Translate(33398)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33058), "plugin://plugin.video.emby-next-gen/?mode=databasereset", False, "DefaultAddonsUpdates.png", utils.Translate(33313)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33340), "plugin://plugin.video.emby-next-gen/?mode=factoryreset", False, "DefaultAddonsUpdates.png", utils.Translate(33400)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33341), "plugin://plugin.video.emby-next-gen/?mode=nodesreset", False, "DefaultAddonsUpdates.png", utils.Translate(33401)) - ItemsListings = add_ListItem(ItemsListings, utils.Translate(33409), "plugin://plugin.video.emby-next-gen/?mode=skinreload", False, "DefaultAddonSkin.png", "") + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33194), "plugin://plugin.service.emby-next-gen/?mode=managelibsselection", "DefaultAddSource.png", utils.Translate(33309)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33059), "plugin://plugin.service.emby-next-gen/?mode=texturecache", "DefaultAddonImages.png", utils.Translate(33310)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(5), "plugin://plugin.service.emby-next-gen/?mode=settings", "DefaultAddon.png", utils.Translate(33398)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33058), "plugin://plugin.service.emby-next-gen/?mode=databasereset", "DefaultAddonsUpdates.png", utils.Translate(33313)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33340), "plugin://plugin.service.emby-next-gen/?mode=factoryreset", "DefaultAddonsUpdates.png", utils.Translate(33400)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33341), "plugin://plugin.service.emby-next-gen/?mode=nodesreset", "DefaultAddonsUpdates.png", utils.Translate(33401)) + ItemsListings = add_ListItem(ItemsListings, utils.Translate(33409), "plugin://plugin.service.emby-next-gen/?mode=skinreload", "DefaultAddonSkin.png", "") xbmcplugin.addDirectoryItems(Handle, ItemsListings, len(ItemsListings)) xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) @@ -80,13 +80,13 @@ def browse(Handle, Id, query, ParentId, Content, ServerId, LibraryId, ContentSup if query in ('NodesDynamic', 'NodesSynced'): for Node in utils.EmbyServers[ServerId].Views.Nodes[query]: if (ContentSupported == "audio" and Node['path'].startswith("library://music/")) or (ContentSupported == "video" and Node['path'].startswith("library://video/")): - ItemsListings = add_ListItem(ItemsListings, Node['title'], Node['path'], True, Node['icon'], "") + ItemsListings = add_ListItem(ItemsListings, Node['title'], Node['path'], Node['icon'], "") # Images (library://picture/ is not supported by Kodi) if query == 'NodesDynamic': for Node in utils.EmbyServers[ServerId].Views.Nodes[query]: if ContentSupported == "image" and not Node['path'].startswith("library://"): - ItemsListings = add_ListItem(ItemsListings, Node['title'], f"plugin://plugin.video.emby-next-gen/?id={Node['path']}&mode=browse&query=ImageDynamic&server={ServerId}&parentid={ParentId}&content={Content}&libraryid={LibraryId}", True, Node['icon'], "") + ItemsListings = add_ListItem(ItemsListings, Node['title'], f"plugin://plugin.service.emby-next-gen/?id={Node['path']}&mode=browse&query=ImageDynamic&server={ServerId}&parentid={ParentId}&content={Content}&libraryid={LibraryId}", Node['icon'], "") xbmcplugin.addDirectoryItems(Handle, ItemsListings, len(ItemsListings)) xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) @@ -98,7 +98,7 @@ def browse(Handle, Id, query, ParentId, Content, ServerId, LibraryId, ContentSup if query == 'ImageDynamic': for Node in utils.EmbyServers[ServerId].Views.PictureNodes[Id]: - ItemsListings = add_ListItem(ItemsListings, Node[0], Node[2], True, Node[3], "") + ItemsListings = add_ListItem(ItemsListings, Node[0], Node[2], Node[3], "") xbmcplugin.addDirectoryItems(Handle, ItemsListings, len(ItemsListings)) xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) @@ -325,7 +325,7 @@ def browse(Handle, Id, query, ParentId, Content, ServerId, LibraryId, ContentSup ItemsListingsCached = load_ListItem(ParentId, SortedItem, ServerId, ItemsListingsCached, Content, LibraryId) globals()["QueryCache"][SortItemContent][f"{Id}{SortItemContent}{ParentId}{ServerId}{LibraryId}"] = [True, ItemsListingsCached, Unsorted, Id, SortItemContent, ServerId, ParentId, LibraryId] - ItemsListings = add_ListItem(ItemsListings, f"--{SortItemContent}--", f"plugin://plugin.video.emby-next-gen/?id={Id}&mode=browse&query={SortItemContent}&server={ServerId}&parentid={ParentId}&content={SortItemContent}&libraryid={LibraryId}", True, IconMapping[SortItemContent], SortItemContent) + ItemsListings = add_ListItem(ItemsListings, f"--{SortItemContent}--", f"plugin://plugin.service.emby-next-gen/?id={Id}&mode=browse&query={SortItemContent}&server={ServerId}&parentid={ParentId}&content={SortItemContent}&libraryid={LibraryId}", IconMapping[SortItemContent], SortItemContent) WindowIdCheck = False else: # unique content @@ -408,7 +408,7 @@ def reload_Window(Content, ContentQuery, WindowId, Handle, Id, query, ServerId, xbmc.log("EMBY.helper.pluginmenu: ReloadWindow timeout", 3) # LOGERROR xbmc.executebuiltin('Dialog.Close(busydialog,true)') - utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "{ReloadWindowId}", "parameters": ["plugin://plugin.video.emby-next-gen/?id={Id}&mode=browse&query={query}&server={ServerId}&parentid={ParentId}&content={ContentQuery}&libraryid={LibraryId}", "return"]}}}}') + utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "{ReloadWindowId}", "parameters": ["plugin://plugin.service.emby-next-gen/?id={Id}&mode=browse&query={query}&server={ServerId}&parentid={ParentId}&content={ContentQuery}&libraryid={LibraryId}", "return"]}}}}') return True return False @@ -509,7 +509,7 @@ def load_ListItem(ParentId, Item, ServerId, ItemsListings, Content, LibraryId): StaggeredQuery = "MusicVideo" params = {'id': Item['Id'], 'mode': "browse", 'query': StaggeredQuery, 'server': ServerId, 'parentid': ParentId, 'content': Content, 'libraryid': LibraryId} - ItemsListings += ((f"plugin://plugin.video.emby-next-gen/?{urlencode(params)}", ListItem, True),) + ItemsListings += ((f"plugin://plugin.service.emby-next-gen/?{urlencode(params)}", ListItem, True),) else: path, _ = common.get_path_type_from_item(ServerId, Item) ItemsListings += ((path, ListItem, False),) @@ -517,12 +517,12 @@ def load_ListItem(ParentId, Item, ServerId, ItemsListings, Content, LibraryId): return ItemsListings #Menu structure nodes -def add_ListItem(ItemsListings, label, path, isFolder, artwork, HelpText): +def add_ListItem(ItemsListings, label, path, artwork, HelpText): ListItem = xbmcgui.ListItem(label, HelpText, path, True) ListItem.setContentLookup(False) ListItem.setProperties({'IsFolder': 'true', 'IsPlayable': 'false'}) - ListItem.setArt({"thumb": artwork, "fanart": "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg", "landscape": artwork or "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg", "banner": "special://home/addons/plugin.video.emby-next-gen/resources/banner.png", "clearlogo": "special://home/addons/plugin.video.emby-next-gen/resources/clearlogo.png", "icon": artwork}) - ItemsListings += ((path, ListItem, isFolder),) + ListItem.setArt({"thumb": artwork, "fanart": "special://home/addons/plugin.service.emby-next-gen/resources/fanart.jpg", "landscape": artwork or "special://home/addons/plugin.service.emby-next-gen/resources/fanart.jpg", "banner": "special://home/addons/plugin.service.emby-next-gen/resources/banner.png", "clearlogo": "special://home/addons/plugin.service.emby-next-gen/resources/clearlogo.png", "icon": artwork}) + ItemsListings += ((path, ListItem, True),) return ItemsListings def get_EmbyServerList(): @@ -753,7 +753,7 @@ def cache_textures_generator(selection): TempUrls = TotalRecords * [()] ItemCounter = 0 - for Item in EmbyServer.API.get_Items(None, ["PhotoAlbum"], True, True, {}): + for Item in EmbyServer.API.get_Items(None, ["PhotoAlbum"], True, True, {}, "", False): path, _ = common.get_path_type_from_item(ServerId, Item) TempUrls[ItemCounter] = (path,) ItemCounter += 1 @@ -765,7 +765,7 @@ def cache_textures_generator(selection): TempUrls = TotalRecords * [()] ItemCounter = 0 - for Item in EmbyServer.API.get_Items(None, ["Photo"], True, True, {}): + for Item in EmbyServer.API.get_Items(None, ["Photo"], True, True, {}, "", False): path, _ = common.get_path_type_from_item(ServerId, Item) TempUrls[ItemCounter] = (path,) ItemCounter += 1 diff --git a/helper/queue.py b/helper/queue.py index a473412af..be118e0ed 100644 --- a/helper/queue.py +++ b/helper/queue.py @@ -13,10 +13,14 @@ def get(self): try: with self.Lock: with self.Busy: - ReturnData = self.QueuedItems[0] - self.QueuedItems = self.QueuedItems[1:] + if self.QueuedItems: + ReturnData = self.QueuedItems[0] + self.QueuedItems = self.QueuedItems[1:] + else: # clear triggered + ReturnData = () except Exception as Error: xbmc.log(f"EMBY.helper.queue: get: {Error}", 2) # LOGWARNING + ReturnData = () if not self.QueuedItems: self.LockQueue() @@ -31,6 +35,7 @@ def getall(self): self.QueuedItems = () except Exception as Error: xbmc.log(f"EMBY.helper.queue: getall: {Error}", 2) # LOGWARNING + ReturnData = () self.LockQueue() return ReturnData diff --git a/helper/utils.py b/helper/utils.py index 03a804722..2f57c20aa 100644 --- a/helper/utils.py +++ b/helper/utils.py @@ -1,3 +1,4 @@ +import sys import os import shutil import json @@ -17,7 +18,11 @@ import xbmcaddon import xbmcgui -Addon = xbmcaddon.Addon("plugin.video.emby-next-gen") +try: + Addon = xbmcaddon.Addon("plugin.service.emby-next-gen") +except Exception as error: + sys.exit(0) + EmbyTypeMapping = {"Person": "actor", "Video": "movie", "Movie": "movie", "Series": "tvshow", "Season": "season", "Episode": "episode", "Audio": "song", "MusicAlbum": "album", "MusicArtist": "artist", "Genre": "genre", "MusicGenre": "genre", "Tag": "tag" , "Studio": "studio" , "BoxSet": "set", "Folder": None, "MusicVideo": "musicvideo", "Playlist": "Playlist"} KodiTypeMapping = {"actor": "Person", "tvshow": "Series", "season": "Season", "episode": "Episode", "song": "Audio", "album": "MusicAlbum", "artist": "MusicArtist", "genre": "Genre", "tag": "Tag", "studio": "Studio" , "set": "BoxSet", "musicvideo": "MusicVideo", "playlist": "Playlist", "movie": "Movie"} addon_version = Addon.getAddonInfo('version') @@ -149,9 +154,9 @@ ArtworkLimitationThumb = 40 ArtworkLimitationBackdrop = 100 ArtworkLimitationChapter = 20 -DownloadPath = "special://profile/addon_data/plugin.video.emby-next-gen/" -FolderAddonUserdata = "special://profile/addon_data/plugin.video.emby-next-gen/" -FolderEmbyTemp = "special://profile/addon_data/plugin.video.emby-next-gen/temp/" +DownloadPath = "special://profile/addon_data/plugin.service.emby-next-gen/" +FolderAddonUserdata = "special://profile/addon_data/plugin.service.emby-next-gen/" +FolderEmbyTemp = "special://profile/addon_data/plugin.service.emby-next-gen/temp/" FolderUserdataThumbnails = "special://profile/Thumbnails/" PlaylistPath = "special://profile/playlists/mixed/" KodiFavFile = "special://profile/favourites.xml" @@ -275,14 +280,19 @@ def image_overlay(ImageTag, ServerId, EmbyID, ImageType, ImageIndex, OverlayText BorderSize = int(ImageHeight * 0.01) # 1% of image height is box border size BoxTop = int(ImageHeight * 0.75) # Box top position is 75% of image height BoxHeight = int(ImageHeight * 0.15) # 15% of image height is box height - BoxWidth = int(ImageWidth - 2 * BorderSize) + BoxWidth = int(ImageWidth) fontsize = 5 - _, _, FontWidth, FontHeight = font.getbbox("Title Seauence") + + try: + _, _, FontWidth, FontHeight = font.getbbox("Title Sequence") + except Exception as Error: + xbmc.log(f"EMBY.helper.utils: Pillow issue (getbox): {Error}", 3) # LOGERROR + return BinaryData, ContentType while FontHeight < BoxHeight - BorderSize * 2 and FontWidth < BoxWidth - BorderSize * 2: fontsize += 1 font = ImageFont.truetype(FontPath, fontsize) - _, _, FontWidth, FontHeight = font.getbbox("Title Seauence") + _, _, FontWidth, FontHeight = font.getbbox("Title Sequence") OverlayText = OverlayText.split("\n") OverlayTextNewLines = len(OverlayText) @@ -292,8 +302,8 @@ def image_overlay(ImageTag, ServerId, EmbyID, ImageType, ImageIndex, OverlayText font = ImageFont.truetype(FontPath, fontsize) OverlayText = "\n".join(OverlayText) - draw.rectangle((-100, BoxTop - BorderSize, BoxWidth + 200, BoxTop + BoxHeight + BorderSize * 2), fill=(0, 0, 0, 127), outline="white", width=BorderSize) - draw.text(xy=(ImageWidth / 2, BoxTop + BorderSize * 2 + FontHeight / 2), text=OverlayText, fill="#FFFFFF", font=font, anchor="mm", align="center") + draw.rectangle((-100, BoxTop, BoxWidth + 200, BoxTop + BoxHeight), fill=(0, 0, 0, 127), outline="white", width=BorderSize) + draw.text(xy=(ImageWidth / 2, BoxTop + (BoxHeight / 2)) , text=OverlayText, fill="#FFFFFF", font=font, anchor="mm", align="center") imgByteArr = io.BytesIO() img.save(imgByteArr, format=img.format) return imgByteArr.getvalue(), "image/jpeg" @@ -360,7 +370,14 @@ def mkDir(Path): Path = translatePath(Path) if not os.path.isdir(Path): - os.mkdir(Path) + try: + os.mkdir(Path) + return True + except Exception as Error: + xbmc.log(f"EMBY.helper.utils: mkDir: {Error}", 3) # LOGERROR + return False + + return True def delFile(Path): Path = translatePath(Path) @@ -421,7 +438,7 @@ def writeFileString(Path, Data): try: with open(Path, "wb") as outfile: outfile.write(Data) - except Exception as Error: # permission denied + except Exception as Error: xbmc.log(f"EMBY.helper.utils: writeFileString ({Path}): {Error}", 2) # LOGWARNING def getFreeSpace(Path): @@ -440,8 +457,11 @@ def getFreeSpace(Path): def writeFileBinary(Path, Data): Path = translatePath(Path) - with open(Path, "wb") as outfile: - outfile.write(Data) + try: + with open(Path, "wb") as outfile: + outfile.write(Data) + except Exception as Error: + xbmc.log(f"EMBY.helper.utils: writeFileBinary ({Path}): {Error}", 2) # LOGWARNING def checkFileExists(Path): Path = translatePath(Path) @@ -503,6 +523,22 @@ def currenttime_kodi_format_and_unixtime(): def get_unixtime_emby_format(): # Position(ticks) in Emby format 1 tick = 10000ms return datetime.timestamp(datetime.utcnow()) * 10000 +def get_url_info(ConnectionString): + Temp = ConnectionString.split(":") + Scheme = Temp[0] + + if len(Temp) < 3: + if Scheme == "https": + Port = 443 + else: + Port = 80 + else: + Port = int(Temp[2].split("?", 1)[0].split("/", 1)[0]) + + Hostname = Temp[1][2:].split("?", 1)[0].split("/", 1)[0] + xbmc.log(f"Emby.helper.utils: get_url_info: {Scheme} / {Hostname} / {Port}", 0) # LOGDEBUG + return Scheme, Hostname, Port + # Remove all emby playlists def delete_playlists(): SearchFolders = ['special://profile/playlists/video/', 'special://profile/playlists/music/'] @@ -824,24 +860,31 @@ def InitSettings(): else: globals()["device_name"] = quote(device_name) # url encode - ToggleIcon = [] + # Animated icons + NewIcon = "" if animateicon: if icon and icon != "special://home/addons/plugin.video.emby-next-gen/resources/icon-animated.gif": - ToggleIcon = ["resources/icon.png", "resources/icon-animated.gif"] + NewIcon = "animated" globals()["icon"] = "special://home/addons/plugin.video.emby-next-gen/resources/icon-animated.gif" else: - if icon and icon != "special://home/addons/plugin.video.emby-next-gen/resources/icon.png": - ToggleIcon = ["resources/icon-animated.gif", "resources/icon.png"] + if icon and icon != "special://home/addons/plugin.service.emby-next-gen/resources/icon.png": + NewIcon = "static" - globals()["icon"] = "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + globals()["icon"] = "special://home/addons/plugin.service.emby-next-gen/resources/icon.png" + + if NewIcon: + for PluginId in ("video", "image", "audio", "service"): + xbmc.log("EMBY.helper.utils: Toggle icon", 1) # LOGINFO + AddonXml = readFileString(f"special://home/addons/plugin.{PluginId}.emby-next-gen/addon.xml") + + if NewIcon == "static": + AddonXml = AddonXml.replace("resources/icon-animated.gif", "resources/icon.png") + else: + AddonXml = AddonXml.replace("resources/icon.png", "resources/icon-animated.gif") - if ToggleIcon: - xbmc.log("EMBY.helper.utils: Toggle icon", 1) # LOGINFO - AddonXml = readFileString("special://home/addons/plugin.video.emby-next-gen/addon.xml") - AddonXml = AddonXml.replace(ToggleIcon[0], ToggleIcon[1]) - writeFileString("special://home/addons/plugin.video.emby-next-gen/addon.xml", AddonXml) + writeFileString(f"special://home/addons/plugin.{PluginId}.emby-next-gen/addon.xml", AddonXml) globals()["displayMessage"] *= 1000 globals()["newContentTime"] *= 1000 @@ -963,8 +1006,8 @@ def decode_XML(Data): InitSettings() DatabaseFiles = {'texture': "", 'texture-version': 0, 'music': "", 'music-version': 0, 'video': "", 'video-version': 0, 'epg': "", 'epg-version': 0, 'tv': "", 'tv-version': 0} _, FolderDatabasefiles = listDir("special://profile/Database/") -FontPath = translatePath("special://home/addons/plugin.video.emby-next-gen/resources/font/LiberationSans-Bold.ttf") -noimagejpg = readFileBinary("special://home/addons/plugin.video.emby-next-gen/resources/noimage.jpg") +FontPath = translatePath("special://home/addons/plugin.service.emby-next-gen/resources/font/LiberationSans-Bold.ttf") +noimagejpg = readFileBinary("special://home/addons/plugin.service.emby-next-gen/resources/noimage.jpg") set_settings_bool('artworkcacheenable', True) for FolderDatabaseFilename in FolderDatabasefiles: diff --git a/hooks/monitor.py b/hooks/monitor.py index 1f04d30dd..dd4b210a0 100644 --- a/hooks/monitor.py +++ b/hooks/monitor.py @@ -161,7 +161,7 @@ def monitor_EventQueue(): # Threaded / queued elif Event[0] == "managelibsselection": start_new_thread(pluginmenu.select_managelibs, ()) elif Event[0] == "opensettings": - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + xbmc.executebuiltin('Addon.OpenSettings(plugin.service.emby-next-gen)') elif Event[0] == "backup": start_new_thread(Backup, ()) elif Event[0] == "restore": @@ -399,7 +399,7 @@ def VideoLibrary_OnUpdate(): else: if 'item' not in data: if f"KODI{EmbyId}" not in utils.ItemSkipUpdate and EmbyId: # Check EmbyID - if not f"{{'item':{UpdateItem}}}" in UpdateItems: + if f"{{'item':{UpdateItem}}}" not in UpdateItems: xbmc.log(f"EMBY.hooks.monitor: [ VideoLibrary_OnUpdate reset progress {EmbyId} ]", 1) # LOGINFO if int(EmbyId) in EmbyUpdateItems: @@ -471,7 +471,7 @@ def BackupRestore(): utils.delete_playlists() utils.delete_nodes() - RestoreFolderAddonData = f"{RestoreFolder}/addon_data/plugin.video.emby-next-gen/" + RestoreFolderAddonData = f"{RestoreFolder}/addon_data/plugin.service.emby-next-gen/" utils.copytree(RestoreFolderAddonData, utils.FolderAddonUserdata) RestoreFolderDatabase = f"{RestoreFolder}/Database/" utils.copytree(RestoreFolderDatabase, "special://profile/Database/") @@ -498,7 +498,7 @@ def Backup(): utils.delFolder(backup) - destination_data = f"{backup}addon_data/plugin.video.emby-next-gen/" + destination_data = f"{backup}addon_data/plugin.service.emby-next-gen/" destination_databases = f"{backup}Database/" utils.mkDir(backup) utils.mkDir(f"{backup}addon_data/") @@ -626,7 +626,7 @@ def settingschanged(): # threaded by caller for Filename in files: utils.delFile(f"{playlistfolder}{Filename}") - # Chnage download path + # Change download path if DownloadPathPreviousValue != utils.DownloadPath: pluginmenu.downloadreset(DownloadPathPreviousValue) @@ -662,12 +662,31 @@ def ServersConnect(): xbmc.log("EMBY.hooks.monitor: THREAD: ---<[ ServersConnect ]", 0) # LOGDEBUG def setup(): + # move settings (old plugin structure) + if utils.checkFolderExists("special://profile/addon_data/plugin.video.emby-next-gen/"): + if utils.checkFolderExists("special://profile/addon_data/plugin.service.emby-next-gen/"): + utils.delFolder("special://profile/addon_data/plugin.service.emby-next-gen/") + + utils.copytree("special://profile/addon_data/plugin.video.emby-next-gen/", "special://profile/addon_data/plugin.service.emby-next-gen/") + utils.delFolder("special://profile/addon_data/plugin.video.emby-next-gen/") + utils.delete_nodes() + + if not utils.checkFolderExists("special://profile/addon_data/plugin.video.emby-next-gen/"): + return False + # copy default nodes utils.mkDir("special://profile/library/video/") utils.mkDir("special://profile/library/music/") utils.copytree("special://xbmc/system/library/video/", "special://profile/library/video/") utils.copytree("special://xbmc/system/library/music/", "special://profile/library/music/") + # copy animated icons + for PluginId in ("video", "image", "audio"): + Destination = f"special://home/addons/plugin.{PluginId}.emby-next-gen/resources/icon-animated.gif" + + if not utils.checkFileExists(Destination): + utils.copyFile("special://home/addons/plugin.service.emby-next-gen/resources/icon-animated.gif", Destination) + if utils.MinimumSetup == utils.MinimumVersion: return True @@ -725,6 +744,7 @@ def StartUp(): XbmcMonitor.waitForAbort(0) # Shutdown + utils.SystemShutdown = True utils.FavoriteQueue.put("QUIT") EventQueue.put("QUIT") diff --git a/hooks/webservice.py b/hooks/webservice.py index d7f8e6a52..e1d345e36 100644 --- a/hooks/webservice.py +++ b/hooks/webservice.py @@ -30,29 +30,32 @@ DelayedContentLock = allocate_lock() def start(): - globals()['Socket'] = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) - Socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) - Socket.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1) - Socket.bind(('127.0.0.1', 57342)) - Socket.settimeout(None) - xbmc.log("EMBY.hooks.webservice: Start", 1) # LOGINFO - globals()["Running"] = True - start_new_thread(Listen, ()) + if not Running: + globals()["Running"] = True + + try: # intercept multiple start by different threads (just precaution) + globals()['Socket'] = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + Socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) + Socket.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1) + Socket.bind(('127.0.0.1', 57342)) + except Exception as Error: + xbmc.log(f"EMBY.hooks.webservice: Socket start (error) {Error}", 1) # LOGINFO + return False + + xbmc.log("EMBY.hooks.webservice: Start", 1) # LOGINFO + start_new_thread(Listen, ()) + return True + + return False def close(): if Running: globals()["Running"] = False try: - try: - Socket.shutdown(_socket.SHUT_RDWR) - except Exception as Error: - xbmc.log(f"EMBY.hooks.webservice: Socket shutdown (error) {Error}", 1) # LOGINFO - Socket.close() - xbmc.log("EMBY.hooks.webservice: Socket shutdown", 1) # LOGINFO except Exception as Error: - xbmc.log(f"EMBY.hooks.webservice: Socket close (error) {Error}", 3) # LOGERROR + xbmc.log(f"EMBY.hooks.webservice: Socket shutdown (error) {Error}", 1) # LOGINFO xbmc.log("EMBY.hooks.webservice: Shutdown weservice", 1) # LOGINFO xbmc.log(f"EMBY.hooks.webservice: DelayedContent queue size: {len(DelayedContent)}", 0) # LOGDEBUG @@ -60,17 +63,17 @@ def close(): def Listen(): xbmc.log("EMBY.hooks.webservice: THREAD: --->[ webservice/57342 ]", 0) # LOGDEBUG Socket.listen() + Socket.settimeout(1) while not utils or not utils.SystemShutdown: try: fd, _ = Socket._accept() - except Exception as Error: - xbmc.log(f"EMBY.hooks.webservice: Socket shutdown (error) {Error}", 3) # LOGERROR - break + except: + continue start_new_thread(worker_Query, (fd,)) - xbmc.log("EMBY.hooks.webservice: THREAD: ---<[ webservice/57342 ]", 0) # LOGDEBUG + xbmc.log("EMBY.hooks.webservice: THREAD: ---<[ webservice/57342 ]", 1) # LOGDEBUG def worker_Query(fd): # thread by caller xbmc.log("EMBY.hooks.webservice: THREAD: --->[ worker_Query ]", 0) # LOGDEBUG @@ -86,8 +89,8 @@ def worker_Query(fd): # thread by caller IncomingData = data.split(' ') - if not IncomingData[0] == "EVENT" or ("mode" in IncomingData[1] and "query=NodesDynamic" not in IncomingData[1] and "query=NodesSynced" not in IncomingData[1]): - while not utils.EmbyServers or not list(utils.EmbyServers.values())[0].ServerData['Online']: + if IncomingData[0] != "EVENT" or ("mode" in IncomingData[1] and "query=NodesDynamic" not in IncomingData[1] and "query=NodesSynced" not in IncomingData[1]): + while not utils.EmbyServers or not list(utils.EmbyServers.values())[0].Online: Break = False if utils.sleep(1): @@ -237,7 +240,7 @@ def worker_Query(fd): # thread by caller elif CacheId2 in pluginmenu.QueryCache["All"]: pluginmenu.QueryCache["All"][CacheId2][0] = False - utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "videos", "parameters": ["plugin://plugin.video.emby-next-gen/?id=0&mode=browse&query=Search&server={ServerId}&parentid=0&content=All&libraryid=0", "return"]}}}}') + utils.SendJson(f'{{"jsonrpc": "2.0", "id": 1, "method": "GUI.ActivateWindow", "params": {{"window": "videos", "parameters": ["plugin://plugin.service.emby-next-gen/?id=0&mode=browse&query=Search&server={ServerId}&parentid=0&content=All&libraryid=0", "return"]}}}}') xbmc.log("EMBY.hooks.webservice: THREAD: ---<[ worker_Query ] event search", 0) # LOGDEBUG return @@ -245,7 +248,7 @@ def worker_Query(fd): # thread by caller if mode == 'settings': # Simple commands client.send(sendOK) client.close() - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + xbmc.executebuiltin('Addon.OpenSettings(plugin.service.emby-next-gen)') xbmc.log("EMBY.hooks.webservice: THREAD: ---<[ worker_Query ] event settings", 0) # LOGDEBUG return @@ -333,7 +336,7 @@ def worker_Query(fd): # thread by caller PayloadLower = IncomingData[1].lower() isHEAD = IncomingData[0] == "HEAD" - PictureQuery = IncomingData[1].startswith('/picture/') or IncomingData[1].startswith('/delayed_content/picture') + PictureQuery = IncomingData[1].startswith('/picture/') or IncomingData[1].startswith('/delayed_content/picture') or IncomingData[1].startswith('/delayed_content/p-') if 'extrafanart' in PayloadLower or 'extrathumbs' in PayloadLower or 'extras/' in PayloadLower or PayloadLower.endswith('.edl') or PayloadLower.endswith('index.bdmv') or PayloadLower.endswith('index.bdm') or PayloadLower.endswith('.txt') or PayloadLower.endswith('.vprj') or PayloadLower.endswith('.xml') or PayloadLower.endswith('/') or PayloadLower.endswith('.nfo') or (not PictureQuery and (PayloadLower.endswith('.bmp') or PayloadLower.endswith('.jpg') or PayloadLower.endswith('.ico') or PayloadLower.endswith('.png') or PayloadLower.endswith('.ifo') or PayloadLower.endswith('.gif') or PayloadLower.endswith('.tbn') or PayloadLower.endswith('.tiff'))): # Unsupported queries used by Kodi client.send(sendNotFound) @@ -393,8 +396,8 @@ def send_redirect(client, QueryData, Data, Filename): xbmc.executebuiltin('Dialog.Close(busydialog,true)') # workaround due to Kodi bug: https://github.com/xbmc/xbmc/issues/16756 if "main.m3u8" in Data: - MainM3U8 = utils.EmbyServers[QueryData['ServerId']].http.request({'type': "GET", 'handler': Path.replace(f"{utils.EmbyServers[QueryData['ServerId']].ServerData['ServerUrl']}/emby/" , "")}, False, True) - MainM3U8Mod = MainM3U8.decode().replace("hls1/main/", f"{utils.EmbyServers[QueryData['ServerId']].ServerData['ServerUrl']}/emby/videos/{QueryData['EmbyID']}/hls1/main/").encode() + _, _, MainM3U8 = utils.EmbyServers[QueryData['ServerId']].http.request("GET", Path.replace(f"{utils.EmbyServers[QueryData['ServerId']].ServerData['ServerUrl']}/emby/" , ""), {}, {}, True, "", False) + MainM3U8Mod = MainM3U8.decode('utf-8').replace("hls1/main/", f"{utils.EmbyServers[QueryData['ServerId']].ServerData['ServerUrl']}/emby/videos/{QueryData['EmbyID']}/hls1/main/").encode() SendData = f"HTTP/1.1 200 OK\r\nServer: Emby-Next-Gen\r\nConnection: close\r\nContent-Length: {len(MainM3U8Mod)}\r\nContent-Type: text/plain\r\n\r\n".encode() + MainM3U8Mod else: SendData = f"HTTP/1.1 307 Temporary Redirect\r\nServer: Emby-Next-Gen\r\nConnection: close\r\nLocation: {Path}\r\nContent-length: 0\r\n\r\n".encode() @@ -973,15 +976,17 @@ def send_delayed_content(client, ContentId): return True def set_QueuedPlayingItem(QueryData, PlaySessionId): + utils.PlayerBusy = True + if not PlaySessionId: QueryData['PlaySessionId'] = str(uuid.uuid4()).replace("-", "") else: QueryData['PlaySessionId'] = PlaySessionId if 'LiveStreamId' in QueryData: - player.QueuedPlayingItem = [{'CanSeek': True, 'QueueableMediaTypes': "Video,Audio", 'IsPaused': False, 'ItemId': int(QueryData['EmbyID']), 'MediaSourceId': QueryData['MediasourceID'], 'PlaySessionId': QueryData['PlaySessionId'], 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': player.Volume, 'IsMuted': player.Muted, "LiveStreamId": QueryData['LiveStreamId']}, QueryData['IntroStartPositionTicks'], QueryData['IntroEndPositionTicks'], QueryData['CreditsPositionTicks'], utils.EmbyServers[QueryData['ServerId']], ""] + player.QueuedPlayingItem = [{'QueueableMediaTypes': ["Audio", "Video", "Photo"], 'CanSeek': True, 'IsPaused': False, 'ItemId': int(QueryData['EmbyID']), 'MediaSourceId': QueryData['MediasourceID'], 'PlaySessionId': QueryData['PlaySessionId'], 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': player.Volume, 'IsMuted': player.Muted, "LiveStreamId": QueryData['LiveStreamId']}, QueryData['IntroStartPositionTicks'], QueryData['IntroEndPositionTicks'], QueryData['CreditsPositionTicks'], utils.EmbyServers[QueryData['ServerId']], ""] else: - player.QueuedPlayingItem = [{'CanSeek': True, 'QueueableMediaTypes': "Video,Audio", 'IsPaused': False, 'ItemId': int(QueryData['EmbyID']), 'MediaSourceId': QueryData['MediasourceID'], 'PlaySessionId': QueryData['PlaySessionId'], 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': player.Volume, 'IsMuted': player.Muted}, QueryData['IntroStartPositionTicks'], QueryData['IntroEndPositionTicks'], QueryData['CreditsPositionTicks'], utils.EmbyServers[QueryData['ServerId']], ""] + player.QueuedPlayingItem = [{'QueueableMediaTypes': ["Audio", "Video", "Photo"], 'CanSeek': True, 'IsPaused': False, 'ItemId': int(QueryData['EmbyID']), 'MediaSourceId': QueryData['MediasourceID'], 'PlaySessionId': QueryData['PlaySessionId'], 'PositionTicks': 0, 'RunTimeTicks': 0, 'VolumeLevel': player.Volume, 'IsMuted': player.Muted}, QueryData['IntroStartPositionTicks'], QueryData['IntroEndPositionTicks'], QueryData['CreditsPositionTicks'], utils.EmbyServers[QueryData['ServerId']], ""] def open_db(QueryData, Id): if Id not in QueryData['Database']: diff --git a/hooks/websocket.py b/hooks/websocket.py index 9a623eaae..9beb68098 100644 --- a/hooks/websocket.py +++ b/hooks/websocket.py @@ -1,40 +1,13 @@ -from urllib.parse import urlparse import json -import os -import array -import struct -import uuid -import base64 -import hashlib -import ssl from _thread import start_new_thread -import _socket import xbmc import xbmcgui from helper import utils, playerops, queue from database import dbio - -def maskData(mask_key, data): - _m = array.array("B", mask_key) - _d = array.array("B", data) - - for i in range(len(_d)): - _d[i] ^= _m[i % 4] # ixor - - return _d.tobytes() - -class WSClient: +class WebSocket: def __init__(self, EmbyServer): self.EmbyServer = EmbyServer - self.stop = False - self.sock = None - self._recv_buffer = () - self._frame_header = None - self._frame_length = None - self._frame_mask = None - self._cont_data = None - self.EmbyServerSocketUrl = "" self.ConnectionInProgress = False self.Tasks = {} self.EmbyServerSyncCheckRunning = False @@ -42,358 +15,24 @@ def __init__(self, EmbyServer): self.RefreshProgressInit = False self.EPGRefresh = False self.ProgressBar = {} - self.AsyncMessageQueue = queue.Queue() - xbmc.log("Emby.hooks.websocket: WSClient initializing...", 0) # LOGDEBUG - - def start(self): - start_new_thread(self.Listen, ()) - start_new_thread(self.on_message, ()) - - def close(self, Terminate=True): - if Terminate: - self.stop = True - self.AsyncMessageQueue.put("QUIT") - - if self.sock: - xbmc.log(f"Emby.hooks.websocket: Close connection {self.EmbyServer.ServerData['ServerId']} / {self.EmbyServer.ServerData['ServerName']}", 1) # LOGINFO - - try: - self.sock.settimeout(1) - self.sendCommands(struct.pack('!H', 1000), 0x8) - self.sock.shutdown(_socket.SHUT_RDWR) - self.sock.close() - self.sock = None - except Exception as error: - xbmc.log(f"Emby.hooks.websocket: {error}", 3) # LOGERROR - - self._recv_buffer = () - self._frame_header = None - self._frame_length = None - self._frame_mask = None - self._cont_data = None - self.close_EmbyServerBusy() - - def Listen(self): - xbmc.log("Emby.hooks.websocket: THREAD: --->[ websocket ]", 0) # LOGDEBUG - - if self.EmbyServer.ServerData['ServerUrl'].startswith('https'): - self.EmbyServerSocketUrl = self.EmbyServer.ServerData['ServerUrl'].replace('https', "wss") - else: - self.EmbyServerSocketUrl = self.EmbyServer.ServerData['ServerUrl'].replace('http', "ws") - - self.connect() - - while not self.stop: - data = self.recv() - - if self.stop: - break - - if data: - self.AsyncMessageQueue.put(data[1]) - - if data is None: # re-connect - xbmc.log("Emby.hooks.websocket: No data received, reconnecting", 1) # LOGINFO - self.connect() - - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ websocket ]", 0) # LOGDEBUG - - def connect(self): - if not self.ConnectionInProgress: - self.ConnectionInProgress = True - Notification = True + self.MessageQueue = queue.Queue() + xbmc.log("EMBY.hooks.websocket: WSClient initializing...", 0) # LOGDEBUG - while not self.stop: - xbmc.log("Emby.hooks.websocket: --->[ websocket connect ]", 1) # LOGINFO - - if self.establish_connection(): - xbmc.log("Emby.hooks.websocket: ---<[ websocket connect ]", 1) # LOGINFO - break - - if Notification: - utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message=utils.Translate(33430), time=utils.newContentTime, sound=False) - Notification = False - - xbmc.log("Emby.hooks.websocket: [ websocket connect failed ]", 1) # LOGINFO - - if utils.sleep(5): - break - - self.ConnectionInProgress = False - else: - xbmc.log("Emby.hooks.websocket: [ websocket connect in progress ]", 1) # LOGINFO - utils.sleep(1) - - def establish_connection(self): - self.close(False) - url = f"{self.EmbyServerSocketUrl}/embywebsocket?api_key={self.EmbyServer.ServerData['AccessToken']}&DeviceId={self.EmbyServer.ServerData['DeviceId']}" - - # Parse URL - scheme, url = url.split(":", 1) - parsed = urlparse(url, scheme="http") - resource = parsed.path - - if parsed.query: - resource += f"?{parsed.query}" - - hostname = parsed.hostname - port = parsed.port - is_secure = scheme == "wss" - - if not port: - if scheme == "http": - port = 80 - else: - port = 443 - - address_family = _socket.getaddrinfo(hostname, None)[0][0] - self.sock = _socket.socket(address_family, _socket.SOCK_STREAM) - self.sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) - self.sock.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1) - - # Connect - try: - self.sock.connect((hostname, port)) - self.sock.settimeout(60) # set timeout > ping interval (10 seconds ping (6 pings -> timeout)) - - if is_secure: - self.sock = ssl.SSLContext(ssl.PROTOCOL_SSLv23).wrap_socket(self.sock, do_handshake_on_connect=True, suppress_ragged_eofs=True, server_hostname=hostname) - - # Handshake - uid = uuid.uuid4() - EncodingKey = base64.b64encode(uid.bytes).strip().decode('utf-8') - header_str = f"GET {resource} HTTP/1.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nHost: {hostname}:{port}\r\nSec-WebSocket-Key: {EncodingKey}\r\nSec-WebSocket-Version: 13\r\n\r\n" - self.sock.send(header_str.encode('utf-8')) - except Exception as error: - xbmc.log(f"Emby.hooks.websocket: {error}", 3) # LOGERROR - return False - - # Read Headers - status = None - headers = {} + def Message(self): # threaded + xbmc.log("EMBY.hooks.websocket: THREAD: --->[ message ]", 0) # LOGDEBUG while True: - line = () - - while True: - try: - c = self.sock.recv(1).decode() - except Exception as error: - xbmc.log(f"Emby.hooks.websocket: {error}", 3) # LOGERROR - return False - - if not c: - return False - - line += (c,) - - if c == "\n": - break - - line = "".join(line) - - if line == "\r\n": - break - - line = line.strip() - - if not status: - status_info = line.split(" ", 2) - - if len(status_info) < 2: - return False - - status = int(status_info[1]) - else: - kv = line.split(":", 1) - - if len(kv) == 2: - key, value = kv - headers[key.lower()] = value.strip() - else: - xbmc.log("Emby.hooks.websocket: invalid haeader", 0) # LOGDEBUG - return False - - if status != 101: - xbmc.log(f"Emby.hooks.websocket: Handshake status {status}", 0) # LOGDEBUG - return False - - # Validate Headers - result = headers.get("sec-websocket-accept", "") - - if not result: - utils.Dialog.notification(heading=utils.addon_name, icon="DefaultIconError.png", message=utils.Translate(33235), sound=True, time=utils.newContentTime) - return False - - value = f"{EncodingKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - value = value.encode("utf-8") - hashed = base64.b64encode(hashlib.sha1(value).digest()).strip().lower().decode('utf-8') - - if hashed == result.lower(): - start_new_thread(self.ping, ()) - self.sendCommands('{"MessageType": "ScheduledTasksInfoStart", "Data": "0,1500"}', 0x1) - return True - - return False - - def sendCommands(self, payload, opcode): - if opcode == 0x1: - payload = payload.encode("utf-8") - - length = len(payload) - frame_header = struct.pack("B", (1 << 7 | 0 << 6 | 0 << 5 | 0 << 4 | opcode)) - - if length < 0x7d: - frame_header += struct.pack("B", (1 << 7 | length)) - elif length < 1 << 16: # LENGTH_16 - frame_header += struct.pack("B", (1 << 7 | 0x7e)) - frame_header += struct.pack("!H", length) - else: - frame_header += struct.pack("B", (1 << 7 | 0x7f)) - frame_header += struct.pack("!Q", length) - - mask_key = os.urandom(4) - data = frame_header + mask_key + maskData(mask_key, payload) - ServerOnline = True - - if self.sock: - while data: - try: - l = self.sock.send(data) - data = data[l:] - except Exception as error: - xbmc.log(f"Emby.hooks.websocket: {error}", 3) # LOGERROR - ServerOnline = False - break - else: - ServerOnline = False - - return ServerOnline - - def ping(self): - xbmc.log("Emby.hooks.websocket: THREAD: --->[ ping ]", 0) # LOGDEBUG - - while True: - # Check Kodi shutdown - if utils.sleep(10) or self.stop: - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ ping ] shutdown", 0) # LOGDEBUG - return - - if not self.sendCommands(b"", 0x9): - break # Server offline - - self.connect() - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ ping ]", 0) # LOGDEBUG - - def recv(self): - while True: - # Header - if self._frame_header is None: - self._frame_header = self._recv_strict(2) - - if not self._frame_header: # connection closed - return None - - b1 = self._frame_header[0] - b2 = self._frame_header[1] - fin = b1 >> 7 & 1 - opcode = b1 & 0xf - has_mask = b2 >> 7 & 1 - - # Frame length - if self._frame_length is None: - length_bits = b2 & 0x7f - - if length_bits == 0x7e: - length_data = self._recv_strict(2) - self._frame_length = struct.unpack("!H", length_data)[0] - elif length_bits == 0x7f: - length_data = self._recv_strict(8) - self._frame_length = struct.unpack("!Q", length_data)[0] - else: - self._frame_length = length_bits - - # Mask - if self._frame_mask is None: - self._frame_mask = self._recv_strict(4) if has_mask else "" - - # Payload - if self._frame_length: - payload = self._recv_strict(self._frame_length) - else: - payload = b'' - - if has_mask: - payload = maskData(self._frame_mask, payload) - - # Reset for next frame - Debug_frame_header = self._frame_header - Debug_frame_length = self._frame_length - Debug_frame_mask = self._frame_mask - self._frame_header = None - self._frame_length = None - self._frame_mask = None - - if opcode in (0x2, 0x1, 0x0): - if self._cont_data: - self._cont_data[1] += payload - else: - self._cont_data = [opcode, payload] - - if fin: - data = self._cont_data - self._cont_data = None - return data - elif opcode == 0x8: - self.sendCommands(struct.pack('!H', 1000), 0x8) - return False - elif opcode == 0x9: - self.sendCommands(payload, 0xa) # Pong - return False - elif opcode == 0xa: - return False - else: - xbmc.log(f"Emby.hooks.websocket: Uncovered opcode: {opcode} / {payload} / {Debug_frame_header} / {Debug_frame_length} / {Debug_frame_mask}", 3) # LOGERROR - return False - - def _recv_strict(self, bufsize): - shortage = bufsize - sum(len(x) for x in self._recv_buffer) - - while shortage > 0: - try: - bytesData = self.sock.recv(shortage) - except Exception as Error: - xbmc.log(f"Emby.hooks.websocket: Websocket issue: {Error}", 0) # LOGDEBUG - return None - - self._recv_buffer += (bytesData,) - shortage -= len(bytesData) - - unified = b"".join(self._recv_buffer) - - if shortage == 0: - self._recv_buffer = () - return unified - - self._recv_buffer = unified[bufsize:] - return unified[:bufsize] - - def on_message(self): # threaded - xbmc.log("Emby.hooks.websocket: THREAD: --->[ message ]", 0) # LOGDEBUG - - while True: - IncomingData = self.AsyncMessageQueue.get() + IncomingData = self.MessageQueue.get() if IncomingData == "QUIT": xbmc.log("EMBY.hooks.websocket: Queue closed", 1) # LOGINFO break try: - IncomingData = IncomingData.decode('utf-8') - xbmc.log(f"Emby.hooks.websocket: Incoming data: {IncomingData}", 0) # LOGDEBUG + xbmc.log(f"EMBY.hooks.websocket: Incoming data: {IncomingData}", 0) # LOGDEBUG IncomingData = json.loads(IncomingData) except Exception as Error: # connection interrupted and data corrupted - xbmc.log(f"Emby.hooks.websocket: Incoming data: {IncomingData} / {Error}", 3) # LOGERROR + xbmc.log(f"EMBY.hooks.websocket: Incoming data: {IncomingData} / {Error}", 3) # LOGERROR continue if IncomingData['MessageType'] == 'GeneralCommand': @@ -404,7 +43,7 @@ def on_message(self): # threaded if IncomingData['Data']['Name'] == 'DisplayMessage': if IncomingData['Data']['Arguments']['Header'] == "remotecommand": - xbmc.log(f"Emby.hooks.websocket: Incoming remote command: {Text}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.websocket: Incoming remote command: {Text}", 1) # LOGINFO Command = Text.split("|") Event = Command[0].lower() @@ -481,11 +120,16 @@ def on_message(self): # threaded xbmc.executebuiltin('Action(VolumeDown)') elif IncomingData['MessageType'] == 'ScheduledTasksInfo': for Task in IncomingData['Data']: + xbmc.log(f"EMBY.emby.emby: Task update: {Task['Name']} / {Task['State']}", 0) # LOGDEBUG + + if not Task['Name'].lower().startswith("scan"): + continue + if Task["State"] == "Running": if Task.get("Key", "") == "RefreshGuide": self.EPGRefresh = True - if not Task["Name"] in self.Tasks: + if Task["Name"] not in self.Tasks: self.Tasks[Task["Name"]] = True if utils.busyMsg: @@ -532,16 +176,16 @@ def on_message(self): # threaded if utils.busyMsg and "RefreshProgress" in self.ProgressBar and self.ProgressBar["RefreshProgress"][1] == "Loaded": self.ProgressBar["RefreshProgress"][0].update(int(float(IncomingData['Data']['Progress'])), utils.Translate(33199), utils.Translate(33414)) elif IncomingData['MessageType'] == 'UserDataChanged': - xbmc.log(f"Emby.hooks.websocket: [ UserDataChanged ] {IncomingData['Data']['UserDataList']}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.websocket: [ UserDataChanged ] {IncomingData['Data']['UserDataList']}", 1) # LOGINFO UpdateData = () RemoveSkippedItems = () if IncomingData['Data']['UserId'] != self.EmbyServer.ServerData['UserId']: - xbmc.log(f"Emby.hooks.websocket: UserDataChanged skip by wrong UserId: {IncomingData['Data']['UserId']}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.websocket: UserDataChanged skip by wrong UserId: {IncomingData['Data']['UserId']}", 0) # LOGDEBUG continue if playerops.RemoteMode: - xbmc.log("Emby.hooks.websocket: UserDataChanged skip by RemoteMode", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: UserDataChanged skip by RemoteMode", 1) # LOGINFO continue embydb = dbio.DBOpenRO(self.EmbyServer.ServerData['ServerId'], "UserDataChanged") @@ -566,9 +210,9 @@ def on_message(self): # threaded if EpisodeEmbyPresentationKey not in ItemSkipUpdateEmbyPresentationKeys: UpdateData += (ItemData,) else: - xbmc.log(f"Emby.hooks.websocket: UserDataChanged skip by ItemSkipUpdate ancestors / Id: {ItemData['ItemId']} / ItemSkipUpdate: {utils.ItemSkipUpdate}", 0) # DEBUGINFO + xbmc.log(f"EMBY.hooks.websocket: UserDataChanged skip by ItemSkipUpdate ancestors / Id: {ItemData['ItemId']} / ItemSkipUpdate: {utils.ItemSkipUpdate}", 1) # LOGINFO else: - xbmc.log(f"Emby.hooks.websocket: UserDataChanged skip by ItemSkipUpdate / Id: {ItemData['ItemId']} / ItemSkipUpdate: {utils.ItemSkipUpdate}", 0) # DEBUGINFO + xbmc.log(f"EMBY.hooks.websocket: UserDataChanged skip by ItemSkipUpdate / Id: {ItemData['ItemId']} / ItemSkipUpdate: {utils.ItemSkipUpdate}", 1) # LOGINFO RemoveSkippedItems += (ItemData['ItemId'],) dbio.DBCloseRO(self.EmbyServer.ServerData['ServerId'], "UserDataChanged") @@ -579,10 +223,10 @@ def on_message(self): # threaded if UpdateData: self.EmbyServer.library.userdata(UpdateData) elif IncomingData['MessageType'] == 'LibraryChanged': - xbmc.log(f"Emby.hooks.websocket: [ LibraryChanged ] {IncomingData['Data']}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.websocket: [ LibraryChanged ] {IncomingData['Data']}", 1) # LOGINFO if playerops.RemoteMode: - xbmc.log("Emby.hooks.websocket: LibraryChanged skip by RemoteMode", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: LibraryChanged skip by RemoteMode", 1) # LOGINFO continue self.EmbyServer.library.removed(IncomingData['Data']['ItemsRemoved']) @@ -596,11 +240,11 @@ def on_message(self): # threaded self.EmbyServer.library.updated(UpdateItemIds) if self.EmbyServerSyncCheckRunning: - xbmc.log("Emby.hooks.websocket: Emby server sync in progress, delay updates", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: Emby server sync in progress, delay updates", 1) # LOGINFO else: self.EmbyServer.library.RunJobs() elif IncomingData['MessageType'] == 'ServerRestarting': - xbmc.log("Emby.hooks.websocket: [ ServerRestarting ]", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: [ ServerRestarting ]", 1) # LOGINFO self.close_EmbyServerBusy() if utils.restartMsg: @@ -608,12 +252,12 @@ def on_message(self): # threaded self.EmbyServer.ServerReconnect() elif IncomingData['MessageType'] == 'ServerShuttingDown': - xbmc.log("Emby.hooks.websocket: [ ServerShuttingDown ]", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: [ ServerShuttingDown ]", 1) # LOGINFO self.close_EmbyServerBusy() utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33236), time=utils.newContentTime) self.EmbyServer.ServerReconnect() elif IncomingData['MessageType'] == 'RestartRequired': - xbmc.log("Emby.hooks.websocket: [ RestartRequired ]", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: [ RestartRequired ]", 1) # LOGINFO utils.Dialog.notification(heading=utils.addon_name, message=utils.Translate(33237), time=utils.newContentTime) elif IncomingData['MessageType'] == 'Play': playerops.PlayEmby(IncomingData['Data']['ItemIds'], IncomingData['Data']['PlayCommand'], int(IncomingData['Data'].get('StartIndex', 0)), int(IncomingData['Data'].get('StartPositionTicks', -1)), self.EmbyServer, 0) @@ -636,12 +280,12 @@ def on_message(self): # threaded elif IncomingData['Data']['Command'] == "PreviousTrack": playerops.Previous() - xbmc.log(f"Emby.hooks.websocket: command: {IncomingData['Data']['Command']} / PlayedId: {playerops.PlayerId}", 1) # LOGINFO + xbmc.log(f"EMBY.hooks.websocket: command: {IncomingData['Data']['Command']} / PlayedId: {playerops.PlayerId}", 1) # LOGINFO - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ message ]", 0) # LOGDEBUG + xbmc.log("EMBY.hooks.websocket: THREAD: ---<[ message ]", 0) # LOGDEBUG def EmbyServerSyncCheck(self): - xbmc.log("Emby.hooks.websocket: THREAD: --->[ Emby server is busy, sync in progress ]", 1) # LOGINFO + xbmc.log("EMBY.hooks.websocket: THREAD: --->[ Emby server is busy, sync in progress ]", 1) # LOGINFO utils.SyncPause[f"server_busy_{self.EmbyServer.ServerData['ServerId']}"] = True Compare = [False] * len(self.Tasks) @@ -660,8 +304,7 @@ def EmbyServerSyncCheck(self): self.EmbyServer.library.SyncLiveTVEPG() self.EPGRefresh = False - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ Emby server is busy, sync in progress ]", 1) # LOGINFO - + xbmc.log("EMBY.hooks.websocket: THREAD: ---<[ Emby server is busy, sync in progress ]", 1) # LOGINFO def close_EmbyServerBusy(self): if utils.busyMsg: @@ -672,6 +315,10 @@ def close_EmbyServerBusy(self): self.ProgressBar["RefreshProgress"][0].close() del self.ProgressBar["RefreshProgress"] + for TaskId, TaskActive in list(self.Tasks.items()): + if TaskActive: + self.ProgressBar[TaskId].close() + self.Tasks = {} self.RefreshProgressRunning = False self.RefreshProgressInit = False @@ -679,7 +326,7 @@ def close_EmbyServerBusy(self): utils.SyncPause[f"server_busy_{self.EmbyServer.ServerData['ServerId']}"] = False def confirm_remote(self, SessionId, Timeout): # threaded - xbmc.log("Emby.hooks.websocket: THREAD: --->[ Remote confirm ]", 0) # LOGDEBUG + xbmc.log("EMBY.hooks.websocket: THREAD: --->[ Remote confirm ]", 0) # LOGDEBUG self.EmbyServer.API.send_text_msg(SessionId, "remotecommand", f"support|{self.EmbyServer.EmbySession[0]['Id']}", True) if utils.remotecontrol_auto_ack: @@ -690,4 +337,4 @@ def confirm_remote(self, SessionId, Timeout): # threaded if Ack: # send confirm msg self.EmbyServer.API.send_text_msg(SessionId, "remotecommand", f"ack|{self.EmbyServer.EmbySession[0]['Id']}|{self.EmbyServer.EmbySession[0]['DeviceName']}|{self.EmbyServer.EmbySession[0]['UserName']}", True) - xbmc.log("Emby.hooks.websocket: THREAD: ---<[ Remote confirm ]", 0) # LOGDEBUG + xbmc.log("EMBY.hooks.websocket: THREAD: ---<[ Remote confirm ]", 0) # LOGDEBUG diff --git a/resources/iptvsimple.xml b/resources/iptvsimple.xml index 6e2387f6f..f58cb7da4 100644 --- a/resources/iptvsimple.xml +++ b/resources/iptvsimple.xml @@ -2,7 +2,7 @@ multimedia true 0 - special://profile/addon_data/plugin.video.emby-next-gen/temp/SERVERID-livetv.m3u + special://profile/addon_data/plugin.service.emby-next-gen/temp/SERVERID-livetv.m3u true 1 @@ -32,7 +32,7 @@ special://userdata/addon_data/pvr.iptvsimple/channelGroups/customRadioGroups-example.xml false 0 - special://profile/addon_data/plugin.video.emby-next-gen/temp/SERVERID-livetvepg.xml + special://profile/addon_data/plugin.service.emby-next-gen/temp/SERVERID-livetvepg.xml true 0 diff --git a/resources/settings.xml b/resources/settings.xml index 1a28aee30..67009892f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,10 +1,10 @@ -
+
- NotifyAll(plugin.video.emby-next-gen,manageserver) + NotifyAll(plugin.service.emby-next-gen,manageserver) 0 @@ -14,7 +14,7 @@ - NotifyAll(plugin.video.emby-next-gen,factoryreset) + NotifyAll(plugin.service.emby-next-gen,factoryreset) 0 @@ -72,12 +72,12 @@ - NotifyAll(plugin.video.emby-next-gen,managelibsselection) + NotifyAll(plugin.service.emby-next-gen,managelibsselection) 0 - NotifyAll(plugin.video.emby-next-gen,texturecache) + NotifyAll(plugin.service.emby-next-gen,texturecache) 0 @@ -128,7 +128,7 @@ 2 - special://profile/addon_data/plugin.video.emby-next-gen/ + special://profile/addon_data/plugin.service.emby-next-gen/ true true @@ -138,17 +138,17 @@ - NotifyAll(plugin.video.emby-next-gen,downloadreset) + NotifyAll(plugin.service.emby-next-gen,downloadreset) 2 - NotifyAll(plugin.video.emby-next-gen,nodesreset) + NotifyAll(plugin.service.emby-next-gen,nodesreset) 3 - NotifyAll(plugin.video.emby-next-gen,databasereset) + NotifyAll(plugin.service.emby-next-gen,databasereset) 0 @@ -1433,7 +1433,7 @@ - NotifyAll(plugin.video.emby-next-gen,skinreload) + NotifyAll(plugin.service.emby-next-gen,skinreload) 0 @@ -1520,7 +1520,7 @@ - NotifyAll(plugin.video.emby-next-gen,databasevacuummanual) + NotifyAll(plugin.service.emby-next-gen,databasevacuummanual) 3 @@ -1538,7 +1538,7 @@ - NotifyAll(plugin.video.emby-next-gen,backup) + NotifyAll(plugin.service.emby-next-gen,backup) 2 @@ -1548,7 +1548,7 @@ - NotifyAll(plugin.video.emby-next-gen,restore) + NotifyAll(plugin.service.emby-next-gen,restore) 2 diff --git a/resources/skins/default/1080i/SkipCreditsDialog.xml b/resources/skins/default/1080i/SkipCreditsDialog.xml index f9c3794c8..d676b04c2 100644 --- a/resources/skins/default/1080i/SkipCreditsDialog.xml +++ b/resources/skins/default/1080i/SkipCreditsDialog.xml @@ -20,7 +20,7 @@ 98% 7% true - + center white.png white.png diff --git a/resources/skins/default/1080i/SkipIntroDialog.xml b/resources/skins/default/1080i/SkipIntroDialog.xml index b3cd50cf4..abe82326e 100644 --- a/resources/skins/default/1080i/SkipIntroDialog.xml +++ b/resources/skins/default/1080i/SkipIntroDialog.xml @@ -20,7 +20,7 @@ 98% 7% true - + center white.png white.png diff --git a/resources/skins/default/1080i/SkipIntroDialogEmbuary.xml b/resources/skins/default/1080i/SkipIntroDialogEmbuary.xml index c0e477b84..15df5794c 100644 --- a/resources/skins/default/1080i/SkipIntroDialogEmbuary.xml +++ b/resources/skins/default/1080i/SkipIntroDialogEmbuary.xml @@ -20,7 +20,7 @@ 14% 5% true - + diff --git a/resources/skins/default/1080i/script-emby-connect-login-manual.xml b/resources/skins/default/1080i/script-emby-connect-login-manual.xml index 22461dfb3..2f1eb5a5c 100644 --- a/resources/skins/default/1080i/script-emby-connect-login-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-login-manual.xml @@ -57,12 +57,12 @@ font13 white 66000000 - + 110 - + ffe1e1e1 66000000 font12 @@ -83,7 +83,7 @@ 110 - + ffe1e1e1 66000000 font12 @@ -103,7 +103,7 @@ - + 426 65 font13 @@ -121,7 +121,7 @@ Conditional - + 426 65 font13 diff --git a/resources/skins/default/1080i/script-emby-connect-login.xml b/resources/skins/default/1080i/script-emby-connect-login.xml index 47247105a..5c61754aa 100644 --- a/resources/skins/default/1080i/script-emby-connect-login.xml +++ b/resources/skins/default/1080i/script-emby-connect-login.xml @@ -56,12 +56,12 @@ font13 white 66000000 - + 110 - + ffe1e1e1 66000000 font12 @@ -82,7 +82,7 @@ 110 - + ffe1e1e1 66000000 font12 @@ -102,7 +102,7 @@ - + 426 65 font13 @@ -119,7 +119,7 @@ Conditional - + 426 65 font13 @@ -141,7 +141,7 @@ - + font_flag ff464646 66000000 @@ -165,7 +165,7 @@ 135 center - + font_flag true FF52b54b diff --git a/resources/skins/default/1080i/script-emby-connect-server-manual.xml b/resources/skins/default/1080i/script-emby-connect-server-manual.xml index b329f62cd..d2bc679e2 100644 --- a/resources/skins/default/1080i/script-emby-connect-server-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-server-manual.xml @@ -57,12 +57,12 @@ font13 white 66000000 - + 110 - + ffe1e1e1 66000000 font12 @@ -83,7 +83,7 @@ 110 - + ffe1e1e1 66000000 font12 @@ -103,7 +103,7 @@ - + 426 65 font13 @@ -120,7 +120,7 @@ Conditional - + 426 65 font13 diff --git a/resources/skins/default/1080i/script-emby-connect-server.xml b/resources/skins/default/1080i/script-emby-connect-server.xml index b08f520aa..70a103d3f 100644 --- a/resources/skins/default/1080i/script-emby-connect-server.xml +++ b/resources/skins/default/1080i/script-emby-connect-server.xml @@ -64,7 +64,7 @@ font13 white 66000000 - + 200 @@ -161,7 +161,7 @@ 20 - + 476 65 font13 @@ -179,7 +179,7 @@ Conditional - + 476 65 font13 @@ -197,7 +197,7 @@ Conditional - + 476 65 font13 diff --git a/resources/skins/default/1080i/script-emby-connect-users.xml b/resources/skins/default/1080i/script-emby-connect-users.xml index bfdb14ca3..62b22e40d 100644 --- a/resources/skins/default/1080i/script-emby-connect-users.xml +++ b/resources/skins/default/1080i/script-emby-connect-users.xml @@ -57,7 +57,7 @@ font13 white 66000000 - + Conditional @@ -175,7 +175,7 @@ - + 874 65 font13 @@ -193,7 +193,7 @@ Conditional - + 874 65 font13 diff --git a/service.py b/service.py index dc990be66..30f3d38b3 100644 --- a/service.py +++ b/service.py @@ -1,6 +1,31 @@ -from hooks import webservice -webservice.start() +import sys +Param = sys.argv[1:] -if __name__ == "__main__": - import hooks.monitor - hooks.monitor.StartUp() +if not Param: + from hooks import webservice + + if webservice.start(): + import hooks.monitor + hooks.monitor.StartUp() +else: + import _socket + Argv = ';'.join(["service"] + Param) + DataSend, XbmcMonitor, sock = f"EVENT {Argv}".encode('utf-8'), None, _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) + sock.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1) + sock.settimeout(None) + + for _ in range(60): # 60 seconds timeout + try: + sock.connect(('127.0.0.1', 57342)) + sock.send(DataSend) + sock.recv(1024) + sock.close() + break + except: + if not XbmcMonitor: + import xbmc + XbmcMonitor = xbmc.Monitor() + + if XbmcMonitor.waitForAbort(0.1): + break