diff --git a/addon.xml b/addon.xml index a275436c9..7f685113f 100644 --- a/addon.xml +++ b/addon.xml @@ -1,12 +1,13 @@ - + - video audio image + audio video image + @@ -28,6 +29,10 @@ resources/icon.png resources/fanart.jpg + resources/screenshot1.jpg + resources/screenshot2.jpg + resources/screenshot3.jpg + resources/screenshot4.jpg diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 000000000..d9ae41833 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,300 @@ +5.2.07 (beta) +============= + +Fixes: +------ +Connection when server is offline on Kodi start +Kodi 18 unicode issues fixed +remove episode fixed +TV Shows repair sync fixed +music sync (single) issue +Trakt dublette issue +timeout advancedsettings issue +readded and optimized nextepisode node +fix subtitle preselection +re-add favourites +fix sources.xml +reduced skinreloads to a minimum +fix Kodi restart issues +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +User login issue (no admin privileges) +delete item (Context menu) +add user +taglines bugfix +hibernate mode +Login issues +Userlogin without password +backdrops +progress updates + + +New: +---- +Delete Show/Season after watched +New content notifications +Static nodes opimized +Dynamic nodes opimized +Subtitle enabled/disabled default settings based on guisettings.xml +iso, bdmw support in addon mode (fallback to native mode) + + +Known issues: +------------- +Multiserver not working +Changelog not working +1970 issue (Windows only) + + +Feature requests: +----------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Add Playcount option for musicvideos + + +5.2.06 (beta) +============= + +Fixes: +------ +sleep/hibernate +backup +sync issues +New content Notification +Threading limtation +remove items while sync +update TV-Show season +Musicvideo realtime sync issue (no Presentation Key) + +New: +---- +alphabet/first letter nodes +Icon(s) in rootfolder +Remove artwork optional when DB reset +Boost initial sync + +Known issues: +------------- +Loghandler mask not working +Multiserver not working +Dynamic lists slow/artwork +Distributed TV-Shows not synced correctly +Review Artwork/Backdrops +Changelog not working +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +Trakt issue +1970 issue (Windows only) +User login issue (no admin privileges) +delete item (Context menu) fixed +add user fixed +taglines fixed +hibernate fixed +sync performance +Login fixes (to be tested) +Dynamic list performance +Subtitle enabled/disabled should now work based on settings +Userlogin with no passwords works +ISO (to be tested) -> DB reset required +backdrops fixed +progress updates fixed + +Feature requests: +----------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Boost Repair sync +New content notifications +Add Playcount option for musicvideos +Delete Show/Season after finished + + +5.2.05 (beta) +============= + +Fixes: +------ +sleep/hibernate +backup +sync issues +New content Notification +Threading limtation +remove items while sync +update TV-Show season +Musicvideo realtime sync issue (no Presentation Key) + +New: +---- +alphabet/first letter nodes +Icon(s) in rootfolder +Remove artwork optional when DB reset +Boost initial sync + +Known issues: +------------- +Loghandler mask not working +Multiserver not working +Dynamic lists slow/artwork +Distributed TV-Shows not synced correctly +Review Artwork/Backdrops +Changelog not working +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +Trakt issue +1970 issue (Windows only) +User login issue (no admin privileges) + +Feature requests: +----------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Boost Repair sync +New content notifications +Add Playcount option for musicvideos +Delete Show/Season after finished + + +5.2.04 (beta) +============= + +Fixes: +------ +sleep/hibernate +backup +sync issues +New content Notification +Threading limtation +remove items while sync +update TV-Show season + +New: +---- +alphabet/first letter nodes +Icon(s) in rootfolder +Remove artwork optional when DB reset +Boost initial sync + +Known issues: +------------- +Loghandler mask not working +Multiserver not working +Dynamic lists slow/artwork +Distributed TV-Shows not synced correctly +Review Artwork/Backdrops +Changelog not working +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +Trakt issue +1970 issue (Windows only) +User login issue (no admin privileges) + +Feature requests: +----------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Boost Repair sync +New content notifications +Add Playcount option for musicvideos +Delete Show/Season after finished + + +5.2.03 (beta) +============= + +Fixes: +------ +sleep/hibernate +backup +sync issues +New content Notification +Threading limtation + +New: +---- +alphabet/first letter nodes +Icon(s) in rootfolder +Remove artwork optional when DB reset +Boost initial sync + +Known issues: +------------- +Loghandler mask not working +Multiserver not working +Dynamic lists slow/artwork +Distributed TV-Shows not synced correctly +Review Artwork/Backdrops +Changelog not working +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +Trakt issue +1970 issue (Windows only) +User login issue (no admin privileges) + +Feature requests (pending): +--------------------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Boost Repair sync +New content notifications +Add Playcount option for musicvideos +Delete Show/Season after finished + + +5.2.02 (beta) +============= + +Fixes: +------ +sleep/hibernate fixed +backup fixed +sync issues fixed + +New: +---- +alphabet/first letter nodes +Icon(s) in rootfolder +Remove artwork optional when DB reset +Boost initial sync + +Known issues: +------------- +Loghandler mask not working +Multiserver not working +Dynamic lists slow/artwork +Distributed TV-Shows not synced correctly +Review Artwork/Backdrops +Changelog not working +Crash if Emby server is not up on Kodi startup +Crash on Emby Server shutdown +Trakt issue +1970 issue (Windows only) +User login issue (no admin privileges) + +Feature requests (pending): +--------------------------- +PVR support +Remove TV-Shows etc after fully watched +Audiobook support +Autodetect Speed for Transcoding +Improve Subtitle selection +Boost Repair sync +New content notifications +Add Playcount option for musicvideos +Delete Show/Season after finished + diff --git a/context.py b/context.py index 8b44bb6d1..33ca86f59 100644 --- a/context.py +++ b/context.py @@ -1,165 +1,8 @@ +# -*- coding: utf-8 -*- import json -import logging import xbmc -import xbmcaddon - -import database.database -import dialogs.context -import emby.main -import helper.translate -import helper.utils -import helper.loghandler - -class Context(): - def __init__(self, play=False, transcode=False, delete=False): - helper.loghandler.reset() - helper.loghandler.config() - self.LOG = logging.getLogger("EMBY.context.Context") - self._selected_option = None - self.Utils = helper.utils.Utils() - self.server_id = None - self.kodi_id = None - self.media = None - self.XML_PATH = (xbmcaddon.Addon('plugin.video.emby-next-gen').getAddonInfo('path'), "default", "1080i") - self.OPTIONS = { - 'Refresh': helper.translate._(30410), - 'Delete': helper.translate._(30409), - 'Addon': helper.translate._(30408), - 'AddFav': helper.translate._(30405), - 'RemoveFav': helper.translate._(30406), - 'Transcode': helper.translate._(30412) - } - -# try: -# self.kodi_id = max(sys.listitem.getVideoInfoTag().getDbId(), 0) or max(sys.listitem.getMusicInfoTag().getDbId(), 0) or None -# self.media = self.get_media_type() -# self.server_id = sys.listitem.getProperty('embyserver') or None -# item_id = sys.listitem.getProperty('embyid') -# except AttributeError: - if xbmc.getInfoLabel('ListItem.Property(embyid)'): - item_id = xbmc.getInfoLabel('ListItem.Property(embyid)') - else: - self.kodi_id = xbmc.getInfoLabel('ListItem.DBID') - self.media = xbmc.getInfoLabel('ListItem.DBTYPE') - item_id = None - - ServerOnline = False - - for i in range(60): - if self.Utils.window('emby_online.bool'): - ServerOnline = True - break - - xbmc.sleep(500) - - if not ServerOnline: - helper.loghandler.reset() - return - - #Load server connection data - self.server = emby.main.Emby(self.server_id).get_client() - emby.main.Emby().set_state(self.Utils.window('emby.server.state.json')) - - for server in self.Utils.window('emby.server.states.json') or []: - emby.main.Emby(server).set_state(self.Utils.window('emby.server.%s.state.json' % server)) - - if item_id: - self.item = self.server['api'].get_item(item_id) - else: - self.item = self.get_item_id() - - if self.item: - if delete: - self.delete_item() - - elif self.select_menu(): - self.action_menu() - - #Get media type based on sys.listitem. If unfilled, base on visible window -# def get_media_type(self): -# media = sys.listitem.getVideoInfoTag().getMediaType() or sys.listitem.getMusicInfoTag().getMediaType() - -# if not media: -# if xbmc.getCondVisibility('Container.Content(albums)'): -# media = "album" -# elif xbmc.getCondVisibility('Container.Content(artists)'): -# media = "artist" -# elif xbmc.getCondVisibility('Container.Content(songs)'): -# media = "song" -# elif xbmc.getCondVisibility('Container.Content(pictures)'): -# media = "picture" -# else: -# self.LOG.info("media is unknown") - -# return media.decode('utf-8') - - #Get synced item from embydb - def get_item_id(self): - item = database.database.get_item(self.kodi_id, self.media) - - if not item: - return {} - - return { - 'Id': item[0], - 'UserData': json.loads(item[4]) if item[4] else {}, - 'Type': item[3] - } - - #Display the select dialog. - #Favorites, Refresh, Delete (opt), Settings. - def select_menu(self): - options = [] - - if self.item['Type'] not in 'Season': - if self.item['UserData'].get('IsFavorite'): - options.append(self.OPTIONS['RemoveFav']) - else: - options.append(self.OPTIONS['AddFav']) - - options.append(self.OPTIONS['Refresh']) - - if self.Utils.settings('enableContextDelete.bool'): - options.append(self.OPTIONS['Delete']) - - options.append(self.OPTIONS['Addon']) - context_menu = dialogs.context.ContextMenu("script-emby-context.xml", *self.XML_PATH) - context_menu.set_options(options) - context_menu.doModal() - - if context_menu.is_selected(): - self._selected_option = context_menu.get_selected() - - return self._selected_option - - def action_menu(self): - selected = self.Utils.StringDecode(self._selected_option) - - if selected == self.OPTIONS['Refresh']: - self.server['api'].refresh_item(self.item['Id']) - - elif selected == self.OPTIONS['AddFav']: - self.server['api'].favorite(self.item['Id'], True) - - elif selected == self.OPTIONS['RemoveFav']: - self.server['api'].favorite(self.item['Id'], False) - - elif selected == self.OPTIONS['Addon']: - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') - - elif selected == self.OPTIONS['Delete']: - self.delete_item() - - def delete_item(self): - delete = True - - if not self.Utils.settings('skipContextMenu.bool'): - if not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33015)): - delete = False - - if delete: - self.server['api'].delete_item(self.item['Id']) - self.Utils.event("LibraryChanged", {'ItemsRemoved': [self.item['Id']], 'ItemsVerify': [self.item['Id']], 'ItemsUpdated': [], 'ItemsAdded': []}) if __name__ == "__main__": - Context() + data = {} + data = '"[%s]"' % json.dumps(data).replace('"', '\\"') + xbmc.executebuiltin('NotifyAll(plugin.video.emby-next-gen, %s, %s)' % ("context", data)) diff --git a/core/artwork.py b/core/artwork.py index e19e37eb2..ac13e02b6 100644 --- a/core/artwork.py +++ b/core/artwork.py @@ -1,20 +1,17 @@ # -*- coding: utf-8 -*- -import logging import os +import threading try: from urllib import urlencode except: from urllib.parse import urlencode -import threading -import requests import xbmcgui import xbmcvfs -import helper.translate import database.database -import emby.main +import helper.loghandler from . import queries_videos from . import queries_music from . import queries_texture @@ -25,11 +22,10 @@ def __init__(self, cursor, Utils): cursor.execute("PRAGMA database_list;") self.is_music = 'MyMusic' in cursor.fetchall()[0][2] - self.LOG = logging.getLogger("EMBY.core.artwork.Artwork") + self.LOG = helper.loghandler.LOG('EMBY.core.artwork.Artwork') self.Utils = Utils self.cursor = cursor self.threads = [] - self.server = emby.main.Emby() self.CacheAllEntriesThread = None #Update artwork in the video database. @@ -40,21 +36,21 @@ def update(self, image_url, kodi_id, media, image): if image == 'poster' and media in ('song', 'artist', 'album'): return - try: - self.cursor.execute(queries_videos.get_art, (kodi_id, media, image,)) - url = self.cursor.fetchone()[0] - except TypeError: - self.LOG.debug("ADD to kodi_id %s art: %s", kodi_id, image_url) - self.cursor.execute(queries_videos.add_art, (kodi_id, media, image, image_url)) - else: - if url != image_url: - self.delete_cache(url) + self.cursor.execute(queries_videos.get_art, (kodi_id, media, image,)) + result = self.cursor.fetchone() - if not image_url: - return + if result: + url = result[0] - self.LOG.info("UPDATE to kodi_id %s art: %s", kodi_id, image_url) - self.cursor.execute(queries_videos.update_art, (image_url, kodi_id, media, image)) + if url != image_url: + self.delete_cache(url, False) + + if image_url: + self.LOG.info("UPDATE to kodi_id %s art: %s" % (kodi_id, image_url)) + self.cursor.execute(queries_videos.update_art, (image_url, kodi_id, media, image)) + else: + self.LOG.debug("ADD to kodi_id %s art: %s" % (kodi_id, image_url)) + self.cursor.execute(queries_videos.add_art, (kodi_id, media, image, image_url)) #Add all artworks def add(self, artwork, *args): @@ -91,38 +87,39 @@ def delete(self, *args): self.cursor.execute(queries_videos.get_art_url, args) for row in self.cursor.fetchall(): - self.delete_cache(row[0]) + self.delete_cache(row[0], True) #Delete cached artwork - def delete_cache(self, url): - with database.database.Database('texture') as texturedb: + def delete_cache(self, url, CompleteRemove): + with database.database.Database(self.Utils, 'texture', True) as texturedb: cursor = texturedb.cursor + cursor.execute(queries_texture.get_cache, (url,)) + result = cursor.fetchone() - try: - cursor.execute(queries_texture.get_cache, (url,)) - cached = cursor.fetchone()[0] - except TypeError: - self.LOG.debug("Could not find cached url: %s", url) - else: + if result: + cached = result[0] thumbnails = self.Utils.translatePath("special://thumbnails/%s" % cached) xbmcvfs.delete(thumbnails) cursor.execute(queries_texture.delete_cache, (url,)) - if self.is_music: - self.cursor.execute(queries_music.delete_artwork, (url,)) - else: - self.cursor.execute(queries_videos.delete_artwork, (url,)) + if CompleteRemove: + if self.is_music: + self.cursor.execute(queries_music.delete_artwork, (url,)) + else: + self.cursor.execute(queries_videos.delete_artwork, (url,)) - self.LOG.info("DELETE cached %s", cached) + self.LOG.info("DELETE cached %s" % cached) + else: + self.LOG.debug("Could not find cached url: %s" % url) #This method will sync all Kodi artwork to textures13.dband cache them locally. This takes diskspace! def cache_textures(self): - if not self.Utils.set_web_server(): + if not self.Utils.Settings.WebserverData['Enabled']: return self.LOG.info("<[ cache textures ]") - if self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33044)): + if self.Utils.dialog("yesno", heading="{emby}", line1=self.Utils.Translate(33044)): self.delete_all_cache() self._cache_all_video_entries() @@ -134,17 +131,17 @@ def delete_all_cache(self): cache = self.Utils.translatePath('special://thumbnails/') if xbmcvfs.exists(cache): - dirs, ignored = xbmcvfs.listdir(cache) + dirs, _ = xbmcvfs.listdir(cache) for directory in dirs: - ignored, files = xbmcvfs.listdir(os.path.join(cache, directory)) + _, files = xbmcvfs.listdir(os.path.join(cache, directory)) for Filename in files: cached = os.path.join(cache, directory, Filename) xbmcvfs.delete(cached) - self.LOG.debug("DELETE cached %s", cached) + self.LOG.debug("DELETE cached %s" % cached) - with database.database.Database('texture') as kodidb: + with database.database.Database(self.Utils, 'texture', True) as kodidb: kodidb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") for table in kodidb.cursor.fetchall(): @@ -155,7 +152,7 @@ def delete_all_cache(self): #Cache all artwork from video db. Don't include actors def _cache_all_video_entries(self): - with database.database.Database('video') as kodidb: + with database.database.Database(self.Utils, 'video', True) as kodidb: kodidb.cursor.execute(queries_videos.get_artwork) urls = kodidb.cursor.fetchall() @@ -164,7 +161,7 @@ def _cache_all_video_entries(self): #Cache all artwork from music db def _cache_all_music_entries(self): - with database.database.Database('music') as kodidb: + with database.database.Database(self.Utils, 'music', True) as kodidb: kodidb.cursor.execute(queries_music.get_artwork) urls = kodidb.cursor.fetchall() @@ -177,22 +174,20 @@ def __init__(self, urls, Label, Utils): self.urls = urls self.Label = Label self.progress_updates = xbmcgui.DialogProgressBG() - self.progress_updates.create(helper.translate._('addon_name'), helper.translate._(33045)) + self.progress_updates.create(self.Utils.Translate('addon_name'), self.Utils.Translate(33045)) threading.Thread.__init__(self) #Cache all entries def run(self): - webServerPort = self.Utils.window('webServerPort') - webServerUser = self.Utils.window('webServerUser') - webServerPass = self.Utils.window('webServerPass') + import requests total = len(self.urls) for index, url in enumerate(self.urls): - if self.Utils.window('emby_should_stop.bool'): + if self.Utils.Settings.emby_shouldstop: break Value = int((float(float(index)) / float(total)) * 100) - self.progress_updates.update(Value, message="%s: %s" % (helper.translate._(33045), self.Label + ": " + str(index))) + self.progress_updates.update(Value, message="%s: %s" % (self.Utils.Translate(33045), self.Label + ": " + str(index))) if url[0]: url = urlencode({'blahblahblah': url[0]}) @@ -201,7 +196,7 @@ def run(self): url = url[13:] try: - requests.head("http://127.0.0.1:%s/image/image://%s" % (webServerPort, url), auth=(webServerUser, webServerPass)) + requests.head("http://127.0.0.1:%s/image/image://%s" % (self.Utils.Settings.WebserverData['webServerPort'], url), auth=(self.Utils.Settings.WebserverData['webServerUser'], self.Utils.Settings.WebserverData['webServerPass'])) except: break diff --git a/core/common.py b/core/common.py index 9e82a2c39..3139aa4e6 100644 --- a/core/common.py +++ b/core/common.py @@ -1,22 +1,20 @@ # -*- coding: utf-8 -*- -import logging +import helper.loghandler import database.queries class Common(): - def __init__(self, emby_db, objects, Utils, direct_path, Server): - self.LOG = logging.getLogger("EMBY.core.common.Common") - self.Utils = Utils + def __init__(self, emby_db, objects, EmbyServer): + self.LOG = helper.loghandler.LOG('EMBY.core.common.Common') self.emby_db = emby_db self.objects = objects - self.direct_path = direct_path - self.server = Server + self.EmbyServer = EmbyServer #Add streamdata def Streamdata_add(self, obj, Update): if Update: self.emby_db.remove_item_streaminfos(obj['Id']) - if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + if "3d" in self.EmbyServer.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): if len(obj['Item']['MediaSources']) >= 2: Temp = obj['Item']['MediaSources'][1] obj['Item']['MediaSources'][1] = obj['Item']['MediaSources'][0] @@ -35,7 +33,7 @@ def Streamdata_add(self, obj, Update): CountMediaSubtitle = 0 CountStreamSources = 0 DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') - self.emby_db.add_mediasource(*self.Utils.values(DataSource, database.queries.add_mediasource_obj)) + self.emby_db.add_mediasource(*self.EmbyServer.Utils.values(DataSource, database.queries.add_mediasource_obj)) for DataStream in DataSource['MediaStreams']: DataStream['emby_id'] = obj['Item']['Id'] @@ -45,17 +43,17 @@ def Streamdata_add(self, obj, Update): if DataStream['Type'] == "Video": DataStream = self.objects.MapMissingData(DataStream, 'VideoStreams') DataStream['VideoIndex'] = CountMediaStreamVideo - self.emby_db.add_videostreams(*self.Utils.values(DataStream, database.queries.add_videostreams_obj)) + self.emby_db.add_videostreams(*self.EmbyServer.Utils.values(DataStream, database.queries.add_videostreams_obj)) CountMediaStreamVideo += 1 elif DataStream['Type'] == "Audio": DataStream = self.objects.MapMissingData(DataStream, 'AudioStreams') DataStream['AudioIndex'] = CountMediaStreamAudio - self.emby_db.add_audiostreams(*self.Utils.values(DataStream, database.queries.add_audiostreams_obj)) + self.emby_db.add_audiostreams(*self.EmbyServer.Utils.values(DataStream, database.queries.add_audiostreams_obj)) CountMediaStreamAudio += 1 elif DataStream['Type'] == "Subtitle": DataStream = self.objects.MapMissingData(DataStream, 'Subtitles') DataStream['SubtitleIndex'] = CountMediaSubtitle - self.emby_db.add_subtitles(*self.Utils.values(DataStream, database.queries.add_subtitles_obj)) + self.emby_db.add_subtitles(*self.EmbyServer.Utils.values(DataStream, database.queries.add_subtitles_obj)) CountMediaSubtitle += 1 CountStreamSources += 1 @@ -66,38 +64,43 @@ def Streamdata_add(self, obj, Update): def get_path_filename(self, obj, MediaID): #Native Kodi plugins starts with plugin:// -> If native Kodi plugin, drop the link directly in Kodi DB. Emby server cannot play Kodi-Plugins - KodiPluginPath = False - - if obj['Path'].startswith("plugin://"): - KodiPluginPath = True - - if self.direct_path or KodiPluginPath: - if KodiPluginPath: + ForceNativeMode = False + Plugin = False + Temp = obj['Path'].lower() + + if Temp.startswith("plugin://"): + ForceNativeMode = True + Plugin = True + elif Temp.endswith(".iso") or Temp.endswith(".bdmv"): + ForceNativeMode = True + + if self.EmbyServer.Utils.direct_path or ForceNativeMode: + if Plugin: obj['Filename'] = obj['Path'] else: obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] - obj['Path'] = self.Utils.StringDecode(obj['Path']) - obj['Filename'] = self.Utils.StringDecode(obj['Filename']) + obj['Path'] = self.EmbyServer.Utils.StringDecode(obj['Path']) + obj['Filename'] = self.EmbyServer.Utils.StringDecode(obj['Filename']) - if not self.Utils.validate(obj['Path']): - raise Exception("Failed to validate path. User stopped.") + if not self.EmbyServer.Utils.validate(obj['Path']): + return False, obj obj['Path'] = obj['Path'].replace(obj['Filename'], "") if MediaID == "audio": - return obj + return True, obj #Detect Multipart videos if 'PartCount' in obj['Item']: if (obj['Item']['PartCount']) >= 2: - AdditionalParts = self.server['api'].get_additional_parts(obj['Id']) + AdditionalParts = self.EmbyServer.API.get_additional_parts(obj['Id']) obj['Filename'] = obj['Path'] + obj['Filename'] obj['StackTimes'] = str(obj['Runtime']) for AdditionalItem in AdditionalParts['Items']: AdditionalItem = self.objects.MapMissingData(AdditionalItem, 'MediaSources') - Path = self.Utils.StringDecode(AdditionalItem['Path']) + Path = self.EmbyServer.Utils.StringDecode(AdditionalItem['Path']) obj['Filename'] = obj['Filename'] + " , " + Path RunTimePart = round(float((AdditionalItem['RunTimeTicks'] or 0) / 10000000.0), 6) obj['Runtime'] = obj['Runtime'] + RunTimePart @@ -105,7 +108,7 @@ def get_path_filename(self, obj, MediaID): obj['Filename'] = "stack://" + obj['Filename'] else: - Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(obj['Path']) + Filename = self.EmbyServer.Utils.PathToFilenameReplaceSpecialCharecters(obj['Path']) if MediaID == "tvshows": obj['Path'] = "http://127.0.0.1:57578/tvshows/%s/" % obj['SeriesId'] @@ -114,7 +117,7 @@ def get_path_filename(self, obj, MediaID): obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['Item']['MediaSources'][0]['Id'], obj['Item']['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) except: obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['Item']['MediaSources'][0]['Id'], Filename) - self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + self.LOG.warning("No video bitrate available %s" % self.EmbyServer.Utils.StringMod(obj['Item']['Path'])) elif MediaID == "movies": obj['Path'] = "http://127.0.0.1:57578/movies/%s/" % obj['LibraryId'] @@ -122,7 +125,7 @@ def get_path_filename(self, obj, MediaID): obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['MediaSourceID'], obj['Item']['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) except: obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['MediaSourceID'], Filename) - self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + self.LOG.warning("No video bitrate available %s" % self.EmbyServer.Utils.StringMod(obj['Item']['Path'])) elif MediaID == "musicvideos": obj['Path'] = "http://127.0.0.1:57578/musicvideos/%s/" % obj['LibraryId'] @@ -130,22 +133,22 @@ def get_path_filename(self, obj, MediaID): obj['Filename'] = "%s-%s-%s-stream-%s" % (obj['Id'], obj['PresentationKey'], obj['Streams']['video'][0]['BitRate'], Filename) except: obj['Filename'] = "%s-%s-stream-%s" % (obj['Id'], obj['PresentationKey'], Filename) - self.LOG.warning("No video bitrate available %s", self.Utils.StringMod(obj['Item']['Path'])) + self.LOG.warning("No video bitrate available %s" % self.EmbyServer.Utils.StringMod(obj['Item']['Path'])) elif MediaID == "audio": obj['Path'] = "http://127.0.0.1:57578/audio/%s/" % obj['Id'] obj['Filename'] = "%s-stream-%s" % (obj['Id'], Filename) - return obj + return True, obj #Detect Multipart videos if 'PartCount' in obj['Item']: if (obj['Item']['PartCount']) >= 2: - AdditionalParts = self.server['api'].get_additional_parts(obj['Id']) + AdditionalParts = self.EmbyServer.API.get_additional_parts(obj['Id']) obj['Filename'] = obj['Path'] + obj['Filename'] obj['StackTimes'] = str(obj['Runtime']) for AdditionalItem in AdditionalParts['Items']: AdditionalItem = self.objects.MapMissingData(AdditionalItem, 'MediaSources') - Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(AdditionalItem['Path']) + Filename = self.EmbyServer.Utils.PathToFilenameReplaceSpecialCharecters(AdditionalItem['Path']) try: obj['Filename'] = obj['Filename'] + " , " + obj['Path'] + "%s--%s-stream-%s" % (AdditionalItem['Id'], AdditionalItem['MediaSources'][0]['MediaStreams'][0]['BitRate'], Filename) @@ -158,7 +161,7 @@ def get_path_filename(self, obj, MediaID): obj['Filename'] = "stack://" + obj['Filename'] - return obj + return True, obj def library_check(self, e_item, item, library): if library is None: @@ -166,7 +169,7 @@ def library_check(self, e_item, item, library): view_id = e_item[6] view_name = self.emby_db.get_view_name(view_id) else: - ancestors = self.server['api'].get_ancestors(item['Id']) + ancestors = self.EmbyServer.API.get_ancestors(item['Id']) if not ancestors: if item['Type'] == 'MusicArtist': @@ -195,13 +198,11 @@ def library_check(self, e_item, item, library): break - sync = database.database.get_sync() - if not library: library = {} - if view_id not in [x.replace('Mixed:', "") for x in sync['Whitelist'] + sync['Libraries']]: - self.LOG.info("Library %s is not synced. Skip update.", view_id) + if view_id not in [x.replace('Mixed:', "") for x in self.EmbyServer.Utils.SyncData['Whitelist'] + self.EmbyServer.Utils.SyncData['Libraries']]: + self.LOG.info("Library %s is not synced. Skip update." % view_id) return False library['Id'] = view_id diff --git a/core/kodi.py b/core/kodi.py index e7ce84031..7b3d05864 100644 --- a/core/kodi.py +++ b/core/kodi.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -import logging - -import helper.utils +import helper.loghandler from . import artwork from . import queries_videos class Kodi(): - def __init__(self, cursor): - self.LOG = logging.getLogger("EMBY.core.kodi.Kodi") - self.Utils = helper.utils.Utils() + def __init__(self, cursor, Utils): + self.LOG = helper.loghandler.LOG('EMBY.core.kodi.Kodi') + self.Utils = Utils self.cursor = cursor self.artwork = artwork.Artwork(cursor, self.Utils) @@ -54,11 +52,13 @@ def add_path(self, *args): return path_id def get_path(self, *args): - try: - self.cursor.execute(queries_videos.get_path, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_path, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def update_path(self, *args): self.cursor.execute(queries_videos.update_path, args) @@ -75,10 +75,12 @@ def remove_path(self, *args): self.cursor.execute(queries_videos.delete_path, args) def add_file(self, filename, path_id): - try: - self.cursor.execute(queries_videos.get_file, (path_id, filename,)) - file_id = self.cursor.fetchone()[0] - except TypeError: + self.cursor.execute(queries_videos.get_file, (path_id, filename,)) + Data = self.cursor.fetchone() + + if Data: + file_id = Data[0] + else: file_id = self.create_entry_file() self.cursor.execute(queries_videos.add_file, (file_id, path_id, filename)) @@ -94,11 +96,13 @@ def remove_file(self, path, *args): self.cursor.execute(queries_videos.delete_file_by_path, (path_id,) + args) def get_filename(self, *args): - try: - self.cursor.execute(queries_videos.get_filename, args) - return self.cursor.fetchone()[0] - except TypeError: - return "" + self.cursor.execute(queries_videos.get_filename, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return "" def add_people(self, people, *args): def add_thumbnail(person_id, person, person_type): @@ -138,11 +142,13 @@ def add_person(self, *args): return person_id def get_person(self, *args): - try: - self.cursor.execute(queries_videos.get_person, args) - return self.cursor.fetchone()[0] - except TypeError: - return self.add_person(*args) + self.cursor.execute(queries_videos.get_person, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return self.add_person(*args) #Delete current genres first for clean slate def add_genres(self, genres, *args): @@ -157,11 +163,13 @@ def add_genre(self, *args): return genre_id def get_genre(self, *args): - try: - self.cursor.execute(queries_videos.get_genre, args) - return self.cursor.fetchone()[0] - except TypeError: - return self.add_genre(*args) + self.cursor.execute(queries_videos.get_genre, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return self.add_genre(*args) def add_studios(self, studios, *args): for studio in studios: @@ -174,11 +182,13 @@ def add_studio(self, *args): return studio_id def get_studio(self, *args): - try: - self.cursor.execute(queries_videos.get_studio, args) - return self.cursor.fetchone()[0] - except TypeError: - return self.add_studio(*args) + self.cursor.execute(queries_videos.get_studio, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return self.add_studio(*args) #First remove any existing entries #Then re-add video, audio and subtitles @@ -223,18 +233,10 @@ def add_playstate(self, file_id, playcount, date_played, resume, *args): def set_playcount(self, *args): self.cursor.execute(queries_videos.update_playcount, args) -# def add_settings(self, *args): -# self.cursor.execute(queries_videos.update_settings, args) - -# def get_settings(self, *args): -# self.cursor.execute(queries_videos.get_settings, args) -# return self.cursor.fetchone() - def add_tags(self, tags, *args): self.cursor.execute(queries_videos.delete_tags, args) for tag in tags: -# tag_id = self.get_tag(tag, *args) self.get_tag(tag, *args) def add_tag(self, *args): @@ -243,31 +245,36 @@ def add_tag(self, *args): return tag_id def get_tag(self, tag, *args): - try: - self.cursor.execute(queries_videos.get_tag, (tag,)) - tag_id = self.cursor.fetchone()[0] - except TypeError: + self.cursor.execute(queries_videos.get_tag, (tag,)) + Data = self.cursor.fetchone() + + if Data: + tag_id = Data[0] + else: tag_id = self.add_tag(tag) self.cursor.execute(queries_videos.update_tag, (tag_id,) + args) return tag_id def remove_tag(self, tag, *args): - try: - self.cursor.execute(queries_videos.get_tag, (tag,)) - tag_id = self.cursor.fetchone()[0] - except TypeError: + self.cursor.execute(queries_videos.get_tag, (tag,)) + Data = self.cursor.fetchone() + + if Data: + tag_id = Data[0] + else: return self.cursor.execute(queries_videos.delete_tag, (tag_id,) + args) def get_rating_id(self, *args): - try: - self.cursor.execute(queries_videos.get_rating, args) + self.cursor.execute(queries_videos.get_rating, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] - return self.cursor.fetchone()[0] - except TypeError: - return self.create_entry_rating() + return self.create_entry_rating() #Add ratings, rating type and votes def add_ratings(self, *args): diff --git a/core/listitem.py b/core/listitem.py index 23bc5221a..8a9916aaa 100644 --- a/core/listitem.py +++ b/core/listitem.py @@ -1,51 +1,18 @@ # -*- coding: utf-8 -*- -import logging import xbmcgui - -from helper import api +import helper.api +import helper.loghandler from . import obj_ops -class BaseListItem(object): - def __init__(self, obj_type, art_type, art_parent, listitem, item, Utils): - self.LOG = logging.getLogger("EMBY.code.listitem.BaseListItem") - self.li = listitem - self.item = item - self.Utils = Utils - self.objects = obj_ops.Objects(self.Utils) - self.api = api.API(item, self.Utils, item['LI']['Server']) - self.obj = self._get_objects(obj_type) - self.obj['Artwork'] = self._get_artwork(art_type, art_parent) - self.format() - self.set() - - def __getitem__(self, key): - if key in self.obj: - return self.obj[key] - - return None - - def __setitem__(self, key, value): - self.obj[key] = value - - def _get_objects(self, key): - return self.objects.map(self.item, key) - - def _get_artwork(self, key, parent=False): - return self.api.get_all_artwork(self.objects.map(self.item, key), parent) - - #Format object values. Override - def format(self): - pass - - #Set the listitem values based on object. Override - def set(self): - pass - - #Return artwork mapping for object. Override if needed - @classmethod - def art(cls): - return { - 'poster': "Primary", +class ListItem(): + def __init__(self, Utils): + self.objects = obj_ops.Objects() + self.API = helper.api.API(Utils) + + def set(self, item): + listitem = xbmcgui.ListItem() + Properties = {} + art = { 'clearart': "Art", 'clearlogo': "Logo", 'discart': "Disc", @@ -55,793 +22,294 @@ def art(cls): 'fanart': "Backdrop" } - def set_art(self): - artwork = self['Artwork'] - art = self.art() - - for kodi, emby in list(art.items()): - if emby == 'Backdrop': - self._set_art(kodi, artwork[emby][0] if artwork[emby] else " ") + if item['Type'] == 'Genre': + obj = self.objects.map(item, 'BrowseGenre') + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'Artwork'), False) + Properties['IsFolder'] = 'true' + Properties['IsPlayable'] = 'false' + elif item['Type'] in ("Movie", "MusicVideo", 'Episode', 'Season', 'Series', 'Video', 'BoxSet', 'AudioBook', 'Folder', 'Trailer', 'Studio', 'Person', 'Program', 'CollectionFolder', 'UserView'): + obj = self.objects.map(item, 'BrowseVideo') + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'ArtworkParent'), True) + obj['Genres'] = " / ".join(obj['Genres'] or []) + obj['Studios'] = [self.API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Studios'] = " / ".join(obj['Studios']) + obj['Mpaa'] = self.API.get_mpaa(obj['Mpaa'], item) + obj['People'] = obj['People'] or [] + obj['Countries'] = " / ".join(obj['Countries'] or []) + obj['Directors'] = " / ".join(obj['Directors'] or []) + obj['Writers'] = " / ".join(obj['Writers'] or []) + obj['Plot'] = self.API.get_overview(obj['Plot'], item) + obj['ShortPlot'] = self.API.get_overview(obj['ShortPlot'], item) + obj['Rating'] = obj['Rating'] or 0 + obj['DateAdded'], obj['FileDate'] = self.API.get_DateAdded(obj['DateAdded']) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['Resume'] = self.API.adjust_resume((obj['Resume'] or 0) / 10000000.0) + obj['PlayCount'] = self.API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Overlay'] = 7 if obj['Played'] else 6 + obj['Video'] = self.API.video_streams(obj['Video'] or [], obj['Container'], item) + obj['Audio'] = self.API.audio_streams(obj['Audio'] or []) + obj['Streams'] = self.API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj['ChildCount'] = obj['ChildCount'] or 0 + obj['RecursiveCount'] = obj['RecursiveCount'] or 0 + obj['Unwatched'] = obj['Unwatched'] or 0 + obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or [] + obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or "" + obj['Artwork']['Primary'] = obj['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + + if obj['Premiere']: + obj['Premiere'] = obj['Premiere'].split('T')[0] + + if obj['DatePlayed']: + obj['DatePlayed'] = obj['DatePlayed'].split('.')[0].replace('T', " ") + + if obj['Status'] != 'Ended': + obj['Status'] = None + + Folder = False + Properties['totaltime'] = str(obj['Runtime']) + metadata = { + 'title': obj['Title'], + 'originaltitle': obj['OriginalTitle'], + 'sorttitle': obj['SortTitle'], + 'country': obj['Countries'], + 'genre': obj['Genres'], + 'year': obj['Year'], + 'rating': obj['Rating'], + 'playcount': obj['PlayCount'], + 'overlay': obj['Overlay'], + 'director': obj['Directors'], + 'mpaa': obj['Mpaa'], + 'plot': obj['Plot'], + 'plotoutline': obj['ShortPlot'], + 'studio': obj['Studios'], + 'tagline': obj['Tagline'], + 'writer': obj['Writers'], + 'premiered': obj['Premiere'], + 'votes': obj['Votes'], + 'date': obj['Premiere'] or obj['FileDate'], + 'lastplayed': obj['DatePlayed'], + 'duration': obj['Runtime'], + 'aired': obj['Year'] + } + + if obj['DateAdded']: + metadata['dateadded'] = obj['DateAdded'] + + if item['Type'] == 'Movie': + metadata['imdbnumber'] = obj['UniqueId'] + metadata['userrating'] = obj['CriticRating'] + metadata['mediatype'] = "movie" + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'MusicVideo': + metadata['mediatype'] = "musicvideo" + metadata['album'] = obj['Album'] + metadata['artist'] = obj['Artists'] or [] + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'Episode': + metadata['mediatype'] = "episode" + metadata['tvshowtitle'] = obj['SeriesName'] + metadata['season'] = obj['Season'] or 0 + metadata['sortseason'] = obj['Season'] or 0 + metadata['episode'] = obj['Index'] or 0 + metadata['sortepisode'] = obj['Index'] or 0 + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Series.Primary" + art['tvshow.poster'] = "Series.Primary" + art['tvshow.clearart'] = "Art" + art['tvshow.clearlogo'] = "Logo" + elif item['Type'] == 'Season': + metadata['mediatype'] = "season" + metadata['tvshowtitle'] = obj['SeriesName'] + metadata['season'] = obj['Index'] or 0 + metadata['sortseason'] = obj['Index'] or 0 + Properties['NumEpisodes'] = str(obj['RecursiveCount']) + Properties['WatchedEpisodes'] = str(obj['RecursiveCount'] - obj['Unwatched']) + Properties['UnWatchedEpisodes'] = str(obj['Unwatched']) + Properties['IsFolder'] = 'true' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'Series': + metadata['mediatype'] = "tvshow" + metadata['tvshowtitle'] = obj['Title'] + metadata['status'] = obj['Status'] + Properties['TotalSeasons'] = str(obj['ChildCount']) + Properties['TotalEpisodes'] = str(obj['RecursiveCount']) + Properties['WatchedEpisodes'] = str(obj['RecursiveCount'] - obj['Unwatched']) + Properties['UnWatchedEpisodes'] = str(obj['Unwatched']) + Properties['IsFolder'] = 'true' + Properties['IsPlayable'] = 'true' + Folder = True + art['poster'] = "Primary" + elif item['Type'] == 'Video': + metadata['mediatype'] = "video" + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'Boxset': + metadata['mediatype'] = "set" + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'Trailer': + metadata['mediatype'] = "video" + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + art['poster'] = "Primary" + elif item['Type'] == 'Folder': + Properties['IsFolder'] = 'true' + Properties['IsPlayable'] = 'true' + Folder = True + art['poster'] = "Primary" + + if not Folder: +# if obj['Resume'] and obj['Runtime'] and seektime != False: +# Properties['resumetime'] = str(obj['Resume']) +# Properties['StartPercent'] = str(((obj['Resume'] / obj['Runtime']) * 100)) +# else: + Properties['resumetime'] = '0' + Properties['StartPercent'] = '0' + + for track in obj['Streams']['video']: + listitem.addStreamInfo('video', { + 'duration': obj['Runtime'], + 'aspect': track['aspect'], + 'codec': track['codec'], + 'width': track['width'], + 'height': track['height'] + }) + + for track in obj['Streams']['audio']: + listitem.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']}) + + for track in obj['Streams']['subtitle']: + listitem.addStreamInfo('subtitle', {'language': track}) + + listitem.setInfo('video', metadata) + elif item['Type'] in ("Music", "Audio", "MusicAlbum", "MusicArtist", "Artist", "MusicGenre", "Channel"): + obj = self.objects.map(item, 'BrowseAudio') + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = self.API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Rating'] = obj['Rating'] or 0 + obj['DateAdded'], obj['FileDate'] = self.API.get_DateAdded(obj['DateAdded']) + metadata = { + 'title': obj['Title'], + 'genre': obj['Genre'], + 'year': obj['Year'], + 'album': obj['Album'], + 'artist': obj['Artists'], + 'rating': obj['Rating'], + 'comment': obj['Comment'] + } + + if obj['FileDate']: + metadata['date'] = obj['DateAdded'] + + if item['Type'] == 'Music': + metadata['mediatype'] = "music" + elif item['Type'] == 'Audio': + metadata['mediatype'] = "song" + metadata['tracknumber'] = obj['Index'] + metadata['discnumber'] = obj['Disc'] + metadata['duration'] = obj['Runtime'] + metadata['playcount'] = obj['PlayCount'] + metadata['lastplayed'] = obj['DatePlayed'] + metadata['musicbrainztrackid'] = obj['UniqueId'] + Properties['IsPlayable'] = 'true' + Properties['IsFolder'] = 'false' + elif item['Type'] == 'MusicAlbum': + metadata['mediatype'] = "album" + metadata['musicbrainzalbumid'] = obj['UniqueId'] + Properties['IsFolder'] = 'true' + elif item['Type'] in ("MusicArtist", "Artist"): + metadata['mediatype'] = "artist" + metadata['musicbrainzartistid'] = obj['UniqueId'] + Properties['IsFolder'] = 'true' + + listitem.setInfo('music', metadata) + elif item['Type'] in ("Photo", "PhotoAlbum"): + obj = self.objects.map(item, 'BrowsePhoto') + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'Artwork'), False) + obj['Overview'] = self.API.get_overview(obj['Overview'], item) + obj['DateAdded'], obj['FileDate'] = self.API.get_DateAdded(obj['DateAdded']) + metadata = { + 'title': obj['Title'], + 'picturepath': obj['Artwork']['Primary'], + 'exif:width': str(obj.get('Width', 0)), + 'exif:height': str(obj.get('Height', 0)), + 'size': obj['Size'], + 'exif:cameramake': obj['CameraMake'], + 'exif:cameramodel': obj['CameraModel'], + 'exif:exposuretime': str(obj['ExposureTime']), + 'exif:focallength': str(obj['FocalLength']) + } + + if obj['FileDate']: + metadata['date'] = obj['DateAdded'] + + if item['Type'] == 'Photo': + Properties['IsFolder'] = 'false' else: - self._set_art(kodi, artwork.get(emby, " ")) - - def _set_art(self, art, path): - self.LOG.debug(" [ art/%s ] %s", art, path) - - if art in ('fanart_image', 'small_poster', 'tiny_poster', 'medium_landscape', 'medium_poster', 'small_fanartimage', 'medium_fanartimage', 'fanart_noindicators', 'tvshow.poster'): - self.li.setProperty(art, path) - else: - self.li.setArt({art: path}) - -class Playlist(BaseListItem): - def __init__(self, *args, **kwargs): - BaseListItem.__init__(self, 'BrowseFolder', 'Artwork', False, *args, **kwargs) - - def set(self): - self.li.setProperty('path', self['Artwork']['Primary']) - self.li.setProperty('IsFolder', 'true') -# self.li.setThumbnailImage(self['Artwork']['Primary']) -# self.li.setIconImage('DefaultFolder.png') - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - self.li.setProperty('IsPlayable', 'false') - self.li.setLabel(self['Title']) - self.li.setContentLookup(False) - -class Channel(BaseListItem): - def __init__(self, *args, **kwargs): - BaseListItem.__init__(self, 'BrowseChannel', 'Artwork', False, *args, **kwargs) - - @staticmethod - def art(): - return { - 'fanart_image': "Backdrop", - 'thumb': "Primary", - 'fanart': "Backdrop" - } - - def format(self): - self['Title'] = "%s - %s" % (self['Title'], self['ProgramName']) - self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) - self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 - self['Overlay'] = 7 if self['Played'] else 6 - self['Artwork']['Primary'] = self['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" - self['Artwork']['Thumb'] = self['Artwork']['Thumb'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" - self['Artwork']['Backdrop'] = self['Artwork']['Backdrop'] or ["special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg"] - - def set(self): - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'] - } -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) -## self.set_art() - self.li.setProperty('totaltime', str(self['Runtime'])) - self.li.setProperty('IsPlayable', 'true') - self.li.setProperty('IsFolder', 'false') - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Photo(BaseListItem): - def __init__(self, *args, **kwargs): - BaseListItem.__init__(self, 'BrowsePhoto', 'Artwork', False, *args, **kwargs) - - def format(self): - self['Overview'] = self.api.get_overview(self['Overview']) - - try: - self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['FileDate'].split('T')[0].split('-'))) - except: - pass - - def set(self): - metadata = { - 'title': self['Title'], - 'picturepath': self['Artwork']['Primary'], - 'date': self['FileDate'], - 'exif:width': str(self.obj.get('Width', 0)), - 'exif:height': str(self.obj.get('Height', 0)), - 'size': self['Size'], - 'exif:cameramake': self['CameraMake'], - 'exif:cameramodel': self['CameraModel'], - 'exif:exposuretime': str(self['ExposureTime']), - 'exif:focallength': str(self['FocalLength']) - } - self.li.setProperty('path', self['Artwork']['Primary']) -# self.li.setThumbnailImage(self['Artwork']['Primary']) - -# if art in ('fanart_image', 'small_poster', 'tiny_poster', -# 'medium_landscape', 'medium_poster', 'small_fanartimage', -# 'medium_fanartimage', 'fanart_noindicators', 'discart', -# 'tvshow.poster'): - -#thumb string - image filename -#poster string - image filename -#banner string - image filename -#fanart string - image filename -#learart string - image filename -#clearlogo string - image filename -#landscape string - image filename -#icon string - image filename - -#listitem.setArt({"thumb":thumb, "fanart": fanart}) - -#Function: setAvailableFanart(images) -#image string (http://www.someurl.com/someimage.png) -#preview [opt] string (http://www.someurl.com/somepreviewimage.png) - -# self.li.setArt({"thumb": self['Artwork']['Primary']}) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - self.li.setProperty('plot', self['Overview']) - self.li.setProperty('IsFolder', 'false') -# self.li.setIconImage('DefaultPicture.png') -# self.li.setProperty('IsPlayable', 'false') - self.li.setLabel(self['Title']) - self.li.setInfo('pictures', metadata) - self.li.setContentLookup(False) - -class Music(BaseListItem): - def __init__(self, *args, **kwargs): - BaseListItem.__init__(self, 'BrowseAudio', 'ArtworkMusic', True, *args, **kwargs) - - @classmethod - def art(cls): - return { - 'clearlogo': "Logo", - 'discart': "Disc", - 'fanart': "Backdrop", - 'fanart_image': "Backdrop", - 'thumb': "Primary" - } - - def format(self): - self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) - self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 - self['Rating'] = self['Rating'] or 0 - - if self['FileDate'] or self['DatePlayed']: - self['DatePlayed'] = (self['DatePlayed'] or self['FileDate']).split('.')[0].replace('T', " ") - - self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['FileDate'].split('T')[0].split('-'))) - - def set(self): - return -# metadata = { -# 'title': self['Title'], -# 'genre': self['Genre'], -# 'year': self['Year'], -# 'album': self['Album'], -# 'artist': self['Artists'], -# 'rating': self['Rating'], -# 'comment': self['Comment'], -# 'date': self['FileDate'], -# 'mediatype': "music" -# } -## self.set_art() - -class PhotoAlbum(Photo): - def __init__(self, *args, **kwargs): - Photo.__init__(self, *args, **kwargs) - - def set(self): - metadata = {'title': self['Title']} - self.li.setProperty('path', self['Artwork']['Primary']) -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - self.li.setProperty('IsFolder', 'true') -# self.li.setIconImage('DefaultFolder.png') -# self.li.setProperty('IsPlayable', 'false') - self.li.setLabel(self['Title']) - self.li.setInfo('pictures', metadata) - self.li.setContentLookup(False) - -class Video(BaseListItem): - def __init__(self, *args, **kwargs): - BaseListItem.__init__(self, 'BrowseVideo', 'ArtworkParent', True, *args, **kwargs) - self.LOG = logging.getLogger("EMBY.code.listitem.Video") - - def format(self): - self['Genres'] = " / ".join(self['Genres'] or []) - self['Studios'] = [self.api.validate_studio(studio) for studio in (self['Studios'] or [])] - self['Studios'] = " / ".join(self['Studios']) - self['Mpaa'] = self.api.get_mpaa(self['Mpaa']) - self['People'] = self['People'] or [] - self['Countries'] = " / ".join(self['Countries'] or []) - self['Directors'] = " / ".join(self['Directors'] or []) - self['Writers'] = " / ".join(self['Writers'] or []) - self['Plot'] = self.api.get_overview(self['Plot']) - self['ShortPlot'] = self.api.get_overview(self['ShortPlot']) - self['DateAdded'] = self['DateAdded'].split('.')[0].replace('T', " ") - self['Rating'] = self['Rating'] or 0 - self['FileDate'] = "%s.%s.%s" % tuple(reversed(self['DateAdded'].split('T')[0].split('-'))) - self['Runtime'] = round(float((self['Runtime'] or 0) / 10000000.0), 6) - self['Resume'] = self.api.adjust_resume((self['Resume'] or 0) / 10000000.0, self.Utils) - self['PlayCount'] = self.api.get_playcount(self['Played'], self['PlayCount']) or 0 - self['Overlay'] = 7 if self['Played'] else 6 - self['Video'] = self.api.video_streams(self['Video'] or [], self['Container']) - self['Audio'] = self.api.audio_streams(self['Audio'] or []) - self['Streams'] = self.api.media_streams(self['Video'], self['Audio'], self['Subtitles']) - self['ChildCount'] = self['ChildCount'] or 0 - self['RecursiveCount'] = self['RecursiveCount'] or 0 - self['Unwatched'] = self['Unwatched'] or 0 - self['Artwork']['Backdrop'] = self['Artwork']['Backdrop'] or [] - self['Artwork']['Thumb'] = self['Artwork']['Thumb'] or "" - self['Artwork']['Primary'] = self['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" - - if self['Premiere']: - self['Premiere'] = self['Premiere'].split('T')[0] - - if self['DatePlayed']: - self['DatePlayed'] = self['DatePlayed'].split('.')[0].replace('T', " ") - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "video", - 'lastplayed': self['DatePlayed'], - 'duration': self['Runtime'] - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.set_playable() - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - - def set_playable(self): - self.li.setProperty('totaltime', str(self['Runtime'])) - self.li.setProperty('IsPlayable', 'true') - self.li.setProperty('IsFolder', 'false') - - if self['Resume'] and self['Runtime'] and self.item['LI']['Seektime'] != False: - self.li.setProperty('resumetime', str(self['Resume'])) - self.li.setProperty('StartPercent', str(((self['Resume']/self['Runtime']) * 100))) - else: - self.li.setProperty('resumetime', '0') - self.li.setProperty('StartPercent', '0') - - for track in self['Streams']['video']: - self.li.addStreamInfo('video', { - 'duration': self['Runtime'], - 'aspect': track['aspect'], - 'codec': track['codec'], - 'width': track['width'], - 'height': track['height'] - }) - - for track in self['Streams']['audio']: - self.li.addStreamInfo('audio', {'codec': track['codec'], 'channels': track['channels']}) - - for track in self['Streams']['subtitle']: - self.li.addStreamInfo('subtitle', {'language': track}) - -class Audio(Music): - def __init__(self, *args, **kwargs): - Music.__init__(self, *args, **kwargs) - - def set(self): - metadata = { - 'title': self['Title'], - 'genre': self['Genre'], - 'year': self['Year'], - 'album': self['Album'], - 'artist': self['Artists'], - 'rating': self['Rating'], - 'comment': self['Comment'], - 'date': self['FileDate'], - 'mediatype': "song", - 'tracknumber': self['Index'], - 'discnumber': self['Disc'], - 'duration': self['Runtime'], - 'playcount': self['PlayCount'], - 'lastplayed': self['DatePlayed'], - 'musicbrainztrackid': self['UniqueId'] - } -## self.set_art() - self.li.setProperty('IsPlayable', 'true') - self.li.setProperty('IsFolder', 'false') - self.li.setLabel(self['Title']) - self.li.setInfo('music', metadata) - self.li.setContentLookup(False) - -class Album(Music): - def __init__(self, *args, **kwargs): - Music.__init__(self, *args, **kwargs) - - def set(self): - metadata = { - 'title': self['Title'], - 'genre': self['Genre'], - 'year': self['Year'], - 'album': self['Album'], - 'artist': self['Artists'], - 'rating': self['Rating'], - 'comment': self['Comment'], - 'date': self['FileDate'], - 'mediatype': "album", - 'musicbrainzalbumid': self['UniqueId'] - } -## self.set_art() - self.li.setLabel(self['Title']) - self.li.setInfo('music', metadata) - self.li.setContentLookup(False) - -class Artist(Music): - def __init__(self, *args, **kwargs): - Music.__init__(self, *args, **kwargs) - - def set(self): - metadata = { - 'title': self['Title'], - 'genre': self['Genre'], - 'year': self['Year'], - 'album': self['Album'], - 'artist': self['Artists'], - 'rating': self['Rating'], - 'comment': self['Comment'], - 'date': self['FileDate'], - 'mediatype': "artist", - 'musicbrainzartistid': self['UniqueId'] - } -## self.set_art() - self.li.setLabel(self['Title']) - self.li.setInfo('music', metadata) - self.li.setContentLookup(False) - -class Episode(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - @classmethod - def art(cls): - return { - 'poster': "Series.Primary", - 'tvshow.poster': "Series.Primary", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb", - 'tvshow.landscape': "Thumb", - 'thumb': "Primary", - 'fanart': "Backdrop" - } - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['OriginalTitle'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "episode", - 'tvshowtitle': self['SeriesName'], - 'season': self['Season'] or 0, - 'sortseason': self['Season'] or 0, - 'episode': self['Index'] or 0, - 'sortepisode': self['Index'] or 0, - 'lastplayed': self['DatePlayed'], - 'duration': self['Runtime'], - 'aired': self['Premiere'] - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.set_playable() - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Season(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "season", - 'tvshowtitle': self['SeriesName'], - 'season': self['Index'] or 0, - 'sortseason': self['Index'] or 0 - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.li.setProperty('NumEpisodes', str(self['RecursiveCount'])) - self.li.setProperty('WatchedEpisodes', str(self['RecursiveCount'] - self['Unwatched'])) - self.li.setProperty('UnWatchedEpisodes', str(self['Unwatched'])) - self.li.setProperty('IsFolder', 'true') - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Series(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def format(self): - super(Series, self).format() - - if self['Status'] != 'Ended': - self['Status'] = None - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['OriginalTitle'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "tvshow", - 'tvshowtitle': self['Title'], - 'status': self['Status'] - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.li.setProperty('TotalSeasons', str(self['ChildCount'])) - self.li.setProperty('TotalEpisodes', str(self['RecursiveCount'])) - self.li.setProperty('WatchedEpisodes', str(self['RecursiveCount'] - self['Unwatched'])) - self.li.setProperty('UnWatchedEpisodes', str(self['Unwatched'])) - self.li.setProperty('IsFolder', 'true') - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Movie(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['OriginalTitle'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "movie", - 'imdbnumber': self['UniqueId'], - 'lastplayed': self['DatePlayed'], - 'duration': self['Runtime'], - 'userrating': self['CriticRating'] - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.set_playable() - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class BoxSet(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "set" - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.li.setProperty('IsFolder', 'true') - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class MusicVideo(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def set(self): -## self.set_art() -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "musicvideo", - 'album': self['Album'], - 'artist': self['Artists'] or [], - 'lastplayed': self['DatePlayed'], - 'duration': self['Runtime'] - } - - if self.item['LI']['DbId']: - metadata['dbid'] = self.item['LI']['DbId'] - - self.li.setCast(self.api.get_actors()) - self.set_playable() - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Intro(Video): - def __init__(self, *args, **kwargs): - Video.__init__(self, *args, **kwargs) - - def format(self): - self['Artwork']['Primary'] = self['Artwork']['Primary'] or self['Artwork']['Thumb'] or (self['Artwork']['Backdrop'][0] if len(self['Artwork']['Backdrop']) else "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg") - self['Artwork']['Primary'] += "&KodiCinemaMode=true" - self['Artwork']['Backdrop'] = [self['Artwork']['Primary']] - super(Intro, self).format() - - def set(self): -## self.set_art() - self.li.setArt({'poster': ""}) # Clear the poster value for intros / trailers to prevent issues in skins -# self.li.setIconImage('DefaultVideo.png') -# self.li.setThumbnailImage(self['Artwork']['Primary']) - self.li.setArt({"thumb": self['Artwork']['Primary'], "icon" : 'DefaultFolder.png'}) - metadata = { - 'title': self['Title'], - 'originaltitle': self['Title'], - 'sorttitle': self['SortTitle'], - 'country': self['Countries'], - 'genre': self['Genres'], - 'year': self['Year'], - 'rating': self['Rating'], - 'playcount': self['PlayCount'], - 'overlay': self['Overlay'], - 'director': self['Directors'], - 'mpaa': self['Mpaa'], - 'plot': self['Plot'], - 'plotoutline': self['ShortPlot'], - 'studio': self['Studios'], - 'tagline': self['Tagline'], - 'writer': self['Writers'], - 'premiered': self['Premiere'], - 'votes': self['Votes'], - 'dateadded': self['DateAdded'], - 'aired': self['Year'], - 'date': self['Premiere'] or self['FileDate'], - 'mediatype': "video", - 'lastplayed': self['DatePlayed'], - 'duration': self['Runtime'] - } - self.li.setCast(self.api.get_actors()) - self.set_playable() - self.li.setLabel(self['Title']) - self.li.setInfo('video', metadata) - self.li.setContentLookup(False) - -class Trailer(Intro): - def __init__(self, *args, **kwargs): - Intro.__init__(self, *args, **kwargs) - - def format(self): - self['Artwork']['Primary'] = self['Artwork']['Primary'] or self['Artwork']['Thumb'] or (self['Artwork']['Backdrop'][0] if len(self['Artwork']['Backdrop']) else "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg") - self['Artwork']['Primary'] += "&KodiTrailer=true" - self['Artwork']['Backdrop'] = [self['Artwork']['Primary']] - Video.format(self) - -MUSIC = { - 'Artist': Artist, - 'MusicArtist': Artist, - 'MusicAlbum': Album, - 'Audio': Audio, - 'Music': Music -} -PHOTO = { - 'Photo': Photo, - 'PhotoAlbum': PhotoAlbum -} -VIDEO = { - 'Episode': Episode, - 'Season': Season, - 'Series': Series, - 'Movie': Movie, - 'MusicVideo': MusicVideo, - 'BoxSet': BoxSet, - 'Trailer': Trailer, - 'AudioBook': Video, - 'Video': Video, - 'Intro': Intro -} -BASIC = { - 'Playlist': Playlist, - 'TvChannel': Channel -} - -#Translate an emby item into a Kodi listitem. -#Returns the listitem -class ListItem(): - def __init__(self, server_addr, Utils): - self.server = server_addr - self.Utils = Utils - - def _detect_type(self, item): - item_type = item['Type'] - - for typ in (VIDEO, MUSIC, PHOTO, BASIC): - if item_type in typ: - return typ[item_type] - - return VIDEO['Video'] - - def set(self, item, listitem, db_id, intro, seektime, *args, **kwargs): - listitem = listitem or xbmcgui.ListItem() - item['LI'] = { - 'DbId': db_id, - 'Seektime': seektime, - 'Server': self.server - } - - if intro: - func = VIDEO['Trailer'] if item['Type'] == 'Trailer' else VIDEO['Intro'] - else: - func = self._detect_type(item) + Properties['IsFolder'] = 'true' + + Properties['plot'] = obj['Overview'] + Properties['IsPlayable'] = 'false' + listitem.setInfo('pictures', metadata) + elif item['Type'] == 'Playlist': + obj = self.objects.map(item, 'BrowseFolder') + metadata = { + 'title': obj['Title'] + } + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'Artwork'), False) + Properties['IsFolder'] = 'true' + Properties['IsPlayable'] = 'false' + listitem.setInfo('video', metadata) + elif item['Type'] == 'TvChannel': + obj = self.objects.map(item, 'BrowseChannel') + obj['Artwork'] = self.API.get_all_artwork(self.objects.map(item, 'Artwork'), False) + obj['Title'] = "%s - %s" % (obj['Title'], obj['ProgramName']) + obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) + obj['PlayCount'] = self.API.get_playcount(obj['Played'], obj['PlayCount']) or 0 + obj['Overlay'] = 7 if obj['Played'] else 6 + obj['Artwork']['Primary'] = obj['Artwork']['Primary'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + obj['Artwork']['Thumb'] = obj['Artwork']['Thumb'] or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" + obj['Artwork']['Backdrop'] = obj['Artwork']['Backdrop'] or ["special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg"] + metadata = { + 'title': obj['Title'], + 'originaltitle': obj['Title'], + 'playcount': obj['PlayCount'], + 'overlay': obj['Overlay'] + } + Properties['totaltime'] = str(obj['Runtime']) + Properties['IsFolder'] = 'false' + Properties['IsPlayable'] = 'true' + listitem.setInfo('video', metadata) + + if Properties: + listitem.setProperties(Properties) + + if obj['Artwork']: + ArtworkData = {} + + for kodi, emby in list(art.items()): + if emby == 'Backdrop': + ArtworkData[kodi] = obj['Artwork'][emby][0] if obj['Artwork'][emby] else "" + else: + if emby in obj['Artwork']: + ArtworkData[kodi] = obj['Artwork'][emby] + + listitem.setArt(ArtworkData) + + listitem.setLabel(obj['Title']) + listitem.setContentLookup(False) + + if 'People' in obj: + if obj['People']: + listitem.setCast(self.API.get_actors(obj['People'])) - func(listitem, item, self.Utils, *args, **kwargs) - item.pop('LI') return listitem diff --git a/core/movies.py b/core/movies.py index ccc56de9b..e07691779 100644 --- a/core/movies.py +++ b/core/movies.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -import logging - -import emby.downloader -import helper.wrapper import helper.api +import helper.loghandler import database.emby_db import database.queries from . import obj_ops @@ -13,40 +10,29 @@ from . import common class Movies(): - def __init__(self, server, embydb, videodb, direct_path, Utils): - self.LOG = logging.getLogger("EMBY.core.movies.Movies") - self.Utils = Utils - self.server = server + def __init__(self, EmbyServer, embydb, videodb): + self.LOG = helper.loghandler.LOG('EMBY.core.movies.Movies') + self.EmbyServer = EmbyServer self.emby = embydb self.video = videodb - self.direct_path = direct_path self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) - self.objects = obj_ops.Objects(self.Utils) + self.objects = obj_ops.Objects() self.item_ids = [] - self.Downloader = emby.downloader.Downloader(self.Utils) - self.Common = common.Common(self.emby_db, self.objects, self.Utils, self.direct_path, self.server) - self.KodiDBIO = kodi.Kodi(videodb.cursor) + self.Common = common.Common(self.emby_db, self.objects, self.EmbyServer) + self.KodiDBIO = kodi.Kodi(videodb.cursor, self.EmbyServer.Utils) self.MoviesDBIO = MoviesDBIO(videodb.cursor) - self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) - - def __getitem__(self, key): - if key == 'Movie': - return self.movie - elif key == 'BoxSet': - return self.boxset - + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.EmbyServer.Utils) + self.APIHelper = helper.api.API(self.EmbyServer.Utils) #If item does not exist, entry will be added. #If item exists, entry will be updated - @helper.wrapper.stop - def movie(self, item, library=None): + def movie(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Movie') obj['Item'] = item obj['Library'] = library @@ -56,21 +42,21 @@ def movie(self, item, library=None): StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] if str(StackedID) != obj['Id']: - self.LOG.info("Skipping stacked movie %s [%s/%s]", obj['Title'], StackedID, obj['Id']) - Movies(self.server, self.emby, self.video, self.direct_path, self.Utils).remove(StackedID) + self.LOG.info("Skipping stacked movie %s [%s/%s]" % (obj['Title'], StackedID, obj['Id'])) + Movies(self.EmbyServer, self.emby, self.video).remove(StackedID) - try: + if e_item: obj['MovieId'] = e_item[0] obj['FileId'] = e_item[1] obj['PathId'] = e_item[2] - except TypeError: + + if self.MoviesDBIO.get(*self.EmbyServer.Utils.values(obj, queries_videos.get_movie_obj)) is None: + update = False + self.LOG.info("MovieId %s missing from kodi. repairing the entry." % obj['MovieId']) + else: update = False - self.LOG.debug("MovieId %s not found", obj['Id']) + self.LOG.debug("MovieId %s not found" % obj['Id']) obj['MovieId'] = self.MoviesDBIO.create_entry() - else: - if self.MoviesDBIO.get(*self.Utils.values(obj, queries_videos.get_movie_obj)) is None: - update = False - self.LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId']) obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] @@ -80,40 +66,44 @@ def movie(self, item, library=None): obj['Path'] = obj['Item']['MediaSources'][0]['Path'] #don't use 3d movies as default - if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + if "3d" in self.EmbyServer.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): for DataSource in obj['Item']['MediaSources']: - if not "3d" in self.Utils.StringMod(DataSource['Path']): + if not "3d" in self.EmbyServer.Utils.StringMod(DataSource['Path']): DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') obj['Path'] = DataSource['Path'] obj['MediaSourceID'] = DataSource['Id'] obj['Runtime'] = DataSource['RunTimeTicks'] break - obj['Path'] = API.get_file_path(obj['Path']) + obj['Path'] = self.APIHelper.get_file_path(obj['Path'], item) obj['Genres'] = obj['Genres'] or [] - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Studios'] = [self.APIHelper.validate_studio(studio) for studio in (obj['Studios'] or [])] obj['People'] = obj['People'] or [] obj['Genre'] = " / ".join(obj['Genres']) obj['Writers'] = " / ".join(obj['Writers'] or []) obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Plot'] = self.APIHelper.get_overview(obj['Plot'], item) + obj['Mpaa'] = self.APIHelper.get_mpaa(obj['Mpaa'], item) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['People'] = API.get_people_artwork(obj['People']) - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['Premiered'] = self.Utils.convert_to_local(obj['Year']) if not obj['Premiered'] else self.Utils.convert_to_local(obj['Premiered']).replace(" ", "T").split('T')[0] - obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - obj = self.Common.get_path_filename(obj, "movies") + obj['People'] = self.APIHelper.get_people_artwork(obj['People']) + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['Premiered'] = self.EmbyServer.Utils.convert_to_local(obj['Year']) if not obj['Premiered'] else self.EmbyServer.Utils.convert_to_local(obj['Premiered']).replace(" ", "T").split('T')[0] + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = self.APIHelper.video_streams(obj['Video'] or [], obj['Container'], item) + obj['Audio'] = self.APIHelper.audio_streams(obj['Audio'] or []) + obj['Streams'] = self.APIHelper.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + PathValid, obj = self.Common.get_path_filename(obj, "movies") + + if not PathValid: + return "Invalid Filepath" + self.trailer(obj) if obj['Countries']: - self.MoviesDBIO.add_countries(*self.Utils.values(obj, queries_videos.update_country_obj)) + self.MoviesDBIO.add_countries(*self.EmbyServer.Utils.values(obj, queries_videos.update_country_obj)) tags = [] tags.extend(obj['TagItems'] or obj['Tags'] or []) @@ -129,19 +119,19 @@ def movie(self, item, library=None): else: self.movie_add(obj) - self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_movie_obj)) - self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) - self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_movie_obj)) - self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_movie_obj)) - self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_movie_obj)) - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) - self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_movie_obj)) - self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) + self.KodiDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_videos.update_path_movie_obj)) + self.KodiDBIO.update_file(*self.EmbyServer.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_tags(*self.EmbyServer.Utils.values(obj, queries_videos.add_tags_movie_obj)) + self.KodiDBIO.add_genres(*self.EmbyServer.Utils.values(obj, queries_videos.add_genres_movie_obj)) + self.KodiDBIO.add_studios(*self.EmbyServer.Utils.values(obj, queries_videos.add_studios_movie_obj)) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.add_people(*self.EmbyServer.Utils.values(obj, queries_videos.add_people_movie_obj)) + self.KodiDBIO.add_streams(*self.EmbyServer.Utils.values(obj, queries_videos.add_streams_obj)) self.ArtworkDBIO.add(obj['Artwork'], obj['MovieId'], "movie") self.item_ids.append(obj['Id']) if "StackTimes" in obj: - self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + self.KodiDBIO.add_stacktimes(*self.EmbyServer.Utils.values(obj, queries_videos.add_stacktimes_obj)) return not update @@ -150,13 +140,14 @@ def movie_add(self, obj): obj = self.Common.Streamdata_add(obj, False) obj['RatingType'] = "default" obj['RatingId'] = self.KodiDBIO.create_entry_rating() - self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_movie_obj)) + self.KodiDBIO.add_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.add_rating_movie_obj)) if obj['CriticRating'] is not None: - self.KodiDBIO.add_ratings(*self.Utils.values(dict(obj, RatingId=self.KodiDBIO.create_entry_rating(), RatingType="tomatometerallcritics", Rating=float(obj['CriticRating']/10.0)), queries_videos.add_rating_movie_obj)) + obj['CriticRating'] = float(obj['CriticRating'] / 10.0) + self.KodiDBIO.add_ratings(*self.EmbyServer.Utils.values(dict(obj, RatingId=self.KodiDBIO.create_entry_rating(), RatingType="tomatometerallcritics", Rating=obj['CriticRating']), queries_videos.add_rating_movie_obj)) obj['Unique'] = self.MoviesDBIO.create_entry_unique_id() - self.MoviesDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) + self.MoviesDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -164,29 +155,35 @@ def movie_add(self, obj): if provider != 'imdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.MoviesDBIO.create_entry_unique_id()) - self.MoviesDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) + self.MoviesDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) + + obj['PathId'] = self.KodiDBIO.add_path(*self.EmbyServer.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.EmbyServer.Utils.values(obj, queries_videos.add_file_obj)) + + if self.EmbyServer.Utils.Settings.userRating: + self.MoviesDBIO.add(*self.EmbyServer.Utils.values(obj, queries_videos.add_movie_obj)) + else: + self.MoviesDBIO.add_nouserrating(*self.EmbyServer.Utils.values(obj, queries_videos.add_movie_nouserrating_obj)) - obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) - obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) - self.MoviesDBIO.add(*self.Utils.values(obj, queries_videos.add_movie_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_movie_obj)) - self.LOG.info("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_movie_obj)) + self.LOG.info("ADD movie [%s/%s/%s] %s: %s" % (obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])) #Update object to kodi def movie_update(self, obj): obj = self.Common.Streamdata_add(obj, True) obj['RatingType'] = "default" - obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_movie_obj)) - self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_movie_obj)) + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.EmbyServer.Utils.values(obj, queries_videos.get_rating_movie_obj)) + self.KodiDBIO.update_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.update_rating_movie_obj)) if obj['CriticRating'] is not None: - temp_obj = dict(obj, RatingType="tomatometerallcritics", Rating=float(obj['CriticRating']/10.0)) - temp_obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(temp_obj, queries_videos.get_rating_movie_obj)) - self.KodiDBIO.update_ratings(*self.Utils.values(temp_obj, queries_videos.update_rating_movie_obj)) + obj['CriticRating'] = float(obj['CriticRating'] / 10.0) + temp_obj = dict(obj, RatingType="tomatometerallcritics", Rating=obj['CriticRating']) + temp_obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.get_rating_movie_obj)) + self.KodiDBIO.update_ratings(*self.EmbyServer.Utils.values(temp_obj, queries_videos.update_rating_movie_obj)) - self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_movie_obj)) + self.KodiDBIO.remove_unique_ids(*self.EmbyServer.Utils.values(obj, queries_videos.delete_unique_ids_movie_obj)) obj['Unique'] = self.MoviesDBIO.create_entry_unique_id() - self.MoviesDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) + self.MoviesDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_movie_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -194,89 +191,92 @@ def movie_update(self, obj): if provider != 'imdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.MoviesDBIO.create_entry_unique_id()) - self.MoviesDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) + self.MoviesDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_movie_obj)) - self.MoviesDBIO.update(*self.Utils.values(obj, queries_videos.update_movie_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + if self.EmbyServer.Utils.Settings.userRating: + self.MoviesDBIO.update(*self.EmbyServer.Utils.values(obj, queries_videos.update_movie_obj)) + else: + self.MoviesDBIO.update_nouserrating(*self.EmbyServer.Utils.values(obj, queries_videos.update_movie_nouserrating_obj)) + + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE movie [%s/%s/%s] %s: %s" % (obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])) def trailer(self, obj): try: if obj['LocalTrailer']: - trailer = self.server['api'].get_local_trailers(obj['Id']) - API = helper.api.API(trailer, self.Utils, self.server['auth/server-address']) + trailer = self.EmbyServer.API.get_local_trailers(obj['Id']) - if self.direct_path: - obj['Trailer'] = API.get_file_path(trailer[0]['Path']) - obj['Trailer'] = self.Utils.StringDecode(obj['Trailer']) + if self.EmbyServer.Utils.direct_path: + obj['Trailer'] = self.APIHelper.get_file_path(trailer[0]['Path'], trailer) + obj['Trailer'] = self.EmbyServer.Utils.StringDecode(obj['Trailer']) else: obj['Trailer'] = "plugin://plugin.video.emby-next-gen/trailer?id=%s&mode=play" % trailer[0]['Id'] elif obj['Trailer']: obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] except Exception as error: - self.LOG.error("Failed to get trailer: %s", error) + self.LOG.error("Failed to get trailer: %s" % error) obj['Trailer'] = None #If item does not exist, entry will be added. #If item exists, entry will be updated. #Process movies inside boxset. #Process removals from boxset. - @helper.wrapper.stop def boxset(self, item): e_item = self.emby_db.get_item_by_id(item['Id']) - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Boxset') - obj['Overview'] = API.get_overview(obj['Overview']) + obj['Overview'] = self.APIHelper.get_overview(obj['Overview'], item) obj['Checksum'] = obj['Etag'] - try: + if e_item: obj['SetId'] = e_item[0] - self.MoviesDBIO.update_boxset(*self.Utils.values(obj, queries_videos.update_set_obj)) - except TypeError: - self.LOG.debug("SetId %s not found", obj['Id']) - obj['SetId'] = self.MoviesDBIO.add_boxset(*self.Utils.values(obj, queries_videos.add_set_obj)) + self.MoviesDBIO.update_boxset(*self.EmbyServer.Utils.values(obj, queries_videos.update_set_obj)) + else: + self.LOG.debug("SetId %s not found" % obj['Id']) + obj['SetId'] = self.MoviesDBIO.add_boxset(*self.EmbyServer.Utils.values(obj, queries_videos.add_set_obj)) self.boxset_current(obj) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) for movie in obj['Current']: temp_obj = dict(obj) temp_obj['Movie'] = movie temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']] - self.MoviesDBIO.remove_from_boxset(*self.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) - self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) - self.LOG.info("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId']) + self.MoviesDBIO.remove_from_boxset(*self.EmbyServer.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) + self.emby_db.update_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) + self.LOG.info("DELETE from boxset [%s] %s: %s" % (temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId'])) self.ArtworkDBIO.add(obj['Artwork'], obj['SetId'], "set") - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_boxset_obj)) - self.LOG.info("UPDATE boxset [%s] %s", obj['SetId'], obj['Title']) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_boxset_obj)) + self.LOG.info("UPDATE boxset [%s] %s" % (obj['SetId'], obj['Title'])) + return True #Add or removes movies based on the current movies found in the boxset def boxset_current(self, obj): try: - current = self.emby_db.get_item_id_by_parent_id(*self.Utils.values(obj, database.queries.get_item_id_by_parent_boxset_obj)) + current = self.emby_db.get_item_id_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_id_by_parent_boxset_obj)) movies = dict(current) except ValueError: movies = {} obj['Current'] = movies - for all_movies in self.Downloader.get_movies_by_boxset(obj['Id']): + for all_movies in self.EmbyServer.API.get_movies_by_boxset(obj['Id']): for movie in all_movies['Items']: temp_obj = dict(obj) temp_obj['Title'] = movie['Name'] temp_obj['Id'] = movie['Id'] + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) - try: - temp_obj['MovieId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except TypeError: - self.LOG.info("Failed to process %s to boxset.", temp_obj['Title']) + if Data: + temp_obj['MovieId'] = Data[0] + else: + self.LOG.info("Failed to process %s to boxset." % temp_obj['Title']) continue if temp_obj['Id'] not in obj['Current']: - self.MoviesDBIO.set_boxset(*self.Utils.values(temp_obj, queries_videos.update_movie_set_obj)) - self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.update_parent_movie_obj)) - self.LOG.info("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id']) + self.MoviesDBIO.set_boxset(*self.EmbyServer.Utils.values(temp_obj, queries_videos.update_movie_set_obj)) + self.emby_db.update_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.update_parent_movie_obj)) + self.LOG.info("ADD to boxset [%s/%s] %s: %s to boxset" % (temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id'])) else: obj['Current'].pop(temp_obj['Id']) @@ -288,67 +288,64 @@ def boxsets_reset(self): #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks #Poster with progress bar - @helper.wrapper.stop def userdata(self, item): e_item = self.emby_db.get_item_by_id(item['Id']) - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'MovieUserData') obj['Item'] = item - try: + if e_item: obj['MovieId'] = e_item[0] obj['FileId'] = e_item[1] - except TypeError: + else: return obj = self.Common.Streamdata_add(obj, True) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) if obj['DatePlayed']: - obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") if obj['Favorite']: - self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_movie_obj)) + self.KodiDBIO.get_tag(*self.EmbyServer.Utils.values(obj, queries_videos.get_tag_movie_obj)) else: - self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_movie_obj)) + self.KodiDBIO.remove_tag(*self.EmbyServer.Utils.values(obj, queries_videos.delete_tag_movie_obj)) - self.LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume']) - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title']) + self.LOG.debug("New resume point %s: %s" % (obj['Id'], obj['Resume'])) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA movie [%s/%s] %s: %s" % (obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])) #Remove movieid, fileid, emby reference. #Remove artwork, boxset - @helper.wrapper.stop - def remove(self, item_id=None): + def remove(self, item_id): e_item = self.emby_db.get_item_by_id(item_id) obj = {'Id': item_id} - try: + if e_item: obj['KodiId'] = e_item[0] obj['FileId'] = e_item[1] obj['Media'] = e_item[4] - except TypeError: + else: return self.ArtworkDBIO.delete(obj['KodiId'], obj['Media']) if obj['Media'] == 'movie': - self.MoviesDBIO.delete(*self.Utils.values(obj, queries_videos.delete_movie_obj)) + self.MoviesDBIO.delete(*self.EmbyServer.Utils.values(obj, queries_videos.delete_movie_obj)) elif obj['Media'] == 'set': - for movie in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_movie_obj)): + for movie in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_movie_obj)): temp_obj = dict(obj) temp_obj['MovieId'] = movie[1] temp_obj['Movie'] = movie[0] - self.MoviesDBIO.remove_from_boxset(*self.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) - self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) + self.MoviesDBIO.remove_from_boxset(*self.EmbyServer.Utils.values(temp_obj, queries_videos.delete_movie_set_obj)) + self.emby_db.update_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_parent_boxset_obj)) - self.MoviesDBIO.delete_boxset(*self.Utils.values(obj, queries_videos.delete_set_obj)) + self.MoviesDBIO.delete_boxset(*self.EmbyServer.Utils.values(obj, queries_videos.delete_set_obj)) self.emby_db.remove_item(item_id) - self.LOG.info("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id']) + self.LOG.info("DELETE %s [%s/%s] %s" % (obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'])) class MoviesDBIO(): def __init__(self, cursor): @@ -371,37 +368,34 @@ def create_entry_country(self): return self.cursor.fetchone()[0] + 1 def get(self, *args): - try: - self.cursor.execute(queries_videos.get_movie, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_movie, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def add(self, *args): self.cursor.execute(queries_videos.add_movie, args) + def add_nouserrating(self, *args): + self.cursor.execute(queries_videos.add_movie_nouserrating, args) + def update(self, *args): self.cursor.execute(queries_videos.update_movie, args) + def update_nouserrating(self, *args): + self.cursor.execute(queries_videos.update_movie_nouserrating, args) + def delete(self, kodi_id, file_id): self.cursor.execute(queries_videos.delete_movie, (kodi_id,)) self.cursor.execute(queries_videos.delete_file, (file_id,)) -# def get_unique_id(self, *args): -# try: -# self.cursor.execute(queries_videos.get_unique_id, args) -# return self.cursor.fetchone()[0] -# except TypeError: -# return - # Add the provider id, imdb, tvdb def add_unique_id(self, *args): self.cursor.execute(queries_videos.add_unique_id, args) - # Update the provider id, imdb, tvdb -# def update_unique_id(self, *args): -# self.cursor.execute(queries_videos.update_unique_id, args) - def add_countries(self, countries, *args): for country in countries: self.cursor.execute(queries_videos.update_country, (self.get_country(country),) + args) @@ -412,11 +406,13 @@ def add_country(self, *args): return country_id def get_country(self, *args): - try: - self.cursor.execute(queries_videos.get_country, args) - return self.cursor.fetchone()[0] - except TypeError: - return self.add_country(*args) + self.cursor.execute(queries_videos.get_country, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return self.add_country(*args) def add_boxset(self, *args): set_id = self.create_entry_set() diff --git a/core/music.py b/core/music.py index 0bc4e732a..536dde351 100644 --- a/core/music.py +++ b/core/music.py @@ -1,77 +1,64 @@ # -*- coding: utf-8 -*- +import _strptime # Workaround for threads using datetime: _striptime is locked import datetime -import logging import database.queries import database.emby_db -import helper.wrapper import helper.api +import helper.loghandler from . import obj_ops from . import queries_music from . import artwork from . import common class Music(): - def __init__(self, server, embydb, musicdb, direct_path, Utils): - self.LOG = logging.getLogger("EMBY.core.music.Music") - self.Utils = Utils - self.server = server + def __init__(self, EmbyServer, embydb, musicdb): + self.LOG = helper.loghandler.LOG('EMBY.core.music.Music') + self.EmbyServer = EmbyServer self.emby = embydb self.music = musicdb self.emby_db = database.emby_db.EmbyDatabase(self.emby.cursor) - self.objects = obj_ops.Objects(self.Utils) + self.objects = obj_ops.Objects() self.item_ids = [] - self.DBVersion = int(self.Utils.window('kodidbverion.music')) - self.Common = common.Common(self.emby_db, self.objects, self.Utils, direct_path, self.server) - self.MusicDBIO = MusicDBIO(self.music.cursor, int(self.Utils.window('kodidbverion.music'))) - self.ArtworkDBIO = artwork.Artwork(musicdb.cursor, self.Utils) - - if not self.Utils.settings('MusicRescan.bool'): - self.MusicDBIO.disable_rescan() - self.Utils.settings('MusicRescan.bool', True) - - def __getitem__(self, key): - if key in ('MusicArtist', 'AlbumArtist'): - return self.artist - elif key == 'MusicAlbum': - return self.album - elif key == 'Audio': - return self.song + self.Common = common.Common(self.emby_db, self.objects, self.EmbyServer) + self.MusicDBIO = MusicDBIO(self.music.cursor, self.EmbyServer.Utils.DatabaseFiles['music-version']) + self.ArtworkDBIO = artwork.Artwork(musicdb.cursor, self.EmbyServer.Utils) + self.APIHelper = helper.api.API(self.EmbyServer.Utils) + self.MusicDBIO.disable_rescan() #If item does not exist, entry will be added. #If item exists, entry will be updated - @helper.wrapper.stop - def artist(self, item, library=None): + def artist(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Artist') update = True - try: + if e_item: obj['ArtistId'] = e_item[0] - except TypeError: + + if self.MusicDBIO.validate_artist(*self.EmbyServer.Utils.values(obj, queries_music.get_artist_by_id_obj)) is None: + update = False + self.LOG.info("ArtistId %s missing from kodi. repairing the entry." % obj['ArtistId']) + else: update = False obj['ArtistId'] = None - self.LOG.debug("ArtistId %s not found", obj['Id']) - else: - if self.MusicDBIO.validate_artist(*self.Utils.values(obj, queries_music.get_artist_by_id_obj)) is None: - update = False - self.LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId']) + self.LOG.debug("ArtistId %s not found" % obj['Id']) obj['LibraryId'] = library['Id'] obj['LibraryName'] = library['Name'] obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') obj['ArtistType'] = "MusicArtist" obj['Genre'] = " / ".join(obj['Genres'] or []) - obj['Bio'] = API.get_overview(obj['Bio']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Bio'] = self.APIHelper.get_overview(obj['Bio'], item) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj['Thumb'] = obj['Artwork']['Primary'] obj['Backdrops'] = obj['Artwork']['Backdrop'] or "" + obj['Disambiguation'] = obj['LibraryName'] if obj['Thumb']: obj['Thumb'] = "%s" % obj['Thumb'] @@ -80,14 +67,14 @@ def artist(self, item, library=None): obj['Backdrops'] = "%s" % obj['Backdrops'][0] if obj['DateAdded']: - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") if update: self.artist_update(obj) else: self.artist_add(obj) - if self.DBVersion >= 82: + if self.EmbyServer.Utils.DatabaseFiles['music-version'] >= 82: self.MusicDBIO.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['LastScraped'], obj['SortName'], obj['DateAdded'], obj['ArtistId']) else: self.MusicDBIO.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['SortName'], obj['ArtistId']) @@ -100,37 +87,35 @@ def artist(self, item, library=None): #safety checks: It looks like Emby supports the same artist multiple times. #Kodi doesn't allow that. In case that happens we just merge the artist entries def artist_add(self, obj): - obj['ArtistId'] = self.MusicDBIO.get(*self.Utils.values(obj, queries_music.get_artist_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_artist_obj)) - self.LOG.info("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + obj['ArtistId'] = self.MusicDBIO.get(*self.EmbyServer.Utils.values(obj, queries_music.get_artist_obj)) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_artist_obj)) + self.LOG.info("ADD artist [%s] %s: %s" % (obj['ArtistId'], obj['Name'], obj['Id'])) #Update object to kodi def artist_update(self, obj): - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id']) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE artist [%s] %s: %s" % (obj['ArtistId'], obj['Name'], obj['Id'])) #Update object to kodi - @helper.wrapper.stop - def album(self, item, library=None): + def album(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Album') update = True - try: + if e_item: obj['AlbumId'] = e_item[0] - except TypeError: + + if self.MusicDBIO.validate_album(*self.EmbyServer.Utils.values(obj, queries_music.get_album_by_id_obj)) is None: + update = False + else: update = False obj['AlbumId'] = None - self.LOG.debug("AlbumId %s not found", obj['Id']) - else: - if self.MusicDBIO.validate_album(*self.Utils.values(obj, queries_music.get_album_by_id_obj)) is None: - update = False + self.LOG.debug("AlbumId %s not found" % obj['Id']) obj['LibraryId'] = library['Id'] obj['LibraryName'] = library['Name'] @@ -138,13 +123,14 @@ def album(self, item, library=None): obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') obj['Genres'] = obj['Genres'] or [] obj['Genre'] = " / ".join(obj['Genres']) - obj['Bio'] = API.get_overview(obj['Bio']) + obj['Bio'] = self.APIHelper.get_overview(obj['Bio'], item) obj['Artists'] = " / ".join(obj['Artists'] or []) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj['Thumb'] = obj['Artwork']['Primary'] + obj['UniqueId'] = obj['UniqueId'] or None if obj['DateAdded']: - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") if obj['Thumb']: obj['Thumb'] = "%s" % obj['Thumb'] @@ -152,30 +138,35 @@ def album(self, item, library=None): if update: self.album_update(obj) else: + obj['Type'] = obj['LibraryName'] self.album_add(obj) self.artist_link(obj) self.artist_discography(obj) - - if self.DBVersion >= 82: - self.MusicDBIO.update_album(*self.Utils.values(obj, queries_music.update_album_obj82)) - else: - self.MusicDBIO.update_album(*self.Utils.values(obj, queries_music.update_album_obj)) - self.ArtworkDBIO.add(obj['Artwork'], obj['AlbumId'], "album") self.item_ids.append(obj['Id']) return not update #Add object to kodi def album_add(self, obj): - obj['AlbumId'] = self.MusicDBIO.get_album(*self.Utils.values(obj, queries_music.get_album_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_album_obj)) - self.LOG.info("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + if self.EmbyServer.Utils.DatabaseFiles['music-version'] >= 82: + obj['AlbumId'] = self.MusicDBIO.get_album(*self.EmbyServer.Utils.values(obj, queries_music.get_album_obj82)) + else: + obj['AlbumId'] = self.MusicDBIO.get_album(*self.EmbyServer.Utils.values(obj, queries_music.get_album_obj)) + + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_album_obj)) + self.LOG.info("ADD album [%s] %s: %s" % (obj['AlbumId'], obj['Title'], obj['Id'])) #Update object to kodi def album_update(self, obj): - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + + if self.EmbyServer.Utils.DatabaseFiles['music-version'] >= 82: + self.MusicDBIO.update_album(*self.EmbyServer.Utils.values(obj, queries_music.update_album_obj82)) + else: + self.MusicDBIO.update_album(*self.EmbyServer.Utils.values(obj, queries_music.update_album_obj)) + + self.LOG.info("UPDATE album [%s] %s: %s" % (obj['AlbumId'], obj['Title'], obj['Id'])) #Update the artist's discography def artist_discography(self, obj): @@ -183,14 +174,15 @@ def artist_discography(self, obj): temp_obj = dict(obj) temp_obj['Id'] = artist['Id'] temp_obj['AlbumId'] = obj['Id'] + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) - try: - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except TypeError: + if Data: + temp_obj['ArtistId'] = Data[0] + else: continue - self.MusicDBIO.add_discography(*self.Utils.values(temp_obj, queries_music.update_discography_obj)) - self.emby_db.update_parent_id(*self.Utils.values(temp_obj, database.queries.update_parent_album_obj)) + self.MusicDBIO.add_discography(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_discography_obj)) + self.emby_db.update_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.update_parent_album_obj)) #Assign main artists to album. #Artist does not exist in emby database, create the reference @@ -199,71 +191,77 @@ def artist_link(self, obj): temp_obj = dict(obj) temp_obj['Name'] = artist['Name'] temp_obj['Id'] = artist['Id'] + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) - try: - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except TypeError: - try: - self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except Exception as error: - self.LOG.error(error) + if Data: + temp_obj['ArtistId'] = Data[0] + else: + self.artist(self.EmbyServer.API.get_item(temp_obj['Id']), library=None) + Result = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) + + if Result: + temp_obj['ArtistId'] = Result[0] + else: continue - self.MusicDBIO.update_artist_name(*self.Utils.values(temp_obj, queries_music.update_artist_name_obj)) - self.MusicDBIO.link(*self.Utils.values(temp_obj, queries_music.update_link_obj)) + self.MusicDBIO.update_artist_name(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_artist_name_obj)) + self.MusicDBIO.link(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_link_obj)) self.item_ids.append(temp_obj['Id']) #Update object to kodi - @helper.wrapper.stop - def song(self, item, library=None): + def song(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Song') update = True - try: + if e_item: obj['SongId'] = e_item[0] obj['PathId'] = e_item[2] obj['AlbumId'] = e_item[3] - except TypeError: + + if self.MusicDBIO.validate_song(*self.EmbyServer.Utils.values(obj, queries_music.get_song_by_id_obj)) is None: + update = False + else: update = False obj['SongId'] = self.MusicDBIO.create_entry_song() - self.LOG.debug("SongId %s not found", obj['Id']) - else: - if self.MusicDBIO.validate_song(*self.Utils.values(obj, queries_music.get_song_by_id_obj)) is None: - update = False + self.LOG.debug("SongId %s not found" % obj['Id']) obj['LibraryId'] = library['Id'] obj['LibraryName'] = library['Name'] - obj['Path'] = API.get_file_path(obj['Path']) - obj = self.Common.get_path_filename(obj, "audio") + obj['Path'] = self.APIHelper.get_file_path(obj['Path'], item) + PathValid, obj = self.Common.get_path_filename(obj, "audio") + + if not PathValid: + return "Invalid Filepath" + obj['Rating'] = 0 obj['Genres'] = obj['Genres'] or [] - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0 obj['Genre'] = " / ".join(obj['Genres']) obj['Artists'] = " / ".join(obj['Artists'] or []) obj['AlbumArtists'] = obj['AlbumArtists'] or [] - obj['Index'] = obj['Index'] or 0 + obj['Index'] = obj['Index'] or None obj['Disc'] = obj['Disc'] or 1 obj['EmbedCover'] = False - obj['Comment'] = API.get_overview(obj['Comment']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) + obj['Comment'] = "%s (Library: %s)" % (self.APIHelper.get_overview(obj['Comment'], item), obj['LibraryName']) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj['Thumb'] = obj['Artwork']['Primary'] + obj['UniqueId'] = obj['UniqueId'] or None + obj['Album'] = obj['Album'] or "Single" if obj['DateAdded']: - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") if obj['DatePlayed']: - obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") - if obj['Disc'] != 1: + if obj['Disc'] != 1 and obj['Index']: obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index'] if obj['Thumb']: @@ -274,12 +272,12 @@ def song(self, item, library=None): else: self.song_add(obj) - self.MusicDBIO.add_role(*self.Utils.values(obj, queries_music.update_role_obj)) # defaultt role + self.MusicDBIO.add_role(*self.EmbyServer.Utils.values(obj, queries_music.update_role_obj)) # defaultt role self.song_artist_link(obj) self.song_artist_discography(obj) obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists']) - self.MusicDBIO.get_album_artist(*self.Utils.values(obj, queries_music.get_album_artist_obj)) - self.MusicDBIO.add_genres(*self.Utils.values(obj, queries_music.update_genre_song_obj)) + self.MusicDBIO.get_album_artist(*self.EmbyServer.Utils.values(obj, queries_music.get_album_artist_obj)) + self.MusicDBIO.add_genres(*self.EmbyServer.Utils.values(obj, queries_music.update_genre_song_obj)) self.ArtworkDBIO.add(obj['Artwork'], obj['SongId'], "song") self.item_ids.append(obj['Id']) @@ -292,30 +290,48 @@ def song(self, item, library=None): #Verify if there's an album associated. #If no album found, create a single's album def song_add(self, obj): + AlbumFound = False obj['PathId'] = self.MusicDBIO.add_path(obj['Path']) - try: - obj['AlbumId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_song_obj))[0] - except TypeError: - try: - if obj['SongAlbumId'] is None: - raise TypeError("No album id found associated?") + if obj['SongAlbumId']: + result = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_song_obj)) + + if result: + obj['AlbumId'] = result[0] + AlbumFound = True + + if not AlbumFound: + obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + obj['AlbumId'] = None + BackupTitle = obj['Title'] + obj['Title'] = "--NO INFO--" + obj['Type'] = obj['LibraryName'] + + if self.EmbyServer.Utils.DatabaseFiles['music-version'] >= 82: + obj['AlbumId'] = self.MusicDBIO.get_album(*self.EmbyServer.Utils.values(obj, queries_music.get_single_obj82)) + else: + obj['AlbumId'] = self.MusicDBIO.get_album(*self.EmbyServer.Utils.values(obj, queries_music.get_single_obj)) + + obj['Title'] = BackupTitle - self.album(self.server['api'].get_item(obj['SongAlbumId'])) - obj['AlbumId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_song_obj))[0] - except TypeError: - self.single(obj) + if not self.MusicDBIO.add_song(*self.EmbyServer.Utils.values(obj, queries_music.add_song_obj)): + obj['Index'] = None #Duplicate track number for same album + self.MusicDBIO.add_song(*self.EmbyServer.Utils.values(obj, queries_music.add_song_obj)) - self.MusicDBIO.add_song(*self.Utils.values(obj, queries_music.add_song_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_song_obj)) - self.LOG.info("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_song_obj)) + self.LOG.info("ADD song [%s/%s/%s] %s: %s" % (obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])) + return True # obj #Update object to kodi def song_update(self, obj): - self.MusicDBIO.update_path(*self.Utils.values(obj, queries_music.update_path_obj)) - self.MusicDBIO.update_song(*self.Utils.values(obj, queries_music.update_song_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) + self.MusicDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_music.update_path_obj)) + + if not self.MusicDBIO.update_song(*self.EmbyServer.Utils.values(obj, queries_music.update_song_obj)): + obj['Index'] = None #Duplicate track number for same album + self.MusicDBIO.update_song(*self.EmbyServer.Utils.values(obj, queries_music.update_song_obj)) + + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE song [%s/%s/%s] %s: %s" % (obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])) #Update the artist's discography def song_artist_discography(self, obj): @@ -326,24 +342,26 @@ def song_artist_discography(self, obj): temp_obj['Name'] = artist['Name'] temp_obj['Id'] = artist['Id'] artists.append(temp_obj['Name']) + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) + + if Data: + temp_obj['ArtistId'] = Data[0] + else: + self.artist(self.EmbyServer.API.get_item(temp_obj['Id']), library=None) + Result = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) - try: - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except TypeError: - try: - self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except Exception as error: - self.LOG.error(error) + if Result: + temp_obj['ArtistId'] = Result[0] + else: continue - self.MusicDBIO.link(*self.Utils.values(temp_obj, queries_music.update_link_obj)) + self.MusicDBIO.link(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_link_obj)) self.item_ids.append(temp_obj['Id']) if obj['Album']: temp_obj['Title'] = obj['Album'] temp_obj['Year'] = 0 - self.MusicDBIO.add_discography(*self.Utils.values(temp_obj, queries_music.update_discography_obj)) + self.MusicDBIO.add_discography(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_discography_obj)) obj['AlbumArtists'] = artists @@ -355,120 +373,110 @@ def song_artist_link(self, obj): temp_obj['Name'] = artist['Name'] temp_obj['Id'] = artist['Id'] temp_obj['Index'] = index + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) + + if Data: + temp_obj['ArtistId'] = Data[0] + else: + self.artist(self.EmbyServer.API.get_item(temp_obj['Id']), library=None) + Result = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_obj)) - try: - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except TypeError: - try: - self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) - temp_obj['ArtistId'] = self.emby_db.get_item_by_id(*self.Utils.values(temp_obj, database.queries.get_item_obj))[0] - except Exception as error: - self.LOG.error(error) + if Result: + temp_obj['ArtistId'] = Result[0] + else: continue - self.MusicDBIO.link_song_artist(*self.Utils.values(temp_obj, queries_music.update_song_artist_obj)) + self.MusicDBIO.link_song_artist(*self.EmbyServer.Utils.values(temp_obj, queries_music.update_song_artist_obj)) self.item_ids.append(temp_obj['Id']) - def single(self, obj): - obj['AlbumId'] = self.MusicDBIO.create_entry_album() - obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - if self.DBVersion >= 82: - self.MusicDBIO.add_single(*self.Utils.values(obj, queries_music.add_single_obj82)) - else: - self.MusicDBIO.add_single(*self.Utils.values(obj, queries_music.add_single_obj)) - #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks #Poster with progress bar - @helper.wrapper.stop def userdata(self, item): e_item = self.emby_db.get_item_by_id(item['Id']) obj = self.objects.map(item, 'SongUserData') - try: + if e_item: obj['KodiId'] = e_item[0] obj['Media'] = e_item[4] - except TypeError: + else: return obj['Rating'] = 0 if obj['Media'] == 'song': if obj['DatePlayed']: - obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") - self.MusicDBIO.rate_song(*self.Utils.values(obj, queries_music.update_song_rating_obj)) + self.MusicDBIO.rate_song(*self.EmbyServer.Utils.values(obj, queries_music.update_song_rating_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title']) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA %s [%s] %s: %s" % (obj['Media'], obj['KodiId'], obj['Id'], obj['Title'])) #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks #Poster with progress bar #This should address single song scenario, where server doesn't actually create an album for the song - @helper.wrapper.stop def remove(self, item_id): e_item = self.emby_db.get_item_by_id(item_id) obj = {'Id': item_id} - try: + if e_item: obj['KodiId'] = e_item[0] obj['Media'] = e_item[4] - except TypeError: + else: return if obj['Media'] == 'song': self.remove_song(obj['KodiId'], obj['Id']) self.emby_db.remove_wild_item(obj['Id']) - for item in self.emby_db.get_item_by_wild_id(*self.Utils.values(obj, database.queries.get_item_by_wild_obj)): + for item in self.emby_db.get_item_by_wild_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_wild_obj)): if item[1] == 'album': temp_obj = dict(obj) temp_obj['ParentId'] = item[0] - if not self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + if not self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): self.remove_album(temp_obj['ParentId'], obj['Id']) - elif obj['Media'] == 'album': obj['ParentId'] = obj['KodiId'] - for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_song_obj)): + for song in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_song_obj)): self.remove_song(song[1], obj['Id']) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_song_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_song_obj)) self.remove_album(obj['KodiId'], obj['Id']) elif obj['Media'] == 'artist': obj['ParentId'] = obj['KodiId'] - for album in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): + for album in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): temp_obj = dict(obj) temp_obj['ParentId'] = album[1] - for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + for song in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): self.remove_song(song[1], obj['Id']) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_song_obj)) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_artist_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_item_by_parent_song_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_item_by_parent_artist_obj)) self.remove_album(temp_obj['ParentId'], obj['Id']) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_album_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_album_obj)) self.remove_artist(obj['KodiId'], obj['Id']) - self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) + self.emby_db.remove_item(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_obj)) def remove_artist(self, kodi_id, item_id): self.ArtworkDBIO.delete(kodi_id, "artist") self.MusicDBIO.delete(kodi_id) - self.LOG.info("DELETE artist [%s] %s", kodi_id, item_id) + self.LOG.info("DELETE artist [%s] %s" % (kodi_id, item_id)) def remove_album(self, kodi_id, item_id): self.ArtworkDBIO.delete(kodi_id, "album") self.MusicDBIO.delete_album(kodi_id) - self.LOG.info("DELETE album [%s] %s", kodi_id, item_id) + self.LOG.info("DELETE album [%s] %s" % (kodi_id, item_id)) def remove_song(self, kodi_id, item_id): self.ArtworkDBIO.delete(kodi_id, "song") self.MusicDBIO.delete_song(kodi_id) - self.LOG.info("DELETE song [%s] %s", kodi_id, item_id) + self.LOG.info("DELETE song [%s] %s" % (kodi_id, item_id)) #Get all child elements from tv show emby id def get_child(self, item_id): @@ -476,29 +484,29 @@ def get_child(self, item_id): obj = {'Id': item_id} child = [] - try: + if e_item: obj['KodiId'] = e_item[0] obj['FileId'] = e_item[1] obj['ParentId'] = e_item[3] obj['Media'] = e_item[4] - except TypeError: + else: return child obj['ParentId'] = obj['KodiId'] - for album in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): + for album in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_album_obj)): temp_obj = dict(obj) temp_obj['ParentId'] = album[1] child.append((album[0],)) - for song in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): + for song in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_song_obj)): child.append((song[0],)) return child class MusicDBIO(): def __init__(self, cursor, MusicDBVersion): - self.LOG = logging.getLogger("EMBY.core.music.Music") + self.LOG = helper.loghandler.LOG('EMBY.core.music.Music') self.cursor = cursor self.DBVersion = MusicDBVersion @@ -533,28 +541,31 @@ def add_role(self, *args): self.cursor.execute(queries_music.update_role, args) #Get artist or create the entry - def get(self, artist_id, name, musicbrainz): - try: - self.cursor.execute(queries_music.get_artist, (musicbrainz,)) - result = self.cursor.fetchone() + def get(self, artist_id, name, musicbrainz, LibraryName): + self.cursor.execute(queries_music.get_artist, (musicbrainz,)) + result = self.cursor.fetchone() + + if result: artist_id = result[0] artist_name = result[1] - except TypeError: - artist_id = self.add_artist(artist_id, name, musicbrainz) - else: + if artist_name != name: self.update_artist_name(artist_id, name) + else: + artist_id = self.add_artist(artist_id, name, musicbrainz, LibraryName) return artist_id #Safety check, when musicbrainz does not exist - def add_artist(self, artist_id, name, *args): - try: - self.cursor.execute(queries_music.get_artist_by_name, (name,)) - artist_id = self.cursor.fetchone()[0] - except TypeError: + def add_artist(self, artist_id, name, musicbrainz, LibraryName): + self.cursor.execute(queries_music.get_artist_by_name, (name,)) + artist_id = self.cursor.fetchone() + + if artist_id: + artist_id = artist_id[0] + else: artist_id = artist_id or self.create_entry() - self.cursor.execute(queries_music.add_artist, (artist_id, name,) + args) + self.cursor.execute(queries_music.add_artist, (artist_id, name, musicbrainz, LibraryName)) return artist_id @@ -574,44 +585,62 @@ def add_discography(self, *args): self.cursor.execute(queries_music.update_discography, args) def validate_artist(self, *args): - try: - self.cursor.execute(queries_music.get_artist_by_id, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_music.get_artist_by_id, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def validate_album(self, *args): - try: - self.cursor.execute(queries_music.get_album_by_id, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_music.get_album_by_id, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def validate_song(self, *args): - try: - self.cursor.execute(queries_music.get_song_by_id, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_music.get_song_by_id, args) + Data = self.cursor.fetchone() - def get_album(self, album_id, name, musicbrainz, artists=None, *args): - try: - if musicbrainz is not None: - self.cursor.execute(queries_music.get_album, (musicbrainz,)) - album = None - else: - self.cursor.execute(queries_music.get_album_by_name, (name,)) - album = self.cursor.fetchone() + if Data: + return Data[0] - album_id = (album or self.cursor.fetchone())[0] - except TypeError: - album_id = self.add_album(*(album_id, name, musicbrainz,) + args) + return None + + def get_album(self, album_id, name, musicbrainz, Type, artists, *args): + if musicbrainz: + self.cursor.execute(queries_music.get_album, (musicbrainz,)) + else: + self.cursor.execute(queries_music.get_album_by_name, (name, artists,)) + + album = self.cursor.fetchone() + album_id = (album or self.cursor.fetchone()) + + if album_id: + album_id = album_id[0] + else: + album_id = self.add_album(*(album_id, name, musicbrainz, Type, artists,) + args) return album_id - def add_album(self, album_id, *args): + def add_album(self, album_id, name, musicbrainz, Type, artists, *args): album_id = album_id or self.create_entry_album() - self.cursor.execute(queries_music.add_album, (album_id,) + args) + + if Type == "album": + if self.DBVersion >= 82: + self.cursor.execute(queries_music.add_album82, (album_id, name, musicbrainz, Type, artists,) + args) + else: + self.cursor.execute(queries_music.add_album, (album_id, name, musicbrainz, Type, artists,) + args) + else: #single + if self.DBVersion >= 82: + self.cursor.execute(queries_music.add_single82, (album_id, name, musicbrainz, Type, artists,) + args) + else: + self.cursor.execute(queries_music.add_single, (album_id, name, musicbrainz, Type, artists,) + args) + return album_id def update_album(self, *args): @@ -621,10 +650,12 @@ def update_album(self, *args): self.cursor.execute(queries_music.update_album, args) def get_album_artist(self, album_id, artists): - try: - self.cursor.execute(queries_music.get_album_artist, (album_id,)) - curr_artists = self.cursor.fetchone()[0] - except TypeError: + self.cursor.execute(queries_music.get_album_artist, (album_id,)) + curr_artists = self.cursor.fetchone() + + if curr_artists: + curr_artists = curr_artists[0] + else: return if curr_artists != artists: @@ -633,30 +664,31 @@ def get_album_artist(self, album_id, artists): def update_album_artist(self, *args): self.cursor.execute(queries_music.update_album_artist, args) - def add_single(self, *args): - if self.DBVersion >= 82: - self.cursor.execute(queries_music.add_single82, args) - else: - self.cursor.execute(queries_music.add_single, args) - def add_song(self, *args): - if self.DBVersion >= 82: - self.cursor.execute(queries_music.add_song82, args) - else: - self.cursor.execute(queries_music.add_song, args) + try: #Covers duplicate track numbers in same album + if self.DBVersion >= 82: + self.cursor.execute(queries_music.add_song82, args) + else: + self.cursor.execute(queries_music.add_song, args) + + return True + except: + return False def update_song(self, *args): - if self.DBVersion >= 82: - self.cursor.execute(queries_music.update_song82, args) - else: - self.cursor.execute(queries_music.update_song, args) + try: #Covers duplicate track numbers in same album + if self.DBVersion >= 82: + self.cursor.execute(queries_music.update_song82, args) + else: + self.cursor.execute(queries_music.update_song, args) + + return True + except: + return False def link_song_artist(self, *args): self.cursor.execute(queries_music.update_song_artist, args) -# def link_song_album(self, *args): -# self.cursor.execute(queries_music.update_song_album, args) - def rate_song(self, *args): self.cursor.execute(queries_music.update_song_rating, args) @@ -677,12 +709,13 @@ def add_genres(self, kodi_id, genres, media): self.cursor.execute(queries_music.update_genre_song, (genre_id, kodi_id)) def get_genre(self, *args): - try: - self.cursor.execute(queries_music.get_genre, args) + self.cursor.execute(queries_music.get_genre, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] - return self.cursor.fetchone()[0] - except TypeError: - return self.add_genre(*args) + return self.add_genre(*args) def add_genre(self, *args): genre_id = self.create_entry_genre() @@ -699,11 +732,13 @@ def delete_song(self, *args): self.cursor.execute(queries_music.delete_song, args) def get_path(self, *args): - try: - self.cursor.execute(queries_music.get_path, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_music.get_path, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return def add_path(self, *args): path_id = self.get_path(*args) diff --git a/core/musicvideos.py b/core/musicvideos.py index 7b6c78c56..1ab78d1b4 100644 --- a/core/musicvideos.py +++ b/core/musicvideos.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- +import _strptime # Workaround for threads using datetime: _striptime is locked import datetime -import logging import re import database.queries import database.emby_db -import helper.wrapper import helper.api +import helper.loghandler from . import obj_ops from . import kodi from . import queries_videos @@ -14,39 +14,27 @@ from . import common class MusicVideos(): - def __init__(self, server, embydb, videodb, direct_path, Utils): - self.LOG = logging.getLogger("EMBY.core.musicvideos.MusicVideos") - self.Utils = Utils - self.server = server + def __init__(self, EmbyServer, embydb, videodb): + self.LOG = helper.loghandler.LOG('EMBY.core.musicvideos.MusicVideos') + self.EmbyServer = EmbyServer self.emby = embydb self.video = videodb self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) - self.objects = obj_ops.Objects(self.Utils) + self.objects = obj_ops.Objects() self.item_ids = [] - self.Common = common.Common(self.emby_db, self.objects, self.Utils, direct_path, self.server) + self.Common = common.Common(self.emby_db, self.objects, self.EmbyServer) self.MusicVideosDBIO = MusicVideosDBIO(videodb.cursor) - self.KodiDBIO = kodi.Kodi(videodb.cursor) - self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) + self.KodiDBIO = kodi.Kodi(videodb.cursor, self.EmbyServer.Utils) + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.EmbyServer.Utils) + self.APIHelper = helper.api.API(self.EmbyServer.Utils) - def __getitem__(self, key): - if key == 'MusicVideo': - return self.musicvideo - - @helper.wrapper.stop - def musicvideo(self, item, library=None): - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - - If we don't get the track number from Emby, see if we can infer it - from the sortname attribute. - ''' + def musicvideo(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'MusicVideo') obj['Item'] = item obj['Library'] = library @@ -54,18 +42,18 @@ def musicvideo(self, item, library=None): obj['LibraryName'] = library['Name'] update = True - try: + if e_item: obj['MvideoId'] = e_item[0] obj['FileId'] = e_item[1] obj['PathId'] = e_item[2] - except TypeError: + + if self.MusicVideosDBIO.get(*self.EmbyServer.Utils.values(obj, queries_videos.get_musicvideo_obj)) is None: + update = False + self.LOG.info("MvideoId %s missing from kodi. repairing the entry" % obj['MvideoId']) + else: update = False - self.LOG.debug("MvideoId for %s not found", obj['Id']) + self.LOG.debug("MvideoId for %s not found" % obj['Id']) obj['MvideoId'] = self.MusicVideosDBIO.create_entry() - else: - if self.MusicVideosDBIO.get(*self.Utils.values(obj, queries_videos.get_musicvideo_obj)) is None: - update = False - self.LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId']) obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] @@ -75,37 +63,40 @@ def musicvideo(self, item, library=None): obj['Path'] = obj['Item']['MediaSources'][0]['Path'] #don't use 3d movies as default - if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + if "3d" in self.EmbyServer.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): for DataSource in obj['Item']['MediaSources']: - if not "3d" in self.Utils.StringMod(DataSource['Path']): + if not "3d" in self.EmbyServer.Utils.StringMod(DataSource['Path']): DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') obj['Path'] = DataSource['Path'] obj['MediaSourceID'] = DataSource['Id'] obj['Runtime'] = DataSource['RunTimeTicks'] break - obj['Path'] = API.get_file_path(obj['Path']) + obj['Path'] = self.APIHelper.get_file_path(obj['Path'], item) obj['LibraryId'] = library['Id'] obj['LibraryName'] = library['Name'] obj['Genres'] = obj['Genres'] or [] obj['ArtistItems'] = obj['ArtistItems'] or [] - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] - obj['Plot'] = API.get_overview(obj['Plot']) - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Studios'] = [self.APIHelper.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Plot'] = self.APIHelper.get_overview(obj['Plot'], item) + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['Premiere'] = self.Utils.convert_to_local(obj['Premiere']) if obj['Premiere'] else datetime.date(int(str(obj['Year'])[:4]) if obj['Year'] else 2021, 1, 1) + obj['Premiere'] = self.EmbyServer.Utils.convert_to_local(obj['Premiere']) if obj['Premiere'] else datetime.date(int(str(obj['Year'])[:4]) if obj['Year'] else 2021, 1, 1) obj['Genre'] = " / ".join(obj['Genres']) obj['Studio'] = " / ".join(obj['Studios']) obj['Artists'] = " / ".join(obj['Artists'] or []) obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) - obj = self.Common.get_path_filename(obj, "musicvideos") + obj['Video'] = self.APIHelper.video_streams(obj['Video'] or [], obj['Container'], item) + obj['Audio'] = self.APIHelper.audio_streams(obj['Audio'] or []) + obj['Streams'] = self.APIHelper.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) + PathValid, obj = self.Common.get_path_filename(obj, "musicvideos") + + if not PathValid: + return "Invalid Filepath" if obj['Premiere']: obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ") @@ -114,7 +105,7 @@ def musicvideo(self, item, library=None): artist['Type'] = "Artist" obj['People'] = obj['People'] or [] + obj['ArtistItems'] - obj['People'] = API.get_people_artwork(obj['People']) + obj['People'] = self.APIHelper.get_people_artwork(obj['People']) if obj['Index'] is None and obj['SortTitle'] is not None: search = re.search(r'^\d+\s?', obj['SortTitle']) @@ -136,89 +127,82 @@ def musicvideo(self, item, library=None): else: self.musicvideo_add(obj) - self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_mvideo_obj)) - self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) - self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_mvideo_obj)) - self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_mvideo_obj)) - self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_mvideo_obj)) - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) - self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_mvideo_obj)) - self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) + self.KodiDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_videos.update_path_mvideo_obj)) + self.KodiDBIO.update_file(*self.EmbyServer.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_tags(*self.EmbyServer.Utils.values(obj, queries_videos.add_tags_mvideo_obj)) + self.KodiDBIO.add_genres(*self.EmbyServer.Utils.values(obj, queries_videos.add_genres_mvideo_obj)) + self.KodiDBIO.add_studios(*self.EmbyServer.Utils.values(obj, queries_videos.add_studios_mvideo_obj)) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.add_people(*self.EmbyServer.Utils.values(obj, queries_videos.add_people_mvideo_obj)) + self.KodiDBIO.add_streams(*self.EmbyServer.Utils.values(obj, queries_videos.add_streams_obj)) self.ArtworkDBIO.add(obj['Artwork'], obj['MvideoId'], "musicvideo") self.item_ids.append(obj['Id']) if "StackTimes" in obj: - self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + self.KodiDBIO.add_stacktimes(*self.EmbyServer.Utils.values(obj, queries_videos.add_stacktimes_obj)) return not update #Add object to kodi def musicvideo_add(self, obj): obj = self.Common.Streamdata_add(obj, False) - obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) - obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) - self.MusicVideosDBIO.add(*self.Utils.values(obj, queries_videos.add_musicvideo_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_mvideo_obj)) - self.LOG.info("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + obj['PathId'] = self.KodiDBIO.add_path(*self.EmbyServer.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.EmbyServer.Utils.values(obj, queries_videos.add_file_obj)) + self.MusicVideosDBIO.add(*self.EmbyServer.Utils.values(obj, queries_videos.add_musicvideo_obj)) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_mvideo_obj)) + self.LOG.info("ADD mvideo [%s/%s/%s] %s: %s" % (obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])) #Update object to kodi def musicvideo_update(self, obj): obj = self.Common.Streamdata_add(obj, True) - self.MusicVideosDBIO.update(*self.Utils.values(obj, queries_videos.update_musicvideo_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + self.MusicVideosDBIO.update(*self.EmbyServer.Utils.values(obj, queries_videos.update_musicvideo_obj)) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE mvideo [%s/%s/%s] %s: %s" % (obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])) - @helper.wrapper.stop def userdata(self, item): - ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - Poster with progress bar - ''' e_item = self.emby_db.get_item_by_id(item['Id']) - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'MusicVideoUserData') obj['Item'] = item - try: + if e_item: obj['MvideoId'] = e_item[0] obj['FileId'] = e_item[1] - except TypeError: + else: return obj = self.Common.Streamdata_add(obj, True) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) if obj['DatePlayed']: - obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") if obj['Favorite']: - self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_mvideo_obj)) + self.KodiDBIO.get_tag(*self.EmbyServer.Utils.values(obj, queries_videos.get_tag_mvideo_obj)) else: - self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_mvideo_obj)) + self.KodiDBIO.remove_tag(*self.EmbyServer.Utils.values(obj, queries_videos.delete_tag_mvideo_obj)) - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA mvideo [%s/%s] %s: %s" % (obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])) #Remove mvideoid, fileid, pathid, emby reference - @helper.wrapper.stop def remove(self, item_id): e_item = self.emby_db.get_item_by_id(item_id) obj = {'Id': item_id} - try: + if e_item: obj['MvideoId'] = e_item[0] obj['FileId'] = e_item[1] obj['PathId'] = e_item[2] - except TypeError: + else: return self.ArtworkDBIO.delete(obj['MvideoId'], "musicvideo") - self.MusicVideosDBIO.delete(*self.Utils.values(obj, queries_videos.delete_musicvideo_obj)) - self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) - self.LOG.info("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id']) - + self.MusicVideosDBIO.delete(*self.EmbyServer.Utils.values(obj, queries_videos.delete_musicvideo_obj)) + self.emby_db.remove_item(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_obj)) + self.LOG.info("DELETE musicvideo %s [%s/%s] %s" % (obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id'])) class MusicVideosDBIO(): def __init__(self, cursor): @@ -229,11 +213,13 @@ def create_entry(self): return self.cursor.fetchone()[0] + 1 def get(self, *args): - try: - self.cursor.execute(queries_videos.get_musicvideo, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_musicvideo, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def add(self, *args): self.cursor.execute(queries_videos.add_musicvideo, args) diff --git a/core/obj_map.json b/core/obj_map.json deleted file mode 100644 index bde0f1c3a..000000000 --- a/core/obj_map.json +++ /dev/null @@ -1,487 +0,0 @@ -{ - "video": "special://database/MyVideos119.db", - "music": "special://database/MyMusic82.db", - "texture": "special://database/Textures13.db", - "emby": "special://database/emby.db", - "MovieProviderName": "imdb", - "Movie": { - "Id": "Id", - "Title": "Name", - "SortTitle": "SortName", - "Path": "Path", - "Genres": "Genres", - "UniqueId": "ProviderIds/Imdb", - "UniqueIds": "ProviderIds", - "Rating": "CommunityRating", - "Year": "ProductionYear", - "Votes": "VoteCount", - "Plot": "Overview", - "ShortPlot": "ShortOverview", - "People": "People", - "Writers": "People:?Type=Writer$Name", - "Directors": "People:?Type=Director$Name", - "Cast": "People:?Type=Actor$Name", - "Tagline": "Taglines/0", - "Mpaa": "OfficialRating", - "Country": "ProductionLocations/0", - "Countries": "ProductionLocations", - "Studios": "Studios:?$Name", - "Studio": "Studios/0/Name", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "LocalTrailer": "LocalTrailerCount", - "Trailer": "RemoteTrailers/0/Url", - "DateAdded": "DateCreated", - "Premiered": "PremiereDate", - "Played": "UserData/Played", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "Favorite": "UserData/IsFavorite", - "Resume": "UserData/PlaybackPositionTicks", - "Tags": "Tags", - "TagItems": "TagItems:?$Name", - "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", - "Audio": "MediaSources/0/MediaStreams:?Type=Audio", - "Video": "MediaSources/0/MediaStreams:?Type=Video", - "Container": "MediaSources/0/Container", - "EmbyParentId": "ParentId", - "CriticRating": "CriticRating", - "PresentationKey": "PresentationUniqueKey", - "OriginalTitle": "OriginalTitle" - }, - "MovieUserData": { - "Id": "Id", - "Title": "Name", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Resume": "UserData/PlaybackPositionTicks", - "Favorite": "UserData/IsFavorite", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "Played": "UserData/Played", - "PresentationKey": "PresentationUniqueKey" - }, - "Boxset": { - "Id": "Id", - "Title": "Name", - "Overview": "Overview", - "PresentationKey": "PresentationUniqueKey", - "Etag": "Etag" - }, - "SeriesProviderName": "tvdb", - "Series": { - "Id": "Id", - "Title": "Name", - "SortTitle": "SortName", - "People": "People", - "Path": "Path", - "Genres": "Genres", - "Plot": "Overview", - "Rating": "CommunityRating", - "Year": "ProductionYear", - "Votes": "VoteCount", - "Premiere": "PremiereDate", - "UniqueId": "ProviderIds/Tvdb", - "UniqueIds": "ProviderIds", - "Mpaa": "OfficialRating", - "Studios": "Studios:?$Name", - "Tags": "Tags", - "TagItems": "TagItems:?$Name", - "Favorite": "UserData/IsFavorite", - "RecursiveCount": "RecursiveItemCount", - "EmbyParentId": "ParentId", - "Status": "Status", - "PresentationKey": "PresentationUniqueKey", - "OriginalTitle": "OriginalTitle" - }, - "Season": { - "Id": "Id", - "Index": "IndexNumber", - "SeriesId": "SeriesId", - "Location": "LocationType", - "Title": "Name", - "PresentationKey": "PresentationUniqueKey" - }, - "EpisodeProviderName": "tvdb", - "Episode": { - "Id": "Id", - "Title": "Name", - "Path": "Path", - "Plot": "Overview", - "People": "People", - "Rating": "CommunityRating", - "Writers": "People:?Type=Writer$Name", - "Directors": "People:?Type=Director$Name", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Premiere": "PremiereDate", - "Votes": "VoteCount", - "UniqueId": "ProviderIds/Tvdb", - "UniqueIds": "ProviderIds", - "SeriesId": "SeriesId", - "Season": "ParentIndexNumber", - "Index": "IndexNumber", - "AbsoluteNumber": "AbsoluteEpisodeNumber", - "AirsAfterSeason": "AirsAfterSeasonNumber", - "AirsBeforeSeason": "AirsBeforeSeasonNumber,SortParentIndexNumber", - "AirsBeforeEpisode": "AirsBeforeEpisodeNumber,SortIndexNumber", - "MultiEpisode": "IndexNumberEnd", - "Played": "UserData/Played", - "PlayCount": "UserData/PlayCount", - "DateAdded": "DateCreated", - "DatePlayed": "UserData/LastPlayedDate", - "Resume": "UserData/PlaybackPositionTicks", - "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", - "Audio": "MediaSources/0/MediaStreams:?Type=Audio", - "Video": "MediaSources/0/MediaStreams:?Type=Video", - "Container": "MediaSources/0/Container", - "Location": "LocationType", - "EmbyParentId": "SeriesId,ParentId", - "PresentationKey": "PresentationUniqueKey", - "OriginalTitle": "OriginalTitle" - }, - "EpisodeUserData": { - "Id": "Id", - "Title": "Name", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Resume": "UserData/PlaybackPositionTicks", - "Favorite": "UserData/IsFavorite", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "DateAdded": "DateCreated", - "Played": "UserData/Played", - "PresentationKey": "PresentationUniqueKey" - }, - "MusicVideo": { - "Id": "Id", - "Title": "Name", - "Path": "Path", - "DateAdded": "DateCreated", - "DatePlayed": "UserData/LastPlayedDate", - "PlayCount": "UserData/PlayCount", - "Resume": "UserData/PlaybackPositionTicks", - "SortTitle": "SortName", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Plot": "Overview", - "Year": "ProductionYear", - "Premiere": "PremiereDate", - "Genres": "Genres", - "Studios": "Studios?$Name", - "Artists": "ArtistItems:?$Name", - "ArtistItems": "ArtistItems", - "Album": "Album", - "Index": "Track", - "People": "People", - "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", - "Audio": "MediaSources/0/MediaStreams:?Type=Audio", - "Video": "MediaSources/0/MediaStreams:?Type=Video", - "Container": "MediaSources/0/Container", - "Tags": "Tags", - "TagItems": "TagItems:?$Name", - "Played": "UserData/Played", - "Favorite": "UserData/IsFavorite", - "Directors": "People:?Type=Director$Name", - "EmbyParentId": "ParentId", - "PresentationKey": "PresentationUniqueKey" - }, - "MusicVideoUserData": { - "Id": "Id", - "Title": "Name", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Resume": "UserData/PlaybackPositionTicks", - "Favorite": "UserData/IsFavorite", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "Played": "UserData/Played" - }, - "Artist": { - "Id": "Id", - "Name": "Name", - "UniqueId": "ProviderIds/MusicBrainzArtist", - "Genres": "Genres", - "Bio": "Overview", - "EmbyParentId": "ParentId", - "DateAdded": "DateCreated", - "SortName": "SortName", - "PresentationKey": "PresentationUniqueKey" - }, - "Album": { - "Id": "Id", - "Title": "Name", - "UniqueId": "ProviderIds/MusicBrainzAlbum", - "Year": "ProductionYear", - "Genres": "Genres", - "Bio": "Overview", - "AlbumArtists": "AlbumArtists", - "Artists": "AlbumArtists:?$Name", - "ArtistItems": "ArtistItems", - "EmbyParentId": "ParentId", - "DateAdded": "DateCreated", - "PresentationKey": "PresentationUniqueKey" - }, - "Song": { - "Id": "Id", - "Title": "Name", - "Path": "Path", - "DateAdded": "DateCreated", - "Played": "UserData/Played", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "UniqueId": "ProviderIds/MusicBrainzTrackId", - "Genres": "Genres", - "Artists": "ArtistItems:?$Name", - "Index": "IndexNumber", - "Disc": "ParentIndexNumber", - "Year": "ProductionYear", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Comment": "Overview", - "ArtistItems": "ArtistItems", - "AlbumArtists": "AlbumArtists", - "Album": "Album", - "SongAlbumId": "AlbumId", - "Container": "MediaSources/0/Container", - "EmbyParentId": "ParentId", - "PresentationKey": "PresentationUniqueKey" - }, - "SongUserData": { - "Id": "Id", - "Title": "Name", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "DateAdded": "DateCreated", - "Played": "UserData/Played", - "PresentationKey": "PresentationUniqueKey" - }, - "Artwork": { - "Id": "Id", - "Tags": "ImageTags", - "BackdropTags": "BackdropImageTags" - }, - "ArtworkParent": { - "Id": "Id", - "Tags": "ImageTags", - "BackdropTags": "BackdropImageTags", - "ParentBackdropId": "ParentBackdropItemId", - "ParentBackdropTags": "ParentBackdropImageTags", - "ParentLogoId": "ParentLogoItemId", - "ParentLogoTag": "ParentLogoImageTag", - "ParentArtId": "ParentArtItemId", - "ParentArtTag": "ParentArtImageTag", - "ParentThumbId": "ParentThumbItemId", - "ParentThumbTag": "ParentThumbTag", - "SeriesTag": "SeriesPrimaryImageTag", - "SeriesId": "SeriesId" - }, - "ArtworkMusic": { - "Id": "Id", - "Tags": "ImageTags", - "BackdropTags": "BackdropImageTags", - "ParentBackdropId": "ParentBackdropItemId", - "ParentBackdropTags": "ParentBackdropImageTags", - "ParentLogoId": "ParentLogoItemId", - "ParentLogoTag": "ParentLogoImageTag", - "ParentArtId": "ParentArtItemId", - "ParentArtTag": "ParentArtImageTag", - "ParentThumbId": "ParentThumbItemId", - "ParentThumbTag": "ParentThumbTag", - "AlbumId": "AlbumId", - "AlbumTag": "AlbumPrimaryImageTag" - }, - "BrowseVideo": { - "Id": "Id", - "Title": "Name", - "Type": "Type", - "Plot": "Overview", - "Year": "ProductionYear", - "Writers": "People:?Type=Writer$Name", - "Directors": "People:?Type=Director$Name", - "Cast": "People:?Type=Actor$Name", - "Mpaa": "OfficialRating", - "Genres": "Genres", - "Studios": "Studios:?$Name,SeriesStudio", - "Premiere": "PremiereDate,DateCreated", - "Rating": "CommunityRating", - "Votes": "VoteCount", - "Season": "ParentIndexNumber", - "Index": "IndexNumber,AbsoluteEpisodeNumber", - "SeriesName": "SeriesName", - "Countries": "ProductionLocations", - "Played": "UserData/Played", - "People": "People", - "ShortPlot": "ShortOverview", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Tagline": "Taglines/0", - "UniqueId": "ProviderIds/Imdb", - "DatePlayed": "UserData/LastPlayedDate", - "Artists": "ArtistItems:?$Name", - "Album": "Album", - "Votes": "VoteCount", - "Path": "Path", - "LocalTrailer": "LocalTrailerCount", - "Trailer": "RemoteTrailers/0/Url", - "DateAdded": "DateCreated", - "SortTitle": "SortName", - "PlayCount": "UserData/PlayCount", - "Resume": "UserData/PlaybackPositionTicks", - "Subtitles": "MediaStreams:?Type=Subtitle$Language", - "Audio": "MediaStreams:?Type=Audio", - "Video": "MediaStreams:?Type=Video", - "Container": "Container", - "Unwatched": "UserData/UnplayedItemCount", - "ChildCount": "ChildCount", - "RecursiveCount": "RecursiveItemCount", - "MediaType": "MediaType", - "CriticRating": "CriticRating", - "Status": "Status", - "OriginalTitle": "OriginalTitle" - }, - "BrowseAudio": { - "Id": "Id", - "Title": "Name", - "Type": "Type", - "Index": "IndexNumber", - "Disc": "ParentIndexNumber", - "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", - "Year": "ProductionYear", - "Genre": "Genres/0", - "Album": "Album", - "Artists": "ArtistItems/0/Name", - "Rating": "CommunityRating", - "PlayCount": "UserData/PlayCount", - "DatePlayed": "UserData/LastPlayedDate", - "UniqueId": "ProviderIds/MusicBrainzTrackId,ProviderIds/MusicBrainzAlbum,ProviderIds/MusicBrainzArtist", - "Comment": "Overview", - "FileDate": "DateCreated", - "Played": "UserData/Played" - }, - "BrowsePhoto": { - "Id": "Id", - "Title": "Name", - "Type": "Type", - "FileDate": "DateCreated", - "Width": "Width", - "Height": "Height", - "Size": "Size", - "Overview": "Overview", - "CameraMake": "CameraMake", - "CameraModel": "CameraModel", - "ExposureTime": "ExposureTime", - "FocalLength": "FocalLength" - }, - "BrowseFolder": { - "Id": "Id", - "Title": "Name", - "Type": "Type", - "Overview": "Overview" - }, - "BrowseChannel": { - "Id": "Id", - "Title": "Name", - "Type": "Type", - "ProgramName": "CurrentProgram/Name", - "Played": "CurrentProgram/UserData/Played", - "PlayCount": "CurrentProgram/UserData/PlayCount", - "Runtime": "CurrentProgram/RunTimeTicks", - "MediaType": "MediaType" - }, - "MediaSources": { - "emby_id": "emby_id", - "MediaIndex": "MediaIndex", - "Protocol": "Protocol", - "Id": "Id", - "Path": "Path", - "Type": "Type", - "Container": "Container", - "Size": "Size", - "Name": "Name", - "IsRemote": "IsRemote", - "RunTimeTicks": "RunTimeTicks", - "SupportsTranscoding": "SupportsTranscoding", - "SupportsDirectStream": "SupportsDirectStream", - "SupportsDirectPlay": "SupportsDirectPlay", - "IsInfiniteStream": "IsInfiniteStream", - "RequiresOpening": "RequiresOpening", - "RequiresClosing": "RequiresClosing", - "RequiresLooping": "RequiresLooping", - "SupportsProbing": "SupportsProbing", - "Formats": "Formats", - "Bitrate": "Bitrate", - "RequiredHttpHeaders": "RequiredHttpHeaders", - "ReadAtNativeFramerate": "ReadAtNativeFramerate", - "DefaultAudioStreamIndex": "DefaultAudioStreamIndex" - }, - "AudioStreams": { - "emby_id": "emby_id", - "MediaIndex": "MediaIndex", - "AudioIndex": "AudioIndex", - "StreamIndex": "StreamIndex", - "Codec": "Codec", - "Language": "Language", - "TimeBase": "TimeBase", - "CodecTimeBase": "CodecTimeBase", - "DisplayTitle": "DisplayTitle", - "DisplayLanguage": "DisplayLanguage", - "IsInterlaced": "IsInterlaced", - "ChannelLayout": "ChannelLayout", - "BitRate": "BitRate", - "Channels": "Channels", - "SampleRate": "SampleRate", - "IsDefault": "IsDefault", - "IsForced": "IsForced", - "Profile": "Profile", - "Type": "Type", - "IsExternal": "IsExternal", - "IsTextSubtitleStream": "IsTextSubtitleStream", - "SupportsExternalStream": "SupportsExternalStream", - "Protocol": "Protocol" - }, - "VideoStreams": { - "emby_id": "emby_id", - "MediaIndex": "MediaIndex", - "VideoIndex": "VideoIndex", - "StreamIndex": "StreamIndex", - "Codec": "Codec", - "TimeBase": "TimeBase", - "CodecTimeBase": "CodecTimeBase", - "VideoRange": "VideoRange", - "DisplayTitle": "DisplayTitle", - "IsInterlaced": "IsInterlaced", - "BitRate": "BitRate", - "BitDepth": "BitDepth", - "RefFrames": "RefFrames", - "IsDefault": "IsDefault", - "IsForced": "IsForced", - "Height": "Height", - "Width": "Width", - "AverageFrameRate": "AverageFrameRate", - "RealFrameRate": "RealFrameRate", - "Profile": "Profile", - "Type": "Type", - "AspectRatio": "AspectRatio", - "IsExternal": "IsExternal", - "IsTextSubtitleStream": "IsTextSubtitleStream", - "SupportsExternalStream": "SupportsExternalStream", - "Protocol": "Protocol", - "PixelFormat": "PixelFormat", - "Level": "Level", - "IsAnamorphic": "IsAnamorphic" - }, - "Subtitles": { - "emby_id": "emby_id", - "MediaIndex": "MediaIndex", - "SubtitleIndex": "SubtitleIndex", - "StreamIndex": "StreamIndex", - "IsForced": "IsForced", - "IsInterlaced": "IsInterlaced", - "DisplayTitle": "DisplayTitle", - "SupportsExternalStream": "SupportsExternalStream", - "Language": "Language", - "DisplayLanguage": "DisplayLanguage", - "Codec": "Codec", - "CodecTimeBase": "CodecTimeBase", - "Protocol": "Protocol", - "Type": "Type", - "Path": "Path", - "TimeBase": "TimeBase", - "IsTextSubtitleStream": "IsTextSubtitleStream", - "IsDefault": "IsDefault", - "IsExternal": "IsExternal" - } -} diff --git a/core/obj_ops.py b/core/obj_ops.py index cc6e0235f..e8e8d256f 100644 --- a/core/obj_ops.py +++ b/core/obj_ops.py @@ -1,26 +1,506 @@ # -*- coding: utf-8 -*- import json -import os class Objects(): - def __init__(self, Utils): - self.Utils = Utils - - if not self.Utils.window('emby_objects.mapping'): - infile = open(os.path.join(os.path.dirname(__file__), 'obj_map.json'), 'r') - Value = infile.read() - self.Utils.window('emby_objects.mapping', value=Value) - infile.close() - - self.objects = json.loads(self.Utils.window('emby_objects.mapping')) + def __init__(self): self.mapped_item = {} + self.objects = { + "MovieProviderName": "imdb", + "Movie": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "Path": "Path", + "Genres": "Genres", + "UniqueId": "ProviderIds/Imdb", + "UniqueIds": "ProviderIds", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Plot": "Overview", + "ShortPlot": "ShortOverview", + "People": "People", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Tagline": "Taglines/0", + "Mpaa": "OfficialRating", + "Country": "ProductionLocations/0", + "Countries": "ProductionLocations", + "Studios": "Studios:?$Name", + "Studio": "Studios/0/Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "Premiered": "PremiereDate", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Favorite": "UserData/IsFavorite", + "Resume": "UserData/PlaybackPositionTicks", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId", + "CriticRating": "CriticRating", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "MovieUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "Boxset": { + "Id": "Id", + "Title": "Name", + "Overview": "Overview", + "PresentationKey": "PresentationUniqueKey", + "Etag": "Etag" + }, + "SeriesProviderName": "tvdb", + "Series": { + "Id": "Id", + "Title": "Name", + "SortTitle": "SortName", + "People": "People", + "Path": "Path", + "Genres": "Genres", + "Plot": "Overview", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Votes": "VoteCount", + "Premiere": "PremiereDate", + "UniqueId": "ProviderIds/Tvdb", + "UniqueIds": "ProviderIds", + "Mpaa": "OfficialRating", + "Studios": "Studios:?$Name", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Favorite": "UserData/IsFavorite", + "RecursiveCount": "RecursiveItemCount", + "EmbyParentId": "ParentId", + "Status": "Status", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "Season": { + "Id": "Id", + "Index": "IndexNumber", + "SeriesId": "SeriesId", + "Location": "LocationType", + "Title": "Name", + "EmbyParentId": "ParentId", + "PresentationKey": "PresentationUniqueKey" + }, + "EpisodeProviderName": "tvdb", + "Episode": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "Plot": "Overview", + "People": "People", + "Rating": "CommunityRating", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Premiere": "PremiereDate", + "Votes": "VoteCount", + "UniqueId": "ProviderIds/Tvdb", + "UniqueIds": "ProviderIds", + "SeriesId": "SeriesId", + "Season": "ParentIndexNumber", + "Index": "IndexNumber", + "AbsoluteNumber": "AbsoluteEpisodeNumber", + "AirsAfterSeason": "AirsAfterSeasonNumber", + "AirsBeforeSeason": "AirsBeforeSeasonNumber,SortParentIndexNumber", + "AirsBeforeEpisode": "AirsBeforeEpisodeNumber,SortIndexNumber", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Location": "LocationType", + "EmbyParentId": "SeriesId,ParentId", + "PresentationKey": "PresentationUniqueKey", + "OriginalTitle": "OriginalTitle" + }, + "EpisodeUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "MusicVideo": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "DatePlayed": "UserData/LastPlayedDate", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "SortTitle": "SortName", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Plot": "Overview", + "Year": "ProductionYear", + "Premiere": "PremiereDate", + "Genres": "Genres", + "Studios": "Studios?$Name", + "Artists": "ArtistItems:?$Name", + "ArtistItems": "ArtistItems", + "Album": "Album", + "Index": "Track", + "People": "People", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "Tags": "Tags", + "TagItems": "TagItems:?$Name", + "Played": "UserData/Played", + "Favorite": "UserData/IsFavorite", + "Directors": "People:?Type=Director$Name", + "EmbyParentId": "ParentId", + "PresentationKey": "PresentationUniqueKey" + }, + "MusicVideoUserData": { + "Id": "Id", + "Title": "Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Resume": "UserData/PlaybackPositionTicks", + "Favorite": "UserData/IsFavorite", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "Artist": { + "Id": "Id", + "Name": "Name", + "UniqueId": "ProviderIds/MusicBrainzArtist", + "Genres": "Genres", + "Bio": "Overview", + "EmbyParentId": "ParentId", + "DateAdded": "DateCreated", + "SortName": "SortName", + "PresentationKey": "PresentationUniqueKey" + }, + "Album": { + "Id": "Id", + "Title": "Name", + "UniqueId": "ProviderIds/MusicBrainzAlbum", + "Year": "ProductionYear", + "Genres": "Genres", + "Bio": "Overview", + "AlbumArtists": "AlbumArtists", + "Artists": "AlbumArtists:?$Name", + "ArtistItems": "ArtistItems", + "EmbyParentId": "ParentId", + "DateAdded": "DateCreated", + "PresentationKey": "PresentationUniqueKey" + }, + "Song": { + "Id": "Id", + "Title": "Name", + "Path": "Path", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId", + "Genres": "Genres", + "Artists": "ArtistItems:?$Name", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Year": "ProductionYear", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Comment": "Overview", + "ArtistItems": "ArtistItems", + "AlbumArtists": "AlbumArtists", + "Album": "Album", + "SongAlbumId": "AlbumId", + "Container": "MediaSources/0/Container", + "EmbyParentId": "ParentId", + "PresentationKey": "PresentationUniqueKey" + }, + "SongUserData": { + "Id": "Id", + "Title": "Name", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PresentationKey": "PresentationUniqueKey" + }, + "Artwork": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags" + }, + "ArtworkParent": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "SeriesTag": "SeriesPrimaryImageTag", + "SeriesId": "SeriesId" + }, + "ArtworkMusic": { + "Id": "Id", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags", + "ParentBackdropId": "ParentBackdropItemId", + "ParentBackdropTags": "ParentBackdropImageTags", + "ParentLogoId": "ParentLogoItemId", + "ParentLogoTag": "ParentLogoImageTag", + "ParentArtId": "ParentArtItemId", + "ParentArtTag": "ParentArtImageTag", + "ParentThumbId": "ParentThumbItemId", + "ParentThumbTag": "ParentThumbTag", + "AlbumId": "AlbumId", + "AlbumTag": "AlbumPrimaryImageTag" + }, + "BrowseVideo": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Plot": "Overview", + "Year": "ProductionYear", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Mpaa": "OfficialRating", + "Genres": "Genres", + "Studios": "Studios:?$Name,SeriesStudio", + "Premiere": "PremiereDate,DateCreated", + "Rating": "CommunityRating", + "Votes": "VoteCount", + "Season": "ParentIndexNumber", + "Index": "IndexNumber,AbsoluteEpisodeNumber", + "SeriesName": "SeriesName", + "Countries": "ProductionLocations", + "Played": "UserData/Played", + "People": "People", + "ShortPlot": "ShortOverview", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Tagline": "Taglines/0", + "UniqueId": "ProviderIds/Imdb", + "DatePlayed": "UserData/LastPlayedDate", + "Artists": "ArtistItems:?$Name", + "Album": "Album", + "Path": "Path", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "SortTitle": "SortName", + "PlayCount": "UserData/PlayCount", + "Resume": "UserData/PlaybackPositionTicks", + "Subtitles": "MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaStreams:?Type=Audio", + "Video": "MediaStreams:?Type=Video", + "Container": "Container", + "Unwatched": "UserData/UnplayedItemCount", + "ChildCount": "ChildCount", + "RecursiveCount": "RecursiveItemCount", + "MediaType": "MediaType", + "CriticRating": "CriticRating", + "Status": "Status", + "OriginalTitle": "OriginalTitle" + }, + "BrowseAudio": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Index": "IndexNumber", + "Disc": "ParentIndexNumber", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "Year": "ProductionYear", + "Genre": "Genres/0", + "Album": "Album", + "Artists": "ArtistItems/0/Name", + "Rating": "CommunityRating", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "UniqueId": "ProviderIds/MusicBrainzTrackId,ProviderIds/MusicBrainzAlbum,ProviderIds/MusicBrainzArtist", + "Comment": "Overview", + "DateAdded": "DateCreated", + "Played": "UserData/Played" + }, + "BrowsePhoto": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Width": "Width", + "Height": "Height", + "Size": "Size", + "Overview": "Overview", + "CameraMake": "CameraMake", + "CameraModel": "CameraModel", + "ExposureTime": "ExposureTime", + "FocalLength": "FocalLength", + "DateAdded": "DateCreated" + }, + "BrowseFolder": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Overview": "Overview" + }, + "BrowseGenre": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "Tags": "ImageTags", + "BackdropTags": "BackdropImageTags" + }, + "BrowseChannel": { + "Id": "Id", + "Title": "Name", + "Type": "Type", + "ProgramName": "CurrentProgram/Name", + "Played": "CurrentProgram/UserData/Played", + "PlayCount": "CurrentProgram/UserData/PlayCount", + "Runtime": "CurrentProgram/RunTimeTicks", + "MediaType": "MediaType" + }, + "MediaSources": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "Protocol": "Protocol", + "Id": "Id", + "Path": "Path", + "Type": "Type", + "Container": "Container", + "Size": "Size", + "Name": "Name", + "IsRemote": "IsRemote", + "RunTimeTicks": "RunTimeTicks", + "SupportsTranscoding": "SupportsTranscoding", + "SupportsDirectStream": "SupportsDirectStream", + "SupportsDirectPlay": "SupportsDirectPlay", + "IsInfiniteStream": "IsInfiniteStream", + "RequiresOpening": "RequiresOpening", + "RequiresClosing": "RequiresClosing", + "RequiresLooping": "RequiresLooping", + "SupportsProbing": "SupportsProbing", + "Formats": "Formats", + "Bitrate": "Bitrate", + "RequiredHttpHeaders": "RequiredHttpHeaders", + "ReadAtNativeFramerate": "ReadAtNativeFramerate", + "DefaultAudioStreamIndex": "DefaultAudioStreamIndex" + }, + "AudioStreams": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "AudioIndex": "AudioIndex", + "StreamIndex": "StreamIndex", + "Codec": "Codec", + "Language": "Language", + "TimeBase": "TimeBase", + "CodecTimeBase": "CodecTimeBase", + "DisplayTitle": "DisplayTitle", + "DisplayLanguage": "DisplayLanguage", + "IsInterlaced": "IsInterlaced", + "ChannelLayout": "ChannelLayout", + "BitRate": "BitRate", + "Channels": "Channels", + "SampleRate": "SampleRate", + "IsDefault": "IsDefault", + "IsForced": "IsForced", + "Profile": "Profile", + "Type": "Type", + "IsExternal": "IsExternal", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "SupportsExternalStream": "SupportsExternalStream", + "Protocol": "Protocol" + }, + "VideoStreams": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "VideoIndex": "VideoIndex", + "StreamIndex": "StreamIndex", + "Codec": "Codec", + "TimeBase": "TimeBase", + "CodecTimeBase": "CodecTimeBase", + "VideoRange": "VideoRange", + "DisplayTitle": "DisplayTitle", + "IsInterlaced": "IsInterlaced", + "BitRate": "BitRate", + "BitDepth": "BitDepth", + "RefFrames": "RefFrames", + "IsDefault": "IsDefault", + "IsForced": "IsForced", + "Height": "Height", + "Width": "Width", + "AverageFrameRate": "AverageFrameRate", + "RealFrameRate": "RealFrameRate", + "Profile": "Profile", + "Type": "Type", + "AspectRatio": "AspectRatio", + "IsExternal": "IsExternal", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "SupportsExternalStream": "SupportsExternalStream", + "Protocol": "Protocol", + "PixelFormat": "PixelFormat", + "Level": "Level", + "IsAnamorphic": "IsAnamorphic" + }, + "Subtitles": { + "emby_id": "emby_id", + "MediaIndex": "MediaIndex", + "SubtitleIndex": "SubtitleIndex", + "StreamIndex": "StreamIndex", + "IsForced": "IsForced", + "IsInterlaced": "IsInterlaced", + "DisplayTitle": "DisplayTitle", + "SupportsExternalStream": "SupportsExternalStream", + "Language": "Language", + "DisplayLanguage": "DisplayLanguage", + "Codec": "Codec", + "CodecTimeBase": "CodecTimeBase", + "Protocol": "Protocol", + "Type": "Type", + "Path": "Path", + "TimeBase": "TimeBase", + "IsTextSubtitleStream": "IsTextSubtitleStream", + "IsDefault": "IsDefault", + "IsExternal": "IsExternal" + } + } def MapMissingData(self, item, mapping_name): mapping = self.objects[mapping_name] - for key, value in list(mapping.items()): + for key, _ in list(mapping.items()): if not key in item: - item[key] = "" + item[key] = None return item @@ -38,10 +518,6 @@ def map(self, item, mapping_name): "/": indicates where to go directly ''' self.mapped_item = {} - - if not mapping_name: - raise Exception("execute mapping() first") - mapping = self.objects[mapping_name] for key, value in list(mapping.items()): @@ -69,9 +545,9 @@ def map(self, item, mapping_name): if ':' in obj_param: result = [] - for d in self.__recursiveloop__(obj, obj_param): + for d in self.recursiveloop(obj, obj_param): - if obj_filters and self.__filters__(d, obj_filters): + if obj_filters and self.filters(d, obj_filters): result.append(d) elif not obj_filters: result.append(d) @@ -79,19 +555,19 @@ def map(self, item, mapping_name): obj = result obj_filters = {} elif '/' in obj_param: - obj = self.__recursive__(obj, obj_param) + obj = self.recursive(obj, obj_param) elif obj is item and obj is not None: obj = item.get(obj_param) if obj_filters and obj: - if not self.__filters__(obj, obj_filters): + if not self.filters(obj, obj_filters): obj = None if obj is None and len(params) != params.index(param): continue if obj_key: - obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key) + obj = [d[obj_key] for d in obj if d.get(obj_key)] if isinstance(obj, list) else obj.get(obj_key) self.mapped_item[key] = obj break @@ -103,28 +579,28 @@ def map(self, item, mapping_name): return self.mapped_item - def __recursiveloop__(self, obj, keys): + def recursiveloop(self, obj, keys): first, rest = keys.split(':', 1) - obj = self.__recursive__(obj, first) + obj = self.recursive(obj, first) if obj: if rest: for item in obj: - self.__recursiveloop__(item, rest) + self.recursiveloop(item, rest) else: for item in obj: yield item - def __recursive__(self, obj, keys): + def recursive(self, obj, keys): for string in keys.split('/'): if not obj: - return + return None obj = obj[int(string)] if string.isdigit() else obj.get(string) return obj - def __filters__(self, obj, filters): + def filters(self, obj, filters): result = False for key, value in iter(list(filters.items())): @@ -133,8 +609,7 @@ def __filters__(self, obj, filters): if value.startswith('!'): inverse = True value = value.split('!', 1)[1] - - if value.lower() == "null": + elif value.lower() == "null": value = None result = obj.get(key) != value if inverse else obj.get(key) == value diff --git a/core/queries_music.py b/core/queries_music.py index 2b2a3941b..c29c528a3 100644 --- a/core/queries_music.py +++ b/core/queries_music.py @@ -4,7 +4,7 @@ create_song = """ SELECT coalesce(max(idSong), 0) FROM song """ create_genre = """ SELECT coalesce(max(idGenre), 0) FROM genre """ get_artist = """ SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = ? """ -get_artist_obj = ["{ArtistId}", "{Name}", "{UniqueId}"] +get_artist_obj = ["{ArtistId}", "{Name}", "{UniqueId}", "{Disambiguation}"] get_artist_by_name = """ SELECT idArtist FROM artist WHERE strArtist = ? COLLATE NOCASE """ get_artist_by_id = """ SELECT * FROM artist WHERE idArtist = ? """ get_artist_by_id_obj = ["{ArtistId}"] @@ -13,19 +13,21 @@ get_song_by_id = """ SELECT * FROM song WHERE idSong = ? """ get_song_by_id_obj = ["{SongId}"] get_album = """ SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = ? """ -get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album"] -get_album_by_name = """ SELECT idAlbum, strArtistDisp FROM album WHERE strAlbum = ? """ +get_album_by_name = """ SELECT idAlbum FROM album WHERE strAlbum = ? AND strArtistDisp = ? """ get_album_artist = """ SELECT strArtistDisp FROM album WHERE idAlbum = ? """ get_album_artist_obj = ["{AlbumId}", "{strAlbumArtists}"] get_genre = """ SELECT idGenre FROM genre WHERE strGenre = ? COLLATE NOCASE """ get_total_episodes = """ SELECT totalCount FROM tvshowcounts WHERE idShow = ? """ get_artwork = """ SELECT url FROM art """ -add_artist = """ INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) VALUES (?, ?, ?) """ -add_album = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) VALUES (?, ?, ?, ?) """ -add_single = """ INSERT INTO album(strArtistDisp, iYear, strGenres, strImage, iUserrating, lastScraped, strReleaseType) VALUES (?, ?, ?, ?, ?, ?, ?) """ -add_single_obj = ["{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "single"] -add_single82 = """ INSERT INTO album(strArtistDisp, strReleaseDate, strGenres, strImage, iUserrating, lastScraped, bScrapedMBID, strReleaseType, dateAdded) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) """ -add_single_obj82 = ["{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "single", "{DateAdded}"] +add_artist = """ INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID, strDisambiguation) VALUES (?, ?, ?, ?) """ +add_single = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType, strArtistDisp, iYear, strGenres, strImage, iUserrating, lastScraped, strType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +get_single_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "single", "{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "{Type}"] +add_single82 = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType, strArtistDisp, strReleaseDate, strOrigReleaseDate, strGenres, strImage, iUserrating, lastScraped, dateAdded, strType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +get_single_obj82 = ["{AlbumId}", "{Title}", "{UniqueId}", "single", "{Artists}", "{Year}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "{DateAdded}", "{Type}"] +add_album = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType, strArtistDisp, iYear, strGenres, strImage, iUserrating, lastScraped, strType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "album", "{Artists}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "{Type}"] +add_album82 = """ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType, strArtistDisp, strReleaseDate, strOrigReleaseDate, strGenres, strImage, iUserrating, lastScraped, dateAdded, strType) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ +get_album_obj82 = ["{AlbumId}", "{Title}", "{UniqueId}", "album", "{Artists}", "{Year}", "{Year}", "{Genre}", "{Thumb}", "{Rating}", "{LastScraped}", "{DateAdded}", "{Type}"] add_song = """ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle, iTrack, iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, rating, comment, dateAdded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ add_song_obj = ["{SongId}", "{AlbumId}", "{PathId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}", "{Filename}", "{UniqueId}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}", "{DateAdded}"] add_song82 = """ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle, iTrack, iDuration, strReleaseDate, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, rating, comment, dateAdded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ diff --git a/core/queries_videos.py b/core/queries_videos.py index d1c439035..99f315145 100644 --- a/core/queries_videos.py +++ b/core/queries_videos.py @@ -98,6 +98,8 @@ add_art = """INSERT OR REPLACE INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?)""" add_movie = """INSERT OR REPLACE INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, userrating, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" add_movie_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{CriticRating}", "{Premiered}"] +add_movie_nouserrating = """INSERT OR REPLACE INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" +add_movie_nouserrating_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{Premiered}"] add_rating = """INSERT OR REPLACE INTO rating(rating_id, media_id, media_type, rating_type, rating, votes) VALUES (?, ?, ?, ?, ?, ?)""" add_rating_movie_obj = ["{RatingId}", "{MovieId}", "movie", "{RatingType}", "{Rating}", "{Votes}"] add_rating_tvshow_obj = ["{RatingId}", "{ShowId}", "tvshow", "default", "{Rating}", "{Votes}"] @@ -135,6 +137,8 @@ get_update_link = """SELECT * FROM {LinkType} WHERE actor_id = ? AND media_id = ? AND media_type = ? COLLATE NOCASE""" update_movie = """UPDATE movie SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?, c16 = ?, c18 = ?, c19 = ?, c21 = ?, userrating = ?, premiered = ? WHERE idMovie = ?""" update_movie_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{CriticRating}", "{Premiered}", "{MovieId}"] +update_movie_nouserrating = """UPDATE movie SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?, c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ? WHERE idMovie = ?""" +update_movie_nouserrating_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{OriginalTitle}", "{Studio}", "{Trailer}", "{Country}", "{Premiered}", "{MovieId}"] update_rating = """UPDATE rating SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ? WHERE rating_id = ?""" update_rating_movie_obj = ["{MovieId}", "movie", "{RatingType}", "{Rating}", "{Votes}", "{RatingId}"] update_rating_tvshow_obj = ["{ShowId}", "tvshow", "default", "{Rating}", "{Votes}", "{RatingId}"] diff --git a/core/tvshows.py b/core/tvshows.py index 6cd17e659..0f9be6684 100644 --- a/core/tvshows.py +++ b/core/tvshows.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -import logging import sqlite3 import ntpath -import emby.downloader import database.queries import database.emby_db -import helper.wrapper import helper.api +import helper.loghandler from . import obj_ops from . import common from . import queries_videos @@ -15,47 +13,28 @@ from . import kodi class TVShows(): - def __init__(self, server, embydb, videodb, direct_path, Utils, update_library=False): - self.LOG = logging.getLogger("EMBY.core.tvshows.TVShows") - self.Utils = Utils + def __init__(self, EmbyServer, embydb, videodb, update_library=False): + self.LOG = helper.loghandler.LOG('EMBY.core.tvshows.TVShows') self.update_library = update_library - self.server = server + self.EmbyServer = EmbyServer self.emby = embydb self.video = videodb - self.direct_path = direct_path self.emby_db = database.emby_db.EmbyDatabase(embydb.cursor) - self.objects = obj_ops.Objects(self.Utils) + self.objects = obj_ops.Objects() self.item_ids = [] - self.display_multiep = self.Utils.settings('displayMultiEpLabel.bool') - self.Downloader = emby.downloader.Downloader(self.Utils) - self.Common = common.Common(self.emby_db, self.objects, self.Utils, self.direct_path, self.server) - self.KodiDBIO = kodi.Kodi(videodb.cursor) + self.Common = common.Common(self.emby_db, self.objects, self.EmbyServer) + self.KodiDBIO = kodi.Kodi(videodb.cursor, self.EmbyServer.Utils) self.TVShowsDBIO = TVShowsDBIO(videodb.cursor) - self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.Utils) - - def __getitem__(self, key): - if key == 'Series': - return self.tvshow - elif key == 'Season': - return self.season - elif key == 'Episode': - return self.episode - - @helper.wrapper.stop - def tvshow(self, item, library=None, pooling=None, redirect=False): - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - If the show is empty, try to remove it. - Process seasons. - Apply series pooling. - ''' + self.ArtworkDBIO = artwork.Artwork(videodb.cursor, self.EmbyServer.Utils) + self.APIHelper = helper.api.API(self.EmbyServer.Utils) + + def tvshow(self, item, library, pooling=None, redirect=None): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Series') obj['Item'] = item obj['Library'] = library @@ -63,48 +42,48 @@ def tvshow(self, item, library=None, pooling=None, redirect=False): obj['LibraryName'] = library['Name'] update = True - if not self.Utils.settings('syncEmptyShows.bool') and not obj['RecursiveCount']: - self.LOG.info("Skipping empty show %s: %s", obj['Title'], obj['Id']) - TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).remove(obj['Id']) + if not obj['RecursiveCount']: + self.LOG.info("Skipping empty show %s: %s" % (obj['Title'], obj['Id'])) + TVShows(self.EmbyServer, self.emby, self.video, False).remove(obj['Id']) return False if pooling is None: StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] if str(StackedID) != obj['Id']: - return TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).tvshow(obj['Item'], library=obj['Library'], pooling=StackedID) + return TVShows(self.EmbyServer, self.emby, self.video, False).tvshow(obj['Item'], obj['Library'], StackedID, False) - try: + if e_item: obj['ShowId'] = e_item[0] obj['PathId'] = e_item[2] - except TypeError as error: + + if self.TVShowsDBIO.get(*self.EmbyServer.Utils.values(obj, queries_videos.get_tvshow_obj)) is None: + update = False + self.LOG.info("ShowId %s missing from kodi. repairing the entry." % obj['ShowId']) + else: update = False - self.LOG.debug("ShowId %s not found", obj['Id']) + self.LOG.debug("ShowId %s not found" % obj['Id']) obj['ShowId'] = self.TVShowsDBIO.create_entry() - else: - if self.TVShowsDBIO.get(*self.Utils.values(obj, queries_videos.get_tvshow_obj)) is None: - update = False - self.LOG.info("ShowId %s missing from kodi. repairing the entry.", obj['ShowId']) - obj['Path'] = API.get_file_path(obj['Item']['Path']) + obj['Path'] = self.APIHelper.get_file_path(obj['Item']['Path'], item) obj['Genres'] = obj['Genres'] or [] obj['People'] = obj['People'] or [] - obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) - obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] + obj['Mpaa'] = self.APIHelper.get_mpaa(obj['Mpaa'], item) + obj['Studios'] = [self.APIHelper.validate_studio(studio) for studio in (obj['Studios'] or [])] obj['Genre'] = " / ".join(obj['Genres']) - obj['People'] = API.get_people_artwork(obj['People']) - obj['Plot'] = API.get_overview(obj['Plot']) + obj['People'] = self.APIHelper.get_people_artwork(obj['People']) + obj['Plot'] = self.APIHelper.get_overview(obj['Plot'], item) obj['Studio'] = " / ".join(obj['Studios']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) if obj['Status'] != 'Ended': obj['Status'] = None if not self.get_path_filename(obj): - return False + return "Invalid Filepath" if obj['Premiere']: - obj['Premiere'] = str(self.Utils.convert_to_local(obj['Premiere'])).split('.')[0].replace('T', " ") + obj['Premiere'] = str(self.EmbyServer.Utils.convert_to_local(obj['Premiere'])).split('.')[0].replace('T', " ") tags = [] tags.extend(obj['TagItems'] or obj['Tags'] or []) @@ -122,21 +101,21 @@ def tvshow(self, item, library=None, pooling=None, redirect=False): if pooling: obj['SeriesId'] = pooling - self.LOG.info("POOL %s [%s/%s]", obj['Title'], obj['Id'], obj['SeriesId']) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_pool_obj)) + self.LOG.info("POOL %s [%s/%s]" % (obj['Title'], obj['Id'], obj['SeriesId'])) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_pool_obj)) return True - self.TVShowsDBIO.link(*self.Utils.values(obj, queries_videos.update_tvshow_link_obj)) - self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_tvshow_obj)) - self.KodiDBIO.add_tags(*self.Utils.values(obj, queries_videos.add_tags_tvshow_obj)) - self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_tvshow_obj)) - self.KodiDBIO.add_genres(*self.Utils.values(obj, queries_videos.add_genres_tvshow_obj)) - self.KodiDBIO.add_studios(*self.Utils.values(obj, queries_videos.add_studios_tvshow_obj)) + self.TVShowsDBIO.link(*self.EmbyServer.Utils.values(obj, queries_videos.update_tvshow_link_obj)) + self.KodiDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_videos.update_path_tvshow_obj)) + self.KodiDBIO.add_tags(*self.EmbyServer.Utils.values(obj, queries_videos.add_tags_tvshow_obj)) + self.KodiDBIO.add_people(*self.EmbyServer.Utils.values(obj, queries_videos.add_people_tvshow_obj)) + self.KodiDBIO.add_genres(*self.EmbyServer.Utils.values(obj, queries_videos.add_genres_tvshow_obj)) + self.KodiDBIO.add_studios(*self.EmbyServer.Utils.values(obj, queries_videos.add_studios_tvshow_obj)) self.ArtworkDBIO.add(obj['Artwork'], obj['ShowId'], "tvshow") self.item_ids.append(obj['Id']) if "StackTimes" in obj: - self.KodiDBIO.add_stacktimes(*self.Utils.values(obj, queries_videos.add_stacktimes_obj)) + self.KodiDBIO.add_stacktimes(*self.EmbyServer.Utils.values(obj, queries_videos.add_stacktimes_obj)) if redirect: self.LOG.info("tvshow added as a redirect") @@ -145,9 +124,9 @@ def tvshow(self, item, library=None, pooling=None, redirect=False): season_episodes = {} try: - all_seasons = self.server['api'].get_seasons(obj['Id'])['Items'] + all_seasons = self.EmbyServer.API.get_seasons(obj['Id'])['Items'] except Exception as error: - self.LOG.error("Unable to pull seasons for %s", obj['Title']) + self.LOG.error("Unable to pull seasons for %s" % obj['Title']) self.LOG.error(error) return True @@ -159,24 +138,27 @@ def tvshow(self, item, library=None, pooling=None, redirect=False): self.emby_db.get_item_by_id(season['Id'])[0] self.item_ids.append(season['Id']) except TypeError: - self.season(season, obj['ShowId'], obj['LibraryId']) + self.season(season, library, obj['ShowId']) - season_id = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_special_obj)) + season_id = self.TVShowsDBIO.get_season(*self.EmbyServer.Utils.values(obj, queries_videos.get_season_special_obj)) self.ArtworkDBIO.add(obj['Artwork'], season_id, "season") for season in season_episodes: - for episodes in self.Downloader.get_episode_by_season(season_episodes[season], season): + for episodes in self.EmbyServer.API.get_episode_by_season(season_episodes[season], season): for episode in episodes['Items']: - self.episode(episode) + Ret = self.episode(episode, library) + + if Ret == "Invalid Filepath": + return Ret return not update #Add object to kodi def tvshow_add(self, obj): obj['RatingId'] = self.KodiDBIO.create_entry_rating() - self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_tvshow_obj)) + self.KodiDBIO.add_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.add_rating_tvshow_obj)) obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() - self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -184,22 +166,22 @@ def tvshow_add(self, obj): if provider != 'tvdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) - self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) obj['TopPathId'] = self.KodiDBIO.add_path(obj['TopLevel']) - self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_toptvshow_obj)) - obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.get_path_obj)) - self.TVShowsDBIO.add(*self.Utils.values(obj, queries_videos.add_tvshow_obj)) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_tvshow_obj)) - self.LOG.info("ADD tvshow [%s/%s/%s] %s: %s", obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Id'], obj['Title']) + self.KodiDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_videos.update_path_toptvshow_obj)) + obj['PathId'] = self.KodiDBIO.add_path(*self.EmbyServer.Utils.values(obj, queries_videos.get_path_obj)) + self.TVShowsDBIO.add(*self.EmbyServer.Utils.values(obj, queries_videos.add_tvshow_obj)) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_tvshow_obj)) + self.LOG.info("ADD tvshow [%s/%s/%s] %s: %s" % (obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Id'], obj['Title'])) #Update object to kodi def tvshow_update(self, obj): - obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_tvshow_obj)) - self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_tvshow_obj)) - self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_tvshow_obj)) + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.EmbyServer.Utils.values(obj, queries_videos.get_rating_tvshow_obj)) + self.KodiDBIO.update_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.update_rating_tvshow_obj)) + self.KodiDBIO.remove_unique_ids(*self.EmbyServer.Utils.values(obj, queries_videos.delete_unique_ids_tvshow_obj)) obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() - self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_tvshow_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -207,11 +189,11 @@ def tvshow_update(self, obj): if provider != 'tvdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) - self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_tvshow_obj)) - self.TVShowsDBIO.update(*self.Utils.values(obj, queries_videos.update_tvshow_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("UPDATE tvshow [%s/%s] %s: %s", obj['PathId'], obj['ShowId'], obj['Id'], obj['Title']) + self.TVShowsDBIO.update(*self.EmbyServer.Utils.values(obj, queries_videos.update_tvshow_obj)) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("UPDATE tvshow [%s/%s] %s: %s" % (obj['PathId'], obj['ShowId'], obj['Id'], obj['Title'])) #Get the path and build it into protocol://path def get_path_filename(self, obj): @@ -219,7 +201,7 @@ def get_path_filename(self, obj): self.LOG.info("Path is missing") return False - if self.direct_path: + if self.EmbyServer.Utils.direct_path: if '\\' in obj['Path']: obj['Path'] = "%s\\" % obj['Path'] obj['TopLevel'] = "%s\\" % ntpath.dirname(ntpath.dirname(obj['Path'])) @@ -227,57 +209,57 @@ def get_path_filename(self, obj): obj['Path'] = "%s/" % obj['Path'] obj['TopLevel'] = "%s/" % ntpath.dirname(ntpath.dirname(obj['Path'])) - obj['Path'] = self.Utils.StringDecode(obj['Path']) - obj['TopLevel'] = self.Utils.StringDecode(obj['TopLevel']) + obj['Path'] = self.EmbyServer.Utils.StringDecode(obj['Path']) + obj['TopLevel'] = self.EmbyServer.Utils.StringDecode(obj['TopLevel']) - if not self.Utils.validate(obj['Path']): - raise Exception("Failed to validate path. User stopped.") + if not self.EmbyServer.Utils.validate(obj['Path']): + return False else: obj['TopLevel'] = "http://127.0.0.1:57578/tvshows/" obj['Path'] = "%s%s/" % (obj['TopLevel'], obj['Id']) return True - @helper.wrapper.stop - def season(self, item, show_id=None, library_id=None): - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - If the show is empty, try to remove it. - ''' - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) + #If item does not exist, entry will be added. + #If item exists, entry will be updated. + #If the show is empty, try to remove it. + def season(self, item, library, show_id=None): + e_item = self.emby_db.get_item_by_id(item['Id']) + library = self.Common.library_check(e_item, item, library) + + if not library: + return False + obj = self.objects.map(item, 'Season') - obj['LibraryId'] = library_id + obj['LibraryId'] = library['Id'] obj['ShowId'] = show_id if obj['ShowId'] is None: if not self.get_show_id(obj): return False - obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_obj)) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.EmbyServer.Utils.values(obj, queries_videos.get_season_obj)) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) if obj['Location'] != 'Virtual': - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_season_obj)) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_season_obj)) self.item_ids.append(obj['Id']) self.ArtworkDBIO.add(obj['Artwork'], obj['SeasonId'], "season") - self.LOG.info("UPDATE season [%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['Title'] or obj['Index'], obj['Id']) + self.LOG.info("UPDATE season [%s/%s] %s: %s" % (obj['ShowId'], obj['SeasonId'], obj['Title'] or obj['Index'], obj['Id'])) return True - @helper.wrapper.stop - def episode(self, item, library=None): - ''' If item does not exist, entry will be added. - If item exists, entry will be updated. - Create additional entry for widgets. - This is only required for plugin/episode. - ''' + #If item does not exist, entry will be added. + #If item exists, entry will be updated. + #Create additional entry for widgets. + #This is only required for plugin/episode. + def episode(self, item, library): e_item = self.emby_db.get_item_by_id(item['Id']) library = self.Common.library_check(e_item, item, library) if not library: return False - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'Episode') obj['Item'] = item obj['Library'] = library @@ -286,31 +268,31 @@ def episode(self, item, library=None): update = True if obj['Location'] == 'Virtual': - self.LOG.info("Skipping virtual episode %s: %s", obj['Title'], obj['Id']) + self.LOG.info("Skipping virtual episode %s: %s" % (obj['Title'], obj['Id'])) return False if obj['SeriesId'] is None: - self.LOG.info("Skipping episode %s with missing SeriesId", obj['Id']) + self.LOG.info("Skipping episode %s with missing SeriesId" % obj['Id']) return False StackedID = self.emby_db.get_stack(obj['PresentationKey']) or obj['Id'] if str(StackedID) != obj['Id']: - self.LOG.info("Skipping stacked episode %s [%s]", obj['Title'], obj['Id']) - TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).remove(StackedID) + self.LOG.info("Skipping stacked episode %s [%s]" % (obj['Title'], obj['Id'])) + TVShows(self.EmbyServer, self.emby, self.video, False).remove(StackedID) - try: + if e_item: obj['EpisodeId'] = e_item[0] obj['FileId'] = e_item[1] obj['PathId'] = e_item[2] - except TypeError: + + if self.TVShowsDBIO.get_episode(*self.EmbyServer.Utils.values(obj, queries_videos.get_episode_obj)) is None: + update = False + self.LOG.info("EpisodeId %s missing from kodi. repairing the entry." % obj['EpisodeId']) + else: update = False - self.LOG.debug("EpisodeId %s not found", obj['Id']) + self.LOG.debug("EpisodeId %s not found" % obj['Id']) obj['EpisodeId'] = self.TVShowsDBIO.create_entry_episode() - else: - if self.TVShowsDBIO.get_episode(*self.Utils.values(obj, queries_videos.get_episode_obj)) is None: - update = False - self.LOG.info("EpisodeId %s missing from kodi. repairing the entry.", obj['EpisodeId']) obj['Item']['MediaSources'][0] = self.objects.MapMissingData(obj['Item']['MediaSources'][0], 'MediaSources') obj['MediaSourceID'] = obj['Item']['MediaSources'][0]['Id'] @@ -320,34 +302,37 @@ def episode(self, item, library=None): obj['Path'] = obj['Item']['MediaSources'][0]['Path'] #don't use 3d movies as default - if "3d" in self.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): + if "3d" in self.EmbyServer.Utils.StringMod(obj['Item']['MediaSources'][0]['Path']): for DataSource in obj['Item']['MediaSources']: - if not "3d" in self.Utils.StringMod(DataSource['Path']): + if not "3d" in self.EmbyServer.Utils.StringMod(DataSource['Path']): DataSource = self.objects.MapMissingData(DataSource, 'MediaSources') obj['Path'] = DataSource['Path'] obj['MediaSourceID'] = DataSource['Id'] obj['Runtime'] = DataSource['RunTimeTicks'] break - obj['Path'] = API.get_file_path(obj['Path']) + obj['Path'] = self.APIHelper.get_file_path(obj['Path'], item) obj['Index'] = obj['Index'] or -1 obj['Writers'] = " / ".join(obj['Writers'] or []) obj['Directors'] = " / ".join(obj['Directors'] or []) - obj['Plot'] = API.get_overview(obj['Plot']) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Plot'] = self.APIHelper.get_overview(obj['Plot'], item) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['People'] = API.get_people_artwork(obj['People'] or []) - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") - obj['DatePlayed'] = None if not obj['DatePlayed'] else self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) - obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) - obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) - obj['Audio'] = API.audio_streams(obj['Audio'] or []) - obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) - obj = self.Common.get_path_filename(obj, "tvshows") + obj['People'] = self.APIHelper.get_people_artwork(obj['People'] or []) + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = None if not obj['DatePlayed'] else self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) + obj['Artwork'] = self.APIHelper.get_all_artwork(self.objects.map(item, 'Artwork')) + obj['Video'] = self.APIHelper.video_streams(obj['Video'] or [], obj['Container'], item) + obj['Audio'] = self.APIHelper.audio_streams(obj['Audio'] or []) + obj['Streams'] = self.APIHelper.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) + PathValid, obj = self.Common.get_path_filename(obj, "tvshows") + + if not PathValid: + return "Invalid Filepath" if obj['Premiere']: - obj['Premiere'] = self.Utils.convert_to_local(obj['Premiere']).split('.')[0].replace('T', " ") + obj['Premiere'] = self.EmbyServer.Utils.convert_to_local(obj['Premiere']).split('.')[0].replace('T', " ") if obj['Season'] is None: if obj['AbsoluteNumber']: @@ -360,25 +345,22 @@ def episode(self, item, library=None): obj['AirsBeforeSeason'] = obj['AirsAfterSeason'] obj['AirsBeforeEpisode'] = 4096 # Kodi default number for afterseason ordering - if obj['MultiEpisode'] and self.display_multiep: - obj['Title'] = "| %02d | %s" % (obj['MultiEpisode'], obj['Title']) - if not self.get_show_id(obj): self.LOG.info("No series id associated") return False - obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.Utils.values(obj, queries_videos.get_season_episode_obj)) + obj['SeasonId'] = self.TVShowsDBIO.get_season(*self.EmbyServer.Utils.values(obj, queries_videos.get_season_episode_obj)) if update: self.episode_update(obj) else: self.episode_add(obj) - self.KodiDBIO.update_path(*self.Utils.values(obj, queries_videos.update_path_episode_obj)) - self.KodiDBIO.update_file(*self.Utils.values(obj, queries_videos.update_file_obj)) - self.KodiDBIO.add_people(*self.Utils.values(obj, queries_videos.add_people_episode_obj)) - self.KodiDBIO.add_streams(*self.Utils.values(obj, queries_videos.add_streams_obj)) - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.update_path(*self.EmbyServer.Utils.values(obj, queries_videos.update_path_episode_obj)) + self.KodiDBIO.update_file(*self.EmbyServer.Utils.values(obj, queries_videos.update_file_obj)) + self.KodiDBIO.add_people(*self.EmbyServer.Utils.values(obj, queries_videos.add_people_episode_obj)) + self.KodiDBIO.add_streams(*self.EmbyServer.Utils.values(obj, queries_videos.add_streams_obj)) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) self.ArtworkDBIO.update(obj['Artwork']['Primary'], obj['EpisodeId'], "episode", "thumb") self.item_ids.append(obj['Id']) return not update @@ -387,9 +369,9 @@ def episode(self, item, library=None): def episode_add(self, obj): obj = self.Common.Streamdata_add(obj, False) obj['RatingId'] = self.KodiDBIO.create_entry_rating() - self.KodiDBIO.add_ratings(*self.Utils.values(obj, queries_videos.add_rating_episode_obj)) + self.KodiDBIO.add_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.add_rating_episode_obj)) obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() - self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -397,29 +379,29 @@ def episode_add(self, obj): if provider != 'tvdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) - self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) - obj['PathId'] = self.KodiDBIO.add_path(*self.Utils.values(obj, queries_videos.add_path_obj)) - obj['FileId'] = self.KodiDBIO.add_file(*self.Utils.values(obj, queries_videos.add_file_obj)) + obj['PathId'] = self.KodiDBIO.add_path(*self.EmbyServer.Utils.values(obj, queries_videos.add_path_obj)) + obj['FileId'] = self.KodiDBIO.add_file(*self.EmbyServer.Utils.values(obj, queries_videos.add_file_obj)) try: - self.TVShowsDBIO.add_episode(*self.Utils.values(obj, queries_videos.add_episode_obj)) + self.TVShowsDBIO.add_episode(*self.EmbyServer.Utils.values(obj, queries_videos.add_episode_obj)) except sqlite3.IntegrityError: - self.LOG.error("IntegrityError for %s", obj) + self.LOG.error("IntegrityError for %s" % obj) obj['EpisodeId'] = self.TVShowsDBIO.create_entry_episode() return self.episode_add(obj) - self.emby_db.add_reference(*self.Utils.values(obj, database.queries.add_reference_episode_obj)) - self.LOG.info("ADD episode [%s/%s/%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title']) + self.emby_db.add_reference(*self.EmbyServer.Utils.values(obj, database.queries.add_reference_episode_obj)) + self.LOG.info("ADD episode [%s/%s/%s/%s] %s: %s" % (obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title'])) #Update object to kodi def episode_update(self, obj): obj = self.Common.Streamdata_add(obj, True) - obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.Utils.values(obj, queries_videos.get_rating_episode_obj)) - self.KodiDBIO.update_ratings(*self.Utils.values(obj, queries_videos.update_rating_episode_obj)) - self.KodiDBIO.remove_unique_ids(*self.Utils.values(obj, queries_videos.delete_unique_ids_episode_obj)) + obj['RatingId'] = self.KodiDBIO.get_rating_id(*self.EmbyServer.Utils.values(obj, queries_videos.get_rating_episode_obj)) + self.KodiDBIO.update_ratings(*self.EmbyServer.Utils.values(obj, queries_videos.update_rating_episode_obj)) + self.KodiDBIO.remove_unique_ids(*self.EmbyServer.Utils.values(obj, queries_videos.delete_unique_ids_episode_obj)) obj['Unique'] = self.TVShowsDBIO.create_entry_unique_id() - self.TVShowsDBIO.add_unique_id(*self.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(obj, queries_videos.add_unique_id_episode_obj)) for provider in obj['UniqueIds'] or {}: unique_id = obj['UniqueIds'][provider] @@ -427,25 +409,26 @@ def episode_update(self, obj): if provider != 'tvdb': temp_obj = dict(obj, ProviderName=provider, UniqueId=unique_id, Unique=self.TVShowsDBIO.create_entry_unique_id()) - self.TVShowsDBIO.add_unique_id(*self.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) + self.TVShowsDBIO.add_unique_id(*self.EmbyServer.Utils.values(temp_obj, queries_videos.add_unique_id_episode_obj)) - self.TVShowsDBIO.update_episode(*self.Utils.values(obj, queries_videos.update_episode_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.emby_db.update_parent_id(*self.Utils.values(obj, database.queries.update_parent_episode_obj)) - self.LOG.info("UPDATE episode [%s/%s/%s/%s] %s: %s", obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title']) + self.TVShowsDBIO.update_episode(*self.EmbyServer.Utils.values(obj, queries_videos.update_episode_obj)) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.emby_db.update_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.update_parent_episode_obj)) + self.LOG.info("UPDATE episode [%s/%s/%s/%s] %s: %s" % (obj['ShowId'], obj['SeasonId'], obj['EpisodeId'], obj['FileId'], obj['Id'], obj['Title'])) def get_show_id(self, obj): if obj.get('ShowId'): return True - obj['ShowId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_series_obj)) + obj['ShowId'] = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_series_obj)) if obj['ShowId'] is None: + TVShows(self.EmbyServer, self.emby, self.video, False).tvshow(self.EmbyServer.API.get_item(obj['SeriesId']), None, None, True) + Data = self.emby_db.get_item_by_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_series_obj)) - try: - TVShows(self.server, self.emby, self.video, self.direct_path, self.Utils, False).tvshow(self.server['api'].get_item(obj['SeriesId']), library=None, redirect=True) - obj['ShowId'] = self.emby_db.get_item_by_id(*self.Utils.values(obj, database.queries.get_item_series_obj))[0] - except (TypeError, KeyError): - self.LOG.error("Unable to add series %s", obj['SeriesId']) + if Data: + obj['ShowId'] = Data[0] + else: + self.LOG.error("Unable to add series %s" % obj['SeriesId']) return False else: obj['ShowId'] = obj['ShowId'][0] @@ -454,10 +437,8 @@ def get_show_id(self, obj): return True #This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - @helper.wrapper.stop def userdata(self, item): e_item = self.emby_db.get_item_by_id(item['Id']) - API = helper.api.API(item, self.Utils, self.server['auth/server-address']) obj = self.objects.map(item, 'EpisodeUserData') obj['Item'] = item @@ -470,118 +451,117 @@ def userdata(self, item): if obj['Media'] == "tvshow": if obj['Favorite']: - self.KodiDBIO.get_tag(*self.Utils.values(obj, queries_videos.get_tag_episode_obj)) + self.KodiDBIO.get_tag(*self.EmbyServer.Utils.values(obj, queries_videos.get_tag_episode_obj)) else: - self.KodiDBIO.remove_tag(*self.Utils.values(obj, queries_videos.delete_tag_episode_obj)) + self.KodiDBIO.remove_tag(*self.EmbyServer.Utils.values(obj, queries_videos.delete_tag_episode_obj)) elif obj['Media'] == "episode": obj = self.Common.Streamdata_add(obj, True) - obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0, self.Utils) + obj['Resume'] = self.APIHelper.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) - obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) + obj['PlayCount'] = self.APIHelper.get_playcount(obj['Played'], obj['PlayCount']) if obj['DatePlayed']: - obj['DatePlayed'] = self.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") + obj['DatePlayed'] = self.EmbyServer.Utils.convert_to_local(obj['DatePlayed']).split('.')[0].replace('T', " ") if obj['DateAdded']: - obj['DateAdded'] = self.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") + obj['DateAdded'] = self.EmbyServer.Utils.convert_to_local(obj['DateAdded']).split('.')[0].replace('T', " ") - self.KodiDBIO.add_playstate(*self.Utils.values(obj, queries_videos.add_bookmark_obj)) + self.KodiDBIO.add_playstate(*self.EmbyServer.Utils.values(obj, queries_videos.add_bookmark_obj)) - self.emby_db.update_reference(*self.Utils.values(obj, database.queries.update_reference_obj)) - self.LOG.info("USERDATA %s [%s/%s] %s: %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'], obj['Title']) + self.emby_db.update_reference(*self.EmbyServer.Utils.values(obj, database.queries.update_reference_obj)) + self.LOG.info("USERDATA %s [%s/%s] %s: %s" % (obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'], obj['Title'])) return - @helper.wrapper.stop + #Remove showid, fileid, pathid, emby reference. + #There's no episodes left, delete show and any possible remaining seasons def remove(self, item_id): - ''' Remove showid, fileid, pathid, emby reference. - There's no episodes left, delete show and any possible remaining seasons - ''' e_item = self.emby_db.get_item_by_id(item_id) obj = {'Id': item_id} - try: + if e_item: obj['KodiId'] = e_item[0] obj['FileId'] = e_item[1] obj['ParentId'] = e_item[3] obj['Media'] = e_item[4] - except TypeError: + else: return if obj['Media'] == 'episode': temp_obj = dict(obj) self.remove_episode(obj['KodiId'], obj['FileId'], obj['Id']) - season = self.emby_db.get_full_item_by_kodi_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) + season = self.emby_db.get_full_item_by_kodi_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) - try: + if season: temp_obj['Id'] = season[0] temp_obj['ParentId'] = season[1] - except TypeError: + else: return - if not self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): + if not self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): self.remove_season(obj['ParentId'], obj['Id']) - self.emby_db.remove_item(*self.Utils.values(temp_obj, database.queries.delete_item_obj)) + self.emby_db.remove_item(temp_obj['Id']) - temp_obj['Id'] = self.emby_db.get_item_by_kodi_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_tvshow_obj)) + temp_obj['Id'] = self.emby_db.get_item_by_kodi_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_tvshow_obj)) - if not self.TVShowsDBIO.get_total_episodes(*self.Utils.values(temp_obj, queries_videos.get_total_episodes_obj)): - for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_season_obj)): + if not self.TVShowsDBIO.get_total_episodes(*self.EmbyServer.Utils.values(temp_obj, queries_videos.get_total_episodes_obj)): + for season in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_season_obj)): self.remove_season(season[1], obj['Id']) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_season_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_item_by_parent_season_obj)) self.remove_tvshow(temp_obj['ParentId'], obj['Id']) - self.emby_db.remove_item(*self.Utils.values(temp_obj, database.queries.delete_item_obj)) + self.emby_db.remove_item(temp_obj['Id']) + elif obj['Media'] == 'tvshow': obj['ParentId'] = obj['KodiId'] - for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): + for season in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): temp_obj = dict(obj) temp_obj['ParentId'] = season[1] - for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): - self.remove_episode(episode[1], episode[2], obj['Id']) + for episode in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], episode[0]) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(temp_obj, database.queries.delete_item_by_parent_episode_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.delete_item_by_parent_episode_obj)) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)) self.remove_tvshow(obj['KodiId'], obj['Id']) elif obj['Media'] == 'season': obj['ParentId'] = obj['KodiId'] - for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): - self.remove_episode(episode[1], episode[2], obj['Id']) + for episode in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_episode_obj)): + self.remove_episode(episode[1], episode[2], episode[0]) - self.emby_db.remove_items_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_episode_obj)) + self.emby_db.remove_items_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_episode_obj)) self.remove_season(obj['KodiId'], obj['Id']) - if not self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)): + if not self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_season_obj)): self.remove_tvshow(obj['ParentId'], obj['Id']) - self.emby_db.remove_item_by_kodi_id(*self.Utils.values(obj, database.queries.delete_item_by_parent_tvshow_obj)) + self.emby_db.remove_item_by_kodi_id(*self.EmbyServer.Utils.values(obj, database.queries.delete_item_by_parent_tvshow_obj)) # Remove any series pooling episodes for episode in self.emby_db.get_media_by_parent_id(obj['Id']): - self.remove_episode(episode[2], episode[3], obj['Id']) + self.remove_episode(episode[2], episode[3], episode[0]) self.emby_db.remove_media_by_parent_id(obj['Id']) - self.emby_db.remove_item(*self.Utils.values(obj, database.queries.delete_item_obj)) + self.emby_db.remove_item(obj['Id']) def remove_tvshow(self, kodi_id, item_id): self.ArtworkDBIO.delete(kodi_id, "tvshow") self.TVShowsDBIO.delete_tvshow(kodi_id) self.emby_db.remove_item_by_kodi_id(kodi_id, "tvshow") - self.LOG.info("DELETE tvshow [%s] %s", kodi_id, item_id) + self.LOG.info("DELETE tvshow [%s] %s" % (kodi_id, item_id)) def remove_season(self, kodi_id, item_id): self.ArtworkDBIO.delete(kodi_id, "season") self.TVShowsDBIO.delete_season(kodi_id) self.emby_db.remove_item_by_kodi_id(kodi_id, "season") - self.LOG.info("DELETE season [%s] %s", kodi_id, item_id) + self.LOG.info("DELETE season [%s] %s" % (kodi_id, item_id)) def remove_episode(self, kodi_id, file_id, item_id): self.ArtworkDBIO.delete(kodi_id, "episode") self.TVShowsDBIO.delete_episode(kodi_id, file_id) self.emby_db.remove_item(item_id) - self.LOG.info("DELETE episode [%s/%s] %s", file_id, kodi_id, item_id) + self.LOG.info("DELETE episode [%s/%s] %s" % (file_id, kodi_id, item_id)) #Get all child elements from tv show emby id def get_child(self, item_id): @@ -589,22 +569,22 @@ def get_child(self, item_id): obj = {'Id': item_id} child = [] - try: + if e_item: obj['KodiId'] = e_item[0] obj['FileId'] = e_item[1] obj['ParentId'] = e_item[3] obj['Media'] = e_item[4] - except TypeError: + else: return child obj['ParentId'] = obj['KodiId'] - for season in self.emby_db.get_item_by_parent_id(*self.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): + for season in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(obj, database.queries.get_item_by_parent_season_obj)): temp_obj = dict(obj) temp_obj['ParentId'] = season[1] child.append(season[0]) - for episode in self.emby_db.get_item_by_parent_id(*self.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): + for episode in self.emby_db.get_item_by_parent_id(*self.EmbyServer.Utils.values(temp_obj, database.queries.get_item_by_parent_episode_obj)): child.append(episode[0]) for episode in self.emby_db.get_media_by_parent_id(obj['Id']): @@ -633,25 +613,31 @@ def create_entry_episode(self): return self.cursor.fetchone()[0] + 1 def get(self, *args): - try: - self.cursor.execute(queries_videos.get_tvshow, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_tvshow, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def get_episode(self, *args): - try: - self.cursor.execute(queries_videos.get_episode, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_episode, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def get_total_episodes(self, *args): - try: - self.cursor.execute(queries_videos.get_total_episodes, args) - return self.cursor.fetchone()[0] - except TypeError: - return + self.cursor.execute(queries_videos.get_total_episodes, args) + Data = self.cursor.fetchone() + + if Data: + return Data[0] + + return None def add_unique_id(self, *args): self.cursor.execute(queries_videos.add_unique_id, args) @@ -667,10 +653,11 @@ def link(self, *args): def get_season(self, name, *args): self.cursor.execute(queries_videos.get_season, args) + Data = self.cursor.fetchone() - try: - season_id = self.cursor.fetchone()[0] - except TypeError: + if Data: + season_id = Data[0] + else: season_id = self.add_season(*args) if name: diff --git a/database/database.py b/database/database.py index 8bf689c79..b6ba3db57 100644 --- a/database/database.py +++ b/database/database.py @@ -1,205 +1,121 @@ # -*- coding: utf-8 -*- -import datetime -import logging -import json import os import sqlite3 + import xbmc import xbmcvfs import xbmcgui -import helper.translate -import helper.utils -import core.obj_ops -#import emby.views +import helper.loghandler from . import emby_db -Utils = helper.utils.Utils() -LOG = logging.getLogger("EMBY.database.database") +LOG = helper.loghandler.LOG('EMBY.database.database') class Database(): - ''' This should be called like a context. - i.e. with Database('emby') as db: - db.cursor - db.conn.commit() - ''' - timeout = 120 - discovered = False - discovered_file = None - - #file: emby, texture, music, video, :memory: or path to file - def __init__(self, fileID=None, commit_close=True): - self.db_file = fileID or "video" + def __init__(self, Utils, fileID, commit_close): + self.Utils = Utils + self.db_file = fileID self.commit_close = commit_close - self.objects = core.obj_ops.Objects(Utils) - self.path = None self.conn = None self.cursor = None #Open the connection and return the Database class. #This is to allow for the cursor, conn and others to be accessible. def __enter__(self): - self.path = self._sql(self.db_file) - self.conn = sqlite3.connect(self.path, timeout=self.timeout) + self.conn = sqlite3.connect(self.Utils.DatabaseFiles[self.db_file], timeout=120) self.cursor = self.conn.cursor() - - if self.db_file in ('video', 'music', 'texture', 'emby'): - self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi - - LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn)) + self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi + LOG.debug("--->[ database: %s ] %s" % (self.db_file, id(self.conn))) return self - def _get_database(self, path, silent=False): - path = Utils.translatePath(path) - - if not silent: - if not xbmcvfs.exists(path): - raise Exception("Database: %s missing" % path) - - conn = sqlite3.connect(path) - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") - tables = cursor.fetchall() - conn.close() - - if not len(tables): - raise Exception("Database: %s malformed?" % path) - - return path - - def _sql(self, DBFile): - databases = self.objects.objects - - if DBFile not in ('video', 'music', 'texture') or databases.get('database_set%s' % DBFile): - return self._get_database(databases[DBFile], True) - - #Read Database from filesystem - folder = Utils.translatePath("special://database/") - dirs, files = xbmcvfs.listdir(folder) - DBIDs = { - 'Textures': "texture", - 'MyMusic': "music", - 'MyVideos': "video" - } - - Version = 0 - - for DBID in DBIDs: - key = DBIDs[DBID] - - if key == DBFile: - for Filename in files: - if (Filename.startswith(DBID) and not Filename.endswith('-wal') and not Filename.endswith('-shm') and not Filename.endswith('db-journal')): - Temp = int(''.join(i for i in Filename if i.isdigit())) - - if Temp > Version: - databases[key] = os.path.join(folder, Filename) - databases['database_set%s' % key] = True - Version = Temp - - Utils.window('kodidbverion.' + DBFile, str(Version)) - return databases[DBFile] - #Close the connection and cursor def __exit__(self, exc_type, exc_val, exc_tb): - changes = self.conn.total_changes - if exc_type is not None: # errors raised - LOG.error("type: %s value: %s", exc_type, exc_val) + LOG.error("type: %s value: %s" % (exc_type, exc_val)) + + if self.commit_close: + changes = self.conn.total_changes + LOG.info("[%s] %s rows updated." % (self.db_file, changes)) - if self.commit_close and changes: - LOG.info("[%s] %s rows updated.", self.db_file, changes) - self.conn.commit() + if changes: + self.conn.commit() - LOG.debug("---<[ database: %s ] %s", self.db_file, id(self.conn)) + LOG.debug("---<[ database: %s ] %s" % (self.db_file, id(self.conn))) self.cursor.close() self.conn.close() - #Open the databases to test if the file exists -def test_databases(): - with Database('video'): - with Database('music'): - pass - - with Database('emby') as embydb: - emby_tables(embydb.cursor) - +def EmbyDatabaseBuild(Utils): + with Database(Utils, 'emby', True) as embydb: + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS emby(emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER, emby_parent_id TEXT, presentation_key TEXT)") + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS view(view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, server_id TEXT)") + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS MediaSources(emby_id TEXT, MediaIndex INTEGER, Protocol TEXT, MediaSourceId TEXT, Path TEXT, Type TEXT, Container TEXT, Size INTEGER, Name TEXT, IsRemote TEXT, RunTimeTicks INTEGER, SupportsTranscoding TEXT, SupportsDirectStream TEXT, SupportsDirectPlay TEXT, IsInfiniteStream TEXT, RequiresOpening TEXT, RequiresClosing TEXT, RequiresLooping TEXT, SupportsProbing TEXT, Formats TEXT, Bitrate INTEGER, RequiredHttpHeaders TEXT, ReadAtNativeFramerate TEXT, DefaultAudioStreamIndex INTEGER)") + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS VideoStreams(emby_id TEXT, MediaIndex INTEGER, VideoIndex INTEGER, Codec TEXT, TimeBase TEXT, CodecTimeBase TEXT, VideoRange TEXT, DisplayTitle TEXT, IsInterlaced TEXT, BitRate INTEGER, BitDepth INTEGER, RefFrames INTEGER, IsDefault TEXT, IsForced TEXT, Height INTEGER, Width INTEGER, AverageFrameRate INTEGER, RealFrameRate INTEGER, Profile TEXT, Type TEXT, AspectRatio TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, PixelFormat TEXT, Level INTEGER, IsAnamorphic TEXT, StreamIndex INTEGER)") + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS AudioStreams(emby_id TEXT, MediaIndex INTEGER, AudioIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, ChannelLayout TEXT, BitRate INTEGER, Channels INTEGER, SampleRate INTEGER, IsDefault TEXT, IsForced TEXT, Profile TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") + embydb.cursor.execute("CREATE TABLE IF NOT EXISTS Subtitle(emby_id TEXT, MediaIndex INTEGER, SubtitleIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, IsDefault TEXT, IsForced TEXT, Path TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") + columns = embydb.cursor.execute("SELECT * FROM VideoStreams") + descriptions = [description[0] for description in columns.description] -#Create the tables for the emby database -def emby_tables(cursor): - cursor.execute("CREATE TABLE IF NOT EXISTS emby(emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER, emby_parent_id TEXT)") - cursor.execute("CREATE TABLE IF NOT EXISTS view(view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)") - cursor.execute("CREATE TABLE IF NOT EXISTS MediaSources(emby_id TEXT, MediaIndex INTEGER, Protocol TEXT, MediaSourceId TEXT, Path TEXT, Type TEXT, Container TEXT, Size INTEGER, Name TEXT, IsRemote TEXT, RunTimeTicks INTEGER, SupportsTranscoding TEXT, SupportsDirectStream TEXT, SupportsDirectPlay TEXT, IsInfiniteStream TEXT, RequiresOpening TEXT, RequiresClosing TEXT, RequiresLooping TEXT, SupportsProbing TEXT, Formats TEXT, Bitrate INTEGER, RequiredHttpHeaders TEXT, ReadAtNativeFramerate TEXT, DefaultAudioStreamIndex INTEGER)") - cursor.execute("CREATE TABLE IF NOT EXISTS VideoStreams(emby_id TEXT, MediaIndex INTEGER, VideoIndex INTEGER, Codec TEXT, TimeBase TEXT, CodecTimeBase TEXT, VideoRange TEXT, DisplayTitle TEXT, IsInterlaced TEXT, BitRate INTEGER, BitDepth INTEGER, RefFrames INTEGER, IsDefault TEXT, IsForced TEXT, Height INTEGER, Width INTEGER, AverageFrameRate INTEGER, RealFrameRate INTEGER, Profile TEXT, Type TEXT, AspectRatio TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, PixelFormat TEXT, Level INTEGER, IsAnamorphic TEXT, StreamIndex INTEGER)") - cursor.execute("CREATE TABLE IF NOT EXISTS AudioStreams(emby_id TEXT, MediaIndex INTEGER, AudioIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, ChannelLayout TEXT, BitRate INTEGER, Channels INTEGER, SampleRate INTEGER, IsDefault TEXT, IsForced TEXT, Profile TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") - cursor.execute("CREATE TABLE IF NOT EXISTS Subtitle(emby_id TEXT, MediaIndex INTEGER, SubtitleIndex INTEGER, Codec TEXT, Language TEXT, TimeBase TEXT, CodecTimeBase TEXT, DisplayTitle TEXT, DisplayLanguage TEXT, IsInterlaced TEXT, IsDefault TEXT, IsForced TEXT, Path TEXT, Type TEXT, IsExternal TEXT, IsTextSubtitleStream TEXT, SupportsExternalStream TEXT, Protocol TEXT, StreamIndex INTEGER)") + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column VideoStreams -> StreamIndex") + embydb.cursor.execute("ALTER TABLE VideoStreams ADD COLUMN StreamIndex 'INTEGER'") - columns = cursor.execute("SELECT * FROM VideoStreams") - descriptions = [description[0] for description in columns.description] + columns = embydb.cursor.execute("SELECT * FROM AudioStreams") + descriptions = [description[0] for description in columns.description] - if 'StreamIndex' not in descriptions: - LOG.info("Add missing column VideoStreams -> StreamIndex") - cursor.execute("ALTER TABLE VideoStreams ADD COLUMN StreamIndex 'INTEGER'") + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column AudioStreams -> StreamIndex") + embydb.cursor.execute("ALTER TABLE AudioStreams ADD COLUMN StreamIndex 'INTEGER'") - columns = cursor.execute("SELECT * FROM AudioStreams") - descriptions = [description[0] for description in columns.description] + columns = embydb.cursor.execute("SELECT * FROM Subtitle") + descriptions = [description[0] for description in columns.description] - if 'StreamIndex' not in descriptions: - LOG.info("Add missing column AudioStreams -> StreamIndex") - cursor.execute("ALTER TABLE AudioStreams ADD COLUMN StreamIndex 'INTEGER'") + if 'StreamIndex' not in descriptions: + LOG.info("Add missing column Subtitle -> StreamIndex") + embydb.cursor.execute("ALTER TABLE Subtitle ADD COLUMN StreamIndex 'INTEGER'") - columns = cursor.execute("SELECT * FROM Subtitle") - descriptions = [description[0] for description in columns.description] + columns = embydb.cursor.execute("SELECT * FROM emby") + descriptions = [description[0] for description in columns.description] - if 'StreamIndex' not in descriptions: - LOG.info("Add missing column Subtitle -> StreamIndex") - cursor.execute("ALTER TABLE Subtitle ADD COLUMN StreamIndex 'INTEGER'") + if 'emby_parent_id' not in descriptions: + LOG.info("Add missing column emby_parent_id") + embydb.cursor.execute("ALTER TABLE emby ADD COLUMN emby_parent_id 'TEXT'") - columns = cursor.execute("SELECT * FROM emby") - descriptions = [description[0] for description in columns.description] + if 'presentation_key' not in descriptions: + LOG.info("Add missing column presentation_key") + embydb.cursor.execute("ALTER TABLE emby ADD COLUMN presentation_key 'TEXT'") - if 'emby_parent_id' not in descriptions: - LOG.info("Add missing column emby_parent_id") - cursor.execute("ALTER TABLE emby ADD COLUMN emby_parent_id 'TEXT'") + columns = embydb.cursor.execute("SELECT * FROM view") + descriptions = [description[0] for description in columns.description] - if 'presentation_key' not in descriptions: - LOG.info("Add missing column presentation_key") - cursor.execute("ALTER TABLE emby ADD COLUMN presentation_key 'TEXT'") + if 'server_id' not in descriptions: + LOG.info("Add missing column server_id") + embydb.cursor.execute("ALTER TABLE view ADD COLUMN server_id 'TEXT'") #Reset both the emby database and the kodi database. -def reset(Force=False): +def reset(Utils, Force): # views = emby.views.Views(Utils) if not Force: - if not Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33074)): + if not Utils.dialog("yesno", heading="{emby}", line1=Utils.Translate(33074)): return - Utils.window('emby_should_stop.bool', True) - count = 60 + Utils.Settings.emby_shouldstop = True - while Utils.window('emby_sync.bool'): - LOG.info("Sync is running...") - count -= 1 + if xbmc.Monitor().waitForAbort(5): + return - if not count: - Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33085)) - return - - if xbmc.Monitor().waitForAbort(1): - return - - reset_kodi() - reset_emby() + reset_kodi(Utils) + reset_emby(Utils) # views.delete_playlists() # views.delete_nodes() -# if Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33086)): - reset_artwork() + if Utils.dialog("yesno", heading="{emby}", line1=Utils.Translate(33086)): + reset_artwork(Utils) addon_data = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - if Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33087)): + if Utils.dialog("yesno", heading="{emby}", line1=Utils.Translate(33087)): xbmcvfs.delete(os.path.join(addon_data, "settings.xml")) xbmcvfs.delete(os.path.join(addon_data, "data.json")) LOG.info("[ reset settings ]") @@ -207,19 +123,18 @@ def reset(Force=False): if xbmcvfs.exists(os.path.join(addon_data, "sync.json")): xbmcvfs.delete(os.path.join(addon_data, "sync.json")) - Utils.settings('enableMusic.bool', False) - Utils.settings('MinimumSetup', "") - Utils.settings('MusicRescan.bool', False) - Utils.settings('SyncInstallRunDone.bool', False) - Utils.settings('Migrate.bool', True) - Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33088)) + Utils.Settings.set_settings('MinimumSetup', "") + Utils.Settings.set_settings_bool('MusicRescan', False) + Utils.Settings.set_settings_bool('SyncInstallRunDone', False) + Utils.Settings.set_settings_bool('Migrate', True) + Utils.dialog("ok", heading="{emby}", line1=Utils.Translate(33088)) xbmc.executebuiltin('RestartApp') -def reset_kodi(): +def reset_kodi(Utils): Progress = xbmcgui.DialogProgressBG() - Progress.create(helper.translate._('addon_name'), "Delete Kodi-Video Database") + Progress.create(Utils.Translate('addon_name'), "Delete Kodi-Video Database") - with Database() as videodb: + with Database(Utils, 'video', True) as videodb: videodb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") tables = videodb.cursor.fetchall() Counter = 0 @@ -235,10 +150,10 @@ def reset_kodi(): Progress.close() - with Database('music') as musicdb: + with Database(Utils, 'music', True) as musicdb: musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") Progress = xbmcgui.DialogProgressBG() - Progress.create(helper.translate._('addon_name'), "Delete Kodi-Music Database") + Progress.create(Utils.Translate('addon_name'), "Delete Kodi-Music Database") tables = musicdb.cursor.fetchall() Counter = 0 Increment = 100.0 / (len(tables) - 1) @@ -255,11 +170,11 @@ def reset_kodi(): LOG.warning("[ reset kodi ]") -def reset_emby(): +def reset_emby(Utils): Progress = xbmcgui.DialogProgressBG() - Progress.create(helper.translate._('addon_name'), "Delete Emby Database") + Progress.create(Utils.Translate('addon_name'), "Delete Emby Database") - with Database('emby') as embydb: + with Database(Utils, 'emby', True) as embydb: embydb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") tables = embydb.cursor.fetchall() Counter = 0 @@ -281,16 +196,16 @@ def reset_emby(): LOG.warning("[ reset emby ]") #Remove all existing texture -def reset_artwork(): +def reset_artwork(Utils): thumbnails = Utils.translatePath('special://thumbnails/') if xbmcvfs.exists(thumbnails): - dirs, ignore = xbmcvfs.listdir(thumbnails) + dirs, _ = xbmcvfs.listdir(thumbnails) for directory in dirs: - ignore, thumbs = xbmcvfs.listdir(os.path.join(thumbnails, directory)) + _, thumbs = xbmcvfs.listdir(os.path.join(thumbnails, directory)) Progress = xbmcgui.DialogProgressBG() - Progress.create(helper.translate._('addon_name'), "Delete Artwork Files: " + directory) + Progress.create(Utils.Translate('addon_name'), "Delete Artwork Files: " + directory) Counter = 0 ThumbsLen = len(thumbs) Increment = 0.0 @@ -301,15 +216,15 @@ def reset_artwork(): for thumb in thumbs: Counter += 1 Progress.update(int(Counter * Increment), message="Delete Artwork Files: " + directory + " / " + thumb) - LOG.debug("DELETE thumbnail %s", thumb) + LOG.debug("DELETE thumbnail %s" % thumb) xbmcvfs.delete(os.path.join(thumbnails, directory, thumb)) Progress.close() Progress = xbmcgui.DialogProgressBG() - Progress.create(helper.translate._('addon_name'), "Delete Texture Database") + Progress.create(Utils.Translate('addon_name'), "Delete Texture Database") - with Database('texture') as texdb: + with Database(Utils, 'texture', True) as texdb: texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") tables = texdb.cursor.fetchall() Counter = 0 @@ -327,73 +242,9 @@ def reset_artwork(): LOG.warning("[ reset artwork ]") -def get_sync(): - path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - - if not xbmcvfs.exists(path): - xbmcvfs.mkdirs(path) - - try: - with open(os.path.join(path, 'sync.json'), 'rb') as infile: - sync = json.load(infile, encoding='utf-8') - except Exception: - sync = {} - - sync['Libraries'] = sync.get('Libraries', []) - sync['RestorePoint'] = sync.get('RestorePoint', {}) - sync['Whitelist'] = list(set(sync.get('Whitelist', []))) - sync['SortedViews'] = sync.get('SortedViews', []) - return sync - -def save_sync(sync): - path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - - if not xbmcvfs.exists(path): - xbmcvfs.mkdirs(path) - - sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - - with open(os.path.join(path, 'sync.json'), 'wb') as outfile: - data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False) - outfile.write(data.encode('utf-8')) - -def get_credentials(): - path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - - if not xbmcvfs.exists(path): - xbmcvfs.mkdirs(path) - - try: - with open(os.path.join(path, 'data.json'), 'rb') as infile: - credentials = json.load(infile, encoding='utf8') - except Exception: - try: - with open(os.path.join(path, 'data.txt'), 'rb') as infile: - credentials = json.load(infile, encoding='utf-8') - save_credentials(credentials) - - xbmcvfs.delete(os.path.join(path, 'data.txt')) - except Exception: - credentials = {} - - credentials['Servers'] = credentials.get('Servers', []) - return credentials - -def save_credentials(credentials): - credentials = credentials or {} - path = Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - - if not xbmcvfs.exists(path): - xbmcvfs.mkdirs(path) - - credentials = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False) - - with open(os.path.join(path, 'data.json'), 'wb') as outfile: - outfile.write(credentials.encode('utf-8')) - #Get Kodi ID from emby ID -def get_kodiID(emby_id): - with Database('emby') as embydb: +def get_kodiID(Utils, emby_id): + with Database(Utils, 'emby', False) as embydb: item = emby_db.EmbyDatabase(embydb.cursor).get_item_by_wild_id(emby_id) if not item: @@ -403,8 +254,8 @@ def get_kodiID(emby_id): return item #Get emby item based on kodi id and media -def get_item(kodi_id, media): - with Database('emby') as embydb: +def get_item(Utils, kodi_id, media): + with Database(Utils, 'emby', False) as embydb: item = emby_db.EmbyDatabase(embydb.cursor).get_full_item_by_kodi_id(kodi_id, media) if not item: @@ -413,8 +264,8 @@ def get_item(kodi_id, media): return item -def get_item_complete(kodi_id, media): - with Database('emby') as embydb: +def get_item_complete(Utils, kodi_id, media): + with Database(Utils, 'emby', False) as embydb: item = emby_db.EmbyDatabase(embydb.cursor).get_full_item_by_kodi_id_complete(kodi_id, media) if not item: @@ -423,8 +274,8 @@ def get_item_complete(kodi_id, media): return item -def get_Presentationkey(EmbyID): - with Database('emby') as embydb: +def get_Presentationkey(Utils, EmbyID): + with Database(Utils, 'emby', False) as embydb: item = emby_db.EmbyDatabase(embydb.cursor).get_kodiid(EmbyID) if not item: @@ -434,8 +285,8 @@ def get_Presentationkey(EmbyID): return item[1] #Get emby item based on kodi id and media -def get_ItemsByPresentationkey(PresentationKey): - with Database('emby') as embydb: +def get_ItemsByPresentationkey(Utils, PresentationKey): + with Database(Utils, 'emby', False) as embydb: items = emby_db.EmbyDatabase(embydb.cursor).get_ItemsByPresentation_key(PresentationKey) return items diff --git a/database/library.py b/database/library.py index 61b40a0c2..0401827f2 100644 --- a/database/library.py +++ b/database/library.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -import logging +import threading +import _strptime # Workaround for threads using datetime: _striptime is locked +from datetime import datetime, timedelta try: import queue as Queue except ImportError: import Queue -import threading -from datetime import datetime, timedelta import xbmc import xbmcgui @@ -15,31 +15,26 @@ import core.musicvideos import core.tvshows import core.music -import emby.main -import emby.downloader import emby.views -import helper.translate -import helper.exceptions +import helper.xmls +import helper.loghandler from . import database from . import sync +from . import emby_db class Library(threading.Thread): - def __init__(self, Utils): - self.LOG = logging.getLogger("EMBY.library.Library") - self.started = False + def __init__(self, Player, EmbyServer): + self.LOG = helper.loghandler.LOG('EMBY.library.Library') + self.Player = Player + self.EmbyServer = EmbyServer + self.SyncSkipResume = False + self.SyncLater = False self.stop_thread = False self.suspend = False self.pending_refresh = False self.screensaver = None self.progress_updates = None self.total_updates = 0 - self.Utils = Utils - self.direct_path = self.Utils.settings('useDirectPaths') == "1" - self.LIMIT = min(int(self.Utils.settings('limitIndex') or 50), 50) - self.DTHREADS = int(self.Utils.settings('limitThreads') or 3) - self.MEDIA = {'Movie': core.movies.Movies, 'BoxSet': core.movies.Movies, 'MusicVideo': core.musicvideos.MusicVideos, 'TVShow': core.tvshows.TVShows, 'Series': core.tvshows.TVShows, 'Season': core.tvshows.TVShows, 'Episode': core.tvshows.TVShows, 'Music': core.music.Music, 'MusicAlbum': core.music.Music, 'MusicArtist': core.music.Music, 'AlbumArtist': core.music.Music, 'Audio': core.music.Music, 'MusicDisableScan': core.music.MusicDBIO} - self.Downloader = emby.downloader.Downloader(self.Utils) - self.server = emby.main.Emby().get_client() self.updated_queue = Queue.Queue() self.userdata_queue = Queue.Queue() self.removed_queue = Queue.Queue() @@ -53,11 +48,14 @@ def __init__(self, Utils): self.emby_threads = [] self.download_threads = [] self.notify_threads = [] - self.writer_threads = {'updated': [], 'userdata': [], 'removed': []} - self.database_lock = threading.Lock() - self.music_database_lock = threading.Lock() - self.sync = sync.Sync + self.writer_threads_updated = [] + self.writer_threads_userdata = [] + self.writer_threads_removed = [] + self.ThreadingLock = threading.Lock() + self.sync = sync.Sync(self.EmbyServer, self.Player, self.ThreadingLock) + self.Views = emby.views.Views(self.EmbyServer) self.progress_percent = 0 + self.Xmls = helper.xmls.Xmls(self.EmbyServer.Utils) threading.Thread.__init__(self) self.start() @@ -94,79 +92,93 @@ def get_naming(self, item): def set_progress_dialog(self): queue_size = self.worker_queue_size() - try: - self.progress_percent = int((float(self.total_updates - queue_size) / float(self.total_updates))*100) - except Exception: + if self.total_updates: + self.progress_percent = int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100) + else: self.progress_percent = 0 - self.LOG.debug("--[ pdialog (%s/%s) ]", queue_size, self.total_updates) + self.LOG.debug("--[ pdialog (%s/%s) ]" % (queue_size, self.total_updates)) - if self.total_updates < int(self.Utils.settings('syncProgress') or 50): + if self.total_updates < int(self.EmbyServer.Utils.Settings.syncProgress): return if self.progress_updates is None: self.LOG.info("-->[ pdialog ]") self.progress_updates = xbmcgui.DialogProgressBG() - self.progress_updates.create(helper.translate._('addon_name'), helper.translate._(33178)) + self.progress_updates.create(self.EmbyServer.Utils.Translate('addon_name'), self.EmbyServer.Utils.Translate(33178)) def update_progress_dialog(self, item): if self.progress_updates: message = self.get_naming(item) - self.progress_updates.update(self.progress_percent, message="%s: %s" % (helper.translate._(33178), message)) + self.progress_updates.update(self.progress_percent, message="%s: %s" % (self.EmbyServer.Utils.Translate(33178), message)) def run(self): self.LOG.warning("--->[ library ]") + self.Views.update_views() + self.sync.update_library = False - while not self.stop_thread: - if xbmc.Monitor().waitForAbort(1): - break + if self.EmbyServer.Utils.SyncData['Libraries']: + if not self.sync.mapping(False): + self.SyncSkipResume = True - try: - if not self.started and not self.startup(): - self.stop_client() + self.sync.FullSync() + elif not self.EmbyServer.Utils.Settings.SyncInstallRunDone: + self.Xmls.sources() - if self.sync.running: - continue + if not self.sync.mapping(False): + if self.SyncLater: + self.EmbyServer.Utils.dialog("ok", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33129)) + self.EmbyServer.Utils.Settings.set_settings_bool('SyncInstallRunDone', True) + self.EmbyServer.Utils.SyncData['Libraries'] = [] + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData) - self.service() + xbmc.executebuiltin('ReloadSkin()') - except helper.exceptions.LibraryException as error: - if error.status == 'StopWriteCalled': - continue + self.Views.update_nodes() + self.get_fast_sync() + while not self.stop_thread: + if xbmc.Monitor().waitForAbort(0.5): break - except Exception as error: - self.LOG.exception(error) - break + if self.sync.running: + continue - if xbmc.Monitor().waitForAbort(2): - break + self.service() - self.Utils.window('emby_sync', clear=True) + if self.Player.SyncPause: + continue + + self.Player.SyncPause = False self.LOG.warning("---<[ library ]") - @helper.wrapper.stop def service(self): - ''' If error is encountered, it will rerun this function. - Start new "daemon threads" to process library updates. - (actual daemon thread is not supported in Kodi) - ''' for thread in self.download_threads: - if thread.Done: + if thread.Done(): self.removed(thread.removed) self.download_threads.remove(thread) - for threads in (self.emby_threads, self.writer_threads['updated'], self.writer_threads['userdata'], self.writer_threads['removed']): + for threads in (self.emby_threads, self.writer_threads_updated, self.writer_threads_userdata, self.writer_threads_removed): for thread in threads: - if thread.Done: + if thread.Done(): threads.remove(thread) - if not xbmc.Player().isPlayingVideo() or self.Utils.settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): - if not xbmc.Player().isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): - if not self.Utils.window('emby_sync_skip_resume.bool'): - if database.get_sync()['Libraries']: - self.sync_libraries(True) + PlayingVideo = self.Player.isPlayingVideo() + + if not PlayingVideo or self.EmbyServer.Utils.Settings.syncDuringPlay or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + if not PlayingVideo or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'): + if not self.SyncSkipResume: + if self.EmbyServer.Utils.SyncData['Libraries']: + self.sync.update_library = False + + if not self.sync.mapping(True): + self.SyncSkipResume = True + + self.sync.FullSync() + self.Views.update_nodes() + + if self.Player.SyncPause: + return self.worker_remove_lib() self.worker_add_lib() @@ -180,41 +192,31 @@ def service(self): self.worker_notify() if self.pending_refresh: - self.Utils.window('emby_sync.bool', True) self.set_progress_dialog() - if not self.Utils.settings('dbSyncScreensaver.bool') and self.screensaver is None: + if not self.EmbyServer.Utils.Settings.dbSyncScreensaver and self.screensaver is None: xbmc.executebuiltin('InhibitIdleShutdown(true)') - self.screensaver = self.Utils.get_screensaver() - self.Utils.set_screensaver(value="") + self.screensaver = self.EmbyServer.Utils.Screensaver + self.EmbyServer.Utils.set_screensaver(value="") - if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']): + if self.pending_refresh and not self.download_threads and not self.writer_threads_updated and not self.writer_threads_userdata and not self.writer_threads_removed: self.pending_refresh = False - self.save_last_sync() + self.EmbyServer.Utils.save_last_sync() self.total_updates = 0 - self.Utils.window('emby_sync', clear=True) if self.progress_updates: self.LOG.info("--<[ pdialog ]") self.progress_updates.close() self.progress_updates = None - if not self.Utils.settings('dbSyncScreensaver.bool') and self.screensaver is not None: + if not self.EmbyServer.Utils.Settings.dbSyncScreensaver and self.screensaver is not None: xbmc.executebuiltin('InhibitIdleShutdown(false)') - self.Utils.set_screensaver(value=self.screensaver) + self.EmbyServer.Utils.set_screensaver(value=self.screensaver) self.screensaver = None if not xbmc.getCondVisibility('Window.IsMedia'): xbmc.executebuiltin('UpdateLibrary(video)') - def stop_client(self): - self.stop_thread = True - - #When there's an active thread. Let the main thread know - def enable_pending_refresh(self): - self.pending_refresh = True - self.Utils.window('emby_sync.bool', True) - #Get how many items are queued up for worker threads def worker_queue_size(self): total = 0 @@ -248,212 +250,147 @@ def _worker_removed_size(self): return total - #Wait 60 seconds to verify the item by moving it to the updated queue to - #verify item is still available to user. - #Used for internal deletion--callback takes too long - #Used for parental control--server does not send a new event when item has been blocked. def worker_verify(self): if self.verify_queue.qsize(): ready = [] not_ready = [] - while True: - try: - time_set, item_id = self.verify_queue.get(timeout=1) - except Queue.Empty: - break + while not self.verify_queue.empty(): + time_set, item_id = self.verify_queue.get() if time_set <= datetime.today(): ready.append(item_id) elif item_id not in list(self.removed_queue.queue): not_ready.append((time_set, item_id,)) - self.verify_queue.task_done() - self.updated(ready) list(map(self.verify_queue.put, not_ready)) # re-add items that are not ready yet #Get items from emby and place them in the appropriate queues def worker_downloads(self): for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)): - if queue[0].qsize() and len(self.download_threads) < self.DTHREADS: - new_thread = emby.downloader.GetItemWorker(self.server, queue[0], queue[1], self.Utils) - self.LOG.info("-->[ q:download/%s ]", id(new_thread)) + if queue[0].qsize() and len(self.download_threads) < int(self.EmbyServer.Utils.Settings.limitThreads): + new_thread = GetItemWorker(self, queue[0], queue[1]) + self.LOG.info("-->[ q:download/%s ]" % id(new_thread)) self.download_threads.append(new_thread) #Get items based on the local emby database and place item in appropriate queues def worker_sort(self): if self.removed_queue.qsize() and len(self.emby_threads) < 2: new_thread = SortWorker(self) - self.LOG.info("-->[ q:sort/%s ]", id(new_thread)) + self.LOG.info("-->[ q:sort/%s ]" % id(new_thread)) self.emby_threads.append(new_thread) #Update items in the Kodi database def worker_updates(self): - if self._worker_removed_size(): + if self._worker_removed_size() or len(self.writer_threads_updated): self.LOG.info("[ DELAY UPDATES ]") return for queues in self.updated_output: queue = self.updated_output[queues] - if queue.qsize() and not len(self.writer_threads['updated']): + if queue.qsize(): # and not len(self.writer_threads_updated): if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = UpdatedWorker(queue, self, "music", self.music_database_lock) + new_thread = UpdatedWorker(queue, self, "music") else: - new_thread = UpdatedWorker(queue, self, "video", self.database_lock) + new_thread = UpdatedWorker(queue, self, "video") - self.LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread)) - self.writer_threads['updated'].append(new_thread) - self.enable_pending_refresh() + self.LOG.info("-->[ q:updated/%s/%s ]" % (queues, id(new_thread))) + self.writer_threads_updated.append(new_thread) + self.pending_refresh = True #Update userdata in the Kodi database def worker_userdata(self): - if self._worker_removed_size(): + if self._worker_removed_size() or len(self.writer_threads_userdata): self.LOG.info("[ DELAY UPDATES ]") return for queues in self.userdata_output: queue = self.userdata_output[queues] - if queue.qsize() and not len(self.writer_threads['userdata']): + if queue.qsize(): # and not len(self.writer_threads_userdata): if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = UserDataWorker(queue, self, "music", self.music_database_lock) + new_thread = UserDataWorker(queue, self, "music") else: - new_thread = UserDataWorker(queue, self, "video", self.database_lock) + new_thread = UserDataWorker(queue, self, "video") - self.LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread)) - self.writer_threads['userdata'].append(new_thread) - self.enable_pending_refresh() + self.LOG.info("-->[ q:userdata/%s/%s ]" % (queues, id(new_thread))) + self.writer_threads_userdata.append(new_thread) + self.pending_refresh = True #Remove items from the Kodi database def worker_remove(self): + if len(self.writer_threads_removed): + self.LOG.info("[ DELAY UPDATES ]") + return + for queues in self.removed_output: queue = self.removed_output[queues] - if queue.qsize() and not len(self.writer_threads['removed']): + if queue.qsize(): # and not len(self.writer_threads_removed): if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'): - new_thread = RemovedWorker(queue, self, "music", self.music_database_lock) + new_thread = RemovedWorker(queue, self, "music") else: - new_thread = RemovedWorker(queue, self, "video", self.database_lock) + new_thread = RemovedWorker(queue, self, "video") - self.LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread)) - self.writer_threads['removed'].append(new_thread) - self.enable_pending_refresh() + self.LOG.info("-->[ q:removed/%s/%s ]" % (queues, id(new_thread))) + self.writer_threads_removed.append(new_thread) + self.pending_refresh = True #Notify the user of new additions def worker_notify(self): if self.notify_output.qsize() and not len(self.notify_threads): new_thread = NotifyWorker(self) - self.LOG.info("-->[ q:notify/%s ]", id(new_thread)) + self.LOG.info("-->[ q:notify/%s ]" % id(new_thread)) self.notify_threads.append(new_thread) def worker_remove_lib(self): if self.remove_lib_queue.qsize(): - while True: - try: - library_id = self.remove_lib_queue.get(timeout=0.5) - except Queue.Empty: - break - - self._remove_libraries(library_id) - self.remove_lib_queue.task_done() + while not self.remove_lib_queue.empty(): + library_id = self.remove_lib_queue.get() + self.sync.remove_library(library_id) + self.Views.remove_library(library_id) + + if self.Player.SyncPause: + return + xbmc.executebuiltin("Container.Refresh") def worker_add_lib(self): if self.add_lib_queue.qsize(): - while True: - try: - library_id, update = self.add_lib_queue.get(timeout=0.5) - except Queue.Empty: - break + while not self.add_lib_queue.empty(): + library_id, update = self.add_lib_queue.get() + self.sync.update_library = update - self._add_libraries(library_id, update) - self.add_lib_queue.task_done() + if library_id not in [x.replace('Mixed:', "") for x in self.EmbyServer.Utils.SyncData['Libraries']]: + with database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: + library = emby_db.EmbyDatabase(embydb.cursor).get_view(library_id) - xbmc.executebuiltin("Container.Refresh") + if library: + self.EmbyServer.Utils.SyncData['Libraries'].append("Mixed:%s" % library_id if library[2] == 'mixed' else library_id) + + if library[2] in ('mixed', 'movies'): + self.EmbyServer.Utils.SyncData['Libraries'].append('Boxsets:%s' % library_id) + else: + self.EmbyServer.Utils.SyncData['Libraries'].append(library_id) + + self.sync.FullSync() + self.Views.update_nodes() + + if self.Player.SyncPause: + return + + self.Views.update_nodes() - def sync_libraries(self, forced=False): - try: - with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: - syncObj.libraries(forced=forced) - except helper.exceptions.LibraryException as error: - raise - - emby.views.Views(self.Utils).get_nodes() - - def _add_libraries(self, library_id, update=False): - try: - with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: - syncObj.libraries(library_id, update) - except helper.exceptions.LibraryException as error: - raise - - emby.views.Views(self.Utils).get_nodes() - - def _remove_libraries(self, library_id): - try: - with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: - syncObj.remove_library(library_id) - - except helper.exceptions.LibraryException as error: - raise - - ViewsClass = emby.views.Views(self.Utils) - ViewsClass.remove_library(library_id) - ViewsClass.get_views() - ViewsClass.get_nodes() - - #Run at startup. - #Check databases. - #Check for the server plugin. - def startup(self): - self.started = True - ViewsClass = emby.views.Views(self.Utils) - ViewsClass.get_views() - ViewsClass.get_nodes() - - try: - if database.get_sync()['Libraries']: - self.sync_libraries() - elif not self.Utils.settings('SyncInstallRunDone.bool'): - with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: - syncObj.libraries() - - ViewsClass.get_nodes() - xbmc.executebuiltin('ReloadSkin()') - return True - - self.get_fast_sync() - return True - except helper.exceptions.LibraryException as error: - self.LOG.error(error.status) - - if error.status in 'SyncLibraryLater': - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33129)) - self.Utils.settings('SyncInstallRunDone.bool', True) - syncLibs = database.get_sync() - syncLibs['Libraries'] = [] - database.save_sync(syncLibs) - return True - - if error.status == 'CompanionMissing': - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33099)) - self.Utils.settings('kodiCompanion.bool', False) - return True - - raise - except Exception as error: - self.LOG.exception(error) - - return False + xbmc.executebuiltin("Container.Refresh") def get_fast_sync(self): enable_fast_sync = False - if self.Utils.settings('SyncInstallRunDone.bool'): - if self.Utils.settings('kodiCompanion.bool'): - for plugin in self.server['api'].get_plugins(): + if self.EmbyServer.Utils.Settings.SyncInstallRunDone: + if self.EmbyServer.Utils.Settings.kodiCompanion: + for plugin in self.EmbyServer.API.get_plugins(): if plugin['Name'] in ("Emby.Kodi Sync Queue", "Kodi companion"): enable_fast_sync = True break @@ -463,172 +400,176 @@ def get_fast_sync(self): #Movie and userdata not provided by server yet def fast_sync(self, plugin): - last_sync = self.Utils.settings('LastIncrementalSync') - syncOBJ = database.get_sync() - self.LOG.info("--[ retrieve changes ] %s", last_sync) - - for library in syncOBJ['Whitelist']: - for data in self.Downloader.get_items(library.replace('Mixed:', ""), "Series,Season,Episode,BoxSet,Movie,MusicVideo,MusicArtist,MusicAlbum,Audio", False, {'MinDateLastSaved': last_sync}): - with database.Database('emby') as embydb: - emby_db = database.emby_db.EmbyDatabase(embydb.cursor) - - for item in data['Items']: - if item['Type'] in self.updated_output: - item['Library'] = {} - item['Library']['Id'] = library - item['Library']['Name'] = emby_db.get_view_name(library) - self.updated_output[item['Type']].put(item) - self.total_updates += 1 - - for data in self.Downloader.get_items(library.replace('Mixed:', ""), "Episode,Movie,MusicVideo,Audio", False, {'MinDateLastSavedForUser': last_sync}): - with database.Database('emby') as embydb: - emby_db = database.emby_db.EmbyDatabase(embydb.cursor) - - for item in data['Items']: - if item['Type'] in self.userdata_output: - item['Library'] = {} - item['Library']['Id'] = library - item['Library']['Name'] = emby_db.get_view_name(library) - self.userdata_output[item['Type']].put(item) - self.total_updates += 1 + last_sync = self.EmbyServer.Utils.Settings.LastIncrementalSync + self.LOG.info("--[ retrieve changes ] %s" % last_sync) + LibraryViews = {} + + with database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: + db = database.emby_db.EmbyDatabase(embydb.cursor) + result = db.get_views() + + for Data in result: + LibID, _, LibContent, _ = Data + LibraryViews[LibID] = LibContent + + for library in self.EmbyServer.Utils.SyncData['Whitelist']: + library = library.replace('Mixed:', "") + + if LibraryViews[library] == "musicvideos": + result = self.EmbyServer.API.get_itemsSync(library, "MusicVideo", False, {'MinDateLastSaved': last_sync}) + elif LibraryViews[library] == "movies": + result = self.EmbyServer.API.get_itemsSync(library, "BoxSet,Movie", False, {'MinDateLastSaved': last_sync}) + elif LibraryViews[library] == "tvshows": + result = self.EmbyServer.API.get_itemsSync(library, "Series,Season,Episode", False, {'MinDateLastSaved': last_sync}) + elif LibraryViews[library] == "music": + result = self.EmbyServer.API.get_itemsSync(library, "MusicArtist,MusicAlbum,Audio", False, {'MinDateLastSaved': last_sync}) + elif LibraryViews[library] == "mixed": + result = self.EmbyServer.API.get_itemsSync(library.replace('Mixed:', ""), "Series,Season,Episode,BoxSet,Movie,MusicVideo,MusicArtist,MusicAlbum,Audio", False, {'MinDateLastSaved': last_sync}) + else: + result = False + + if result: + for data in result: + for item in data['Items']: + if item['Type'] in self.updated_output: + item['Library'] = {} + item['Library']['Id'] = library + item['Library']['Name'] = db.get_view_name(library) + self.updated_output[item['Type']].put(item) + self.total_updates += 1 + + if LibraryViews[library] == "musicvideos": + result = self.EmbyServer.API.get_itemsSync(library, "MusicVideo", False, {'MinDateLastSavedForUser': last_sync}) + elif LibraryViews[library] == "tvshows": + result = self.EmbyServer.API.get_itemsSync(library, "Episode", False, {'MinDateLastSavedForUser': last_sync}) + elif LibraryViews[library] == "movies": + result = self.EmbyServer.API.get_itemsSync(library, "Movie", False, {'MinDateLastSavedForUser': last_sync}) + elif LibraryViews[library] == "music": + result = self.EmbyServer.API.get_itemsSync(library, "Audio", False, {'MinDateLastSavedForUser': last_sync}) + elif LibraryViews[library] == "mixed": + result = self.EmbyServer.API.get_itemsSync(library.replace('Mixed:', ""), "Episode,Movie,MusicVideo,Audio", False, {'MinDateLastSavedForUser': last_sync}) + else: + result = False + + if result: + for data in result: + for item in data['Items']: + if item['Type'] in self.userdata_output: + item['Library'] = {} + item['Library']['Id'] = library + item['Library']['Name'] = db.get_view_name(library) + self.userdata_output[item['Type']].put(item) + self.total_updates += 1 if plugin: - try: - result = self.server['api'].get_sync_queue(last_sync) #Kodi companion plugin - self.removed(result['ItemsRemoved']) - except Exception as error: - self.LOG.exception(error) + result = self.EmbyServer.API.get_sync_queue(last_sync, None) #Kodi companion plugin + self.removed(result['ItemsRemoved']) return True - def save_last_sync(self): - time_now = datetime.utcnow() - timedelta(minutes=2) - last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') - self.Utils.settings('LastIncrementalSync', value=last_sync) - self.LOG.info("--[ sync/%s ]", last_sync) - #Select from libraries synced. Either update or repair libraries. #Send event back to service.py - def select_libraries(self, mode=None): - modes = { - 'SyncLibrarySelection': 'SyncLibrary', - 'RepairLibrarySelection': 'RepairLibrary', - 'AddLibrarySelection': 'SyncLibrary', - 'RemoveLibrarySelection': 'RemoveLibrary' - } - - syncOBJ = database.get_sync() - whitelist = [x.replace('Mixed:', "") for x in syncOBJ['Whitelist']] + def select_libraries(self, mode): + whitelist = [x.replace('Mixed:', "") for x in self.EmbyServer.Utils.SyncData['Whitelist']] libraries = [] - with database.Database('emby') as embydb: + with database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: db = database.emby_db.EmbyDatabase(embydb.cursor) if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'): - for library in syncOBJ['Whitelist']: + for library in self.EmbyServer.Utils.SyncData['Whitelist']: name = db.get_view_name(library.replace('Mixed:', "")) libraries.append({'Id': library, 'Name': name}) else: - available = [x for x in syncOBJ['SortedViews'] if x not in whitelist] + available = [x for x in self.EmbyServer.Utils.SyncData['SortedViews'] if x not in whitelist] for library in available: - name, media = db.get_view(library) + _, name, media, _ = db.get_view(library) if media in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): libraries.append({'Id': library, 'Name': name}) choices = [x['Name'] for x in libraries] - choices.insert(0, helper.translate._(33121)) - selection = self.Utils.dialog("multi", helper.translate._(33120), choices) + choices.insert(0, self.EmbyServer.Utils.Translate(33121)) + selection = self.EmbyServer.Utils.dialog("multi", self.EmbyServer.Utils.Translate(33120), choices) if selection is None: return + #"All" selected if 0 in selection: selection = list(range(1, len(libraries) + 1)) - selected_libraries = [] - - for x in selection: - library = libraries[x - 1] - selected_libraries.append(library['Id']) - - self.Utils.event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'}) - - def patch_music(self, notification=False): - try: - with self.sync(self, self.server, self.Downloader, self.Utils) as syncObj: - syncObj.patch_music(notification) - except Exception as error: - self.LOG.exception(error) - - def add_library(self, library_id, update=False): + if mode == 'SyncLibrarySelection': + for x in selection: + self.add_library(libraries[x - 1]['Id'], True) + elif mode == 'AddLibrarySelection': + for x in selection: + self.add_library(libraries[x - 1]['Id'], False) + elif mode == 'RepairLibrarySelection': + for x in selection: + self.remove_library(libraries[x - 1]['Id']) + self.add_library(libraries[x - 1]['Id'], False) + elif mode == 'RemoveLibrarySelection': + for x in selection: + self.remove_library(libraries[x - 1]['Id']) + + def patch_music(self, notification): + self.sync.patch_music(notification) + + def add_library(self, library_id, update): self.add_lib_queue.put((library_id, update)) - self.LOG.info("---[ added library: %s ]", library_id) + self.LOG.info("---[ added library: %s ]" % library_id) def remove_library(self, library_id): self.remove_lib_queue.put(library_id) - self.LOG.info("---[ removed library: %s ]", library_id) + self.LOG.info("---[ removed library: %s ]" % library_id) #Add item_id to userdata queue def userdata(self, data): - if not data: - return - items = [x['ItemId'] for x in data] - for item in self.Utils.split_list(items, self.LIMIT): + for item in self.EmbyServer.Utils.split_list(items): self.userdata_queue.put(item) self.total_updates += len(items) - self.LOG.info("---[ userdata:%s ]", len(items)) + self.LOG.info("---[ userdata:%s ]" % len(items)) #Add item_id to updated queue def updated(self, data): - if not data: - return - - for item in self.Utils.split_list(data, self.LIMIT): + for item in self.EmbyServer.Utils.split_list(data): self.updated_queue.put(item) self.total_updates += len(data) - self.LOG.info("---[ updated:%s ]", len(data)) + self.LOG.info("---[ updated:%s ]" % len(data)) #Add item_id to removed queue def removed(self, data): - if not data: - return - for item in data: - if item in list(self.removed_queue.queue): continue self.removed_queue.put(item) self.total_updates += len(data) - self.LOG.info("---[ removed:%s ]", len(data)) + self.LOG.info("---[ removed:%s ]" % len(data)) #Setup a 1 minute delay for items to be verified def delay_verify(self, data): - if not data: - return - time_set = datetime.today() + timedelta(seconds=60) for item in data: self.verify_queue.put((time_set, item,)) - self.LOG.info("---[ verify:%s ]", len(data)) + self.LOG.info("---[ verify:%s ]" % len(data)) class UpdatedWorker(threading.Thread): - def __init__(self, queue, library, DB, lock): - self.LOG = logging.getLogger("EMBY.library.UpdatedWorker") + def __init__(self, queue, library, DB): + self.LOG = helper.loghandler.LOG('EMBY.library.UpdatedWorker') self.library = library self.queue = queue - self.notify = self.library.Utils.settings('newContent.bool') - self.lock = lock - self.DB = database.Database(DB) + self.notify = self.library.EmbyServer.Utils.Settings.newContent + self.DB = database.Database(self.library.EmbyServer.Utils, DB, True) self.library.set_progress_dialog() self.is_done = False threading.Thread.__init__(self) @@ -638,47 +579,59 @@ def Done(self): return self.is_done def run(self): - with self.lock: + with self.library.ThreadingLock: with self.DB as kodidb: - with database.Database('emby') as embydb: - while True: - try: - item = self.queue.get(timeout=1) - except Queue.Empty: + with database.Database(self.library.EmbyServer.Utils, 'emby', True) as embydb: + while not self.queue.empty(): + item = self.queue.get() + self.library.update_progress_dialog(item) + + if 'Library' in item: + LibID = item['Library'] + else: + LibID = None + + if item['Type'] == 'Movie': + Ret = core.movies.Movies(self.library.EmbyServer, embydb, kodidb).movie(item, LibID) + elif item['Type'] == 'BoxSet': + Ret = core.movies.Movies(self.library.EmbyServer, embydb, kodidb).boxset(item) + elif item['Type'] == 'MusicVideo': + Ret = core.musicvideos.MusicVideos(self.library.EmbyServer, embydb, kodidb).musicvideo(item, LibID) + elif item['Type'] == 'Series': + Ret = core.tvshows.TVShows(self.library.EmbyServer, embydb, kodidb).tvshow(item, LibID) + elif item['Type'] == 'Season': + Ret = core.tvshows.TVShows(self.library.EmbyServer, embydb, kodidb).season(item, LibID) + elif item['Type'] == 'Episode': + Ret = core.tvshows.TVShows(self.library.EmbyServer, embydb, kodidb).episode(item, LibID) + elif item['Type'] == 'MusicAlbum': + Ret = core.music.Music(self.library.EmbyServer, embydb, kodidb).album(item, LibID) + elif item['Type'] == 'Audio': + Ret = core.music.Music(self.library.EmbyServer, embydb, kodidb).song(item, LibID) + elif item['Type'] in ('MusicArtist', 'AlbumArtist'): + Ret = core.music.Music(self.library.EmbyServer, embydb, kodidb).artist(item, LibID) + else: + self.LOG.error("Media Type not found: %s" % item['Type']) break - self.library.update_progress_dialog(item) + if Ret == "Invalid Filepath": + break - try: - if 'Library' in item: - if self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils)[item['Type']](item, item['Library']) and self.notify: - self.library.notify_output.put(item['Type'], self.library.get_naming(item)) - else: - if self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils)[item['Type']](item, None) and self.notify: - self.library.notify_output.put(item['Type'], self.library.get_naming(item)) - except helper.exceptions.LibraryException as error: - if error.status in ('StopCalled', 'StopWriteCalled'): - self.queue.put(item) - break - except Exception as error: - self.LOG.exception(error) - - self.queue.task_done() - - if self.library.Utils.window('emby_should_stop.bool'): + if Ret and self.notify: + self.library.notify_output.put({'Type': item['Type'], 'Name': self.library.get_naming(item)}) + + if self.library.Player.SyncPause: break - self.LOG.info("--<[ q:updated/%s ]", id(self)) + self.LOG.info("--<[ q:updated/%s ]" % id(self)) self.is_done = True #Incomming Update Data from Websocket class UserDataWorker(threading.Thread): - def __init__(self, queue, library, DB, lock): - self.LOG = logging.getLogger("EMBY.library.UserDataWorker") + def __init__(self, queue, library, DB): + self.LOG = helper.loghandler.LOG('EMBY.library.UserDataWorker') self.library = library self.queue = queue - self.lock = lock - self.DB = database.Database(DB) + self.DB = database.Database(self.library.EmbyServer.Utils, DB, True) self.is_done = False self.library.set_progress_dialog() threading.Thread.__init__(self) @@ -688,37 +641,34 @@ def Done(self): return self.is_done def run(self): - with self.lock: + with self.library.ThreadingLock: with self.DB as kodidb: - with database.Database('emby') as embydb: - while True: - try: - item = self.queue.get(timeout=1) - except Queue.Empty: - break - + with database.Database(self.library.EmbyServer.Utils, 'emby', True) as embydb: + while not self.queue.empty(): + item = self.queue.get() self.library.update_progress_dialog(item) - try: - self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils).userdata(item) - except helper.exceptions.LibraryException as error: - if error.status in ('StopCalled', 'StopWriteCalled'): - self.queue.put(item) - break - except Exception as error: - self.LOG.exception(error) + if item['Type'] in ('Movie', 'BoxSet'): + core.movies.Movies(self.library.EmbyServer, embydb, kodidb).userdata(item) + + elif item['Type'] == 'MusicVideo': + core.musicvideos.MusicVideos(self.library.EmbyServer, embydb, kodidb).userdata(item) + + elif item['Type'] in ('TVShow', 'Series', 'Season', 'Episode'): + core.tvshows.TVShows(self.library.EmbyServer, embydb, kodidb).userdata(item) - self.queue.task_done() + elif item['Type'] in ('Music', 'MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + core.music.Music(self.library.EmbyServer, embydb, kodidb).userdata(item) - if self.library.Utils.window('emby_should_stop.bool'): + if self.library.Player.SyncPause: break - self.LOG.info("--<[ q:userdata/%s ]", id(self)) + self.LOG.info("--<[ q:userdata/%s ]" % id(self)) self.is_done = True class SortWorker(threading.Thread): def __init__(self, library): - self.LOG = logging.getLogger("EMBY.library.SortWorker") + self.LOG = helper.loghandler.LOG('EMBY.library.SortWorker') self.library = library self.is_done = False threading.Thread.__init__(self) @@ -728,42 +678,37 @@ def Done(self): return self.is_done def run(self): - with database.Database('emby') as embydb: - db = database.emby_db.EmbyDatabase(embydb.cursor) - - while True: - try: - item_id = self.library.removed_queue.get(timeout=1) - except Queue.Empty: - break + with self.library.ThreadingLock: + with database.Database(self.library.EmbyServer.Utils, 'emby', True) as embydb: + db = database.emby_db.EmbyDatabase(embydb.cursor) - try: + while not self.library.removed_queue.empty(): + item_id = self.library.removed_queue.get() media = db.get_media_by_id(item_id) - self.library.removed_output[media].put({'Id': item_id, 'Type': media}) - except Exception: - items = db.get_media_by_parent_id(item_id) - if not items: - self.LOG.info("Could not find media %s in the emby database.", item_id) + if media: + self.library.removed_output[media].put({'Id': item_id, 'Type': media}) else: - for item in items: - self.library.removed_output[item[1]].put({'Id': item[0], 'Type': item[1]}) + items = db.get_media_by_parent_id(item_id) - self.library.removed_queue.task_done() + if not items: + self.LOG.info("Could not find media %s in the emby database." % item_id) + else: + for item in items: + self.library.removed_output[item[1]].put({'Id': item[0], 'Type': item[1]}) - if self.library.Utils.window('emby_should_stop.bool'): - break + if self.library.stop_thread: + break - self.LOG.info("--<[ q:sort/%s ]", id(self)) - self.is_done = True + self.LOG.info("--<[ q:sort/%s ]" % id(self)) + self.is_done = True class RemovedWorker(threading.Thread): - def __init__(self, queue, library, DB, lock): - self.LOG = logging.getLogger("EMBY.library.RemovedWorker") + def __init__(self, queue, library, DB): + self.LOG = helper.loghandler.LOG('EMBY.library.RemovedWorker') self.library = library self.queue = queue - self.lock = lock - self.DB = database.Database(DB) + self.DB = database.Database(self.library.EmbyServer.Utils, DB, True) self.is_done = False threading.Thread.__init__(self) self.start() @@ -772,61 +717,101 @@ def Done(self): return self.is_done def run(self): - with self.lock: + with self.library.ThreadingLock: with self.DB as kodidb: - with database.Database('emby') as embydb: - while True: - try: - item = self.queue.get(timeout=1) - except Queue.Empty: - break + with database.Database(self.library.EmbyServer.Utils, 'emby', True) as embydb: + while not self.queue.empty(): + item = self.queue.get() - try: - self.library.MEDIA[item['Type']](self.library.server, embydb, kodidb, self.library.direct_path, self.library.Utils).remove(item['Id']) - except helper.exceptions.LibraryException as error: + if item['Type'] in ('Movie', 'BoxSet'): + core.movies.Movies(self.library.EmbyServer, embydb, kodidb).remove(item['Id']) - if error.status in ('StopCalled', 'StopWriteCalled'): - self.queue.put(item) - break - except Exception as error: - self.LOG.exception(error) + elif item['Type'] == 'MusicVideo': + core.musicvideos.MusicVideos(self.library.EmbyServer, embydb, kodidb).remove(item['Id']) - self.queue.task_done() + elif item['Type'] in ('TVShow', 'Series', 'Season', 'Episode'): + core.tvshows.TVShows(self.library.EmbyServer, embydb, kodidb).remove(item['Id']) - if self.library.Utils.window('emby_should_stop.bool'): + elif item['Type'] in ('Music', 'MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + core.music.Music(self.library.EmbyServer, embydb, kodidb).remove(item['Id']) + + if self.library.Player.SyncPause: break - self.LOG.info("--<[ q:removed/%s ]", id(self)) + self.LOG.info("--<[ q:removed/%s ]" % id(self)) self.is_done = True class NotifyWorker(threading.Thread): def __init__(self, library): - self.LOG = logging.getLogger("EMBY.library.NotifyWorker") + self.LOG = helper.loghandler.LOG('EMBY.library.NotifyWorker') self.library = library - self.video_time = int(self.library.Utils.settings('newvideotime')) * 1000 - self.music_time = int(self.library.Utils.settings('newmusictime')) * 1000 + self.video_time = int(self.library.EmbyServer.Utils.Settings.newvideotime) * 1000 + self.music_time = int(self.library.EmbyServer.Utils.Settings.newmusictime) * 1000 self.is_done = False threading.Thread.__init__(self) + self.start() def Done(self): return self.is_done def run(self): - while True: - try: - item = self.library.notify_output.get(timeout=3) - except Queue.Empty: + while not self.library.notify_output.empty(): + item = self.library.notify_output.get() + time = self.music_time if item['Type'] == 'Audio' else self.video_time + + if time and (not self.library.Player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): + self.library.EmbyServer.Utils.dialog("notification", heading="%s %s" % (self.library.EmbyServer.Utils.Translate(33049), item['Type']), message=item['Name'], icon="{emby}", time=time, sound=False) + + if self.library.stop_thread: break - time = self.music_time if item[0] == 'Audio' else self.video_time + self.LOG.info("--<[ q:notify/%s ]" % id(self)) + self.is_done = True - if time and (not xbmc.Player().isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): - self.library.Utils.dialog("notification", heading="%s %s" % (helper.translate._(33049), item[0]), message=item[1], icon="{emby}", time=time, sound=False) +class GetItemWorker(threading.Thread): + def __init__(self, library, queue, output): + self.library = library + self.queue = queue + self.output = output + self.removed = [] + self.is_done = False + self.LOG = helper.loghandler.LOG('EMBY.downloader.GetItemWorker') + self.info = ( + "Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount,Video3DFormat," + "OfficialRating,CumulativeRunTimeTicks,ItemCounts,PremiereDate,ProductionYear," + "Metascore,AirTime,DateCreated,People,Overview,CommunityRating,StartDate," + "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,Status,EndDate," + "MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio,DisplayOrder," + "PresentationUniqueKey,OriginalTitle,MediaSources,AlternateMediaSources,PartCount" + ) + threading.Thread.__init__(self) + self.start() - self.library.notify_output.task_done() + def Done(self): + return self.is_done - if self.library.Utils.window('emby_should_stop.bool'): - break + def run(self): + count = 0 + + while not self.queue.empty(): + item_ids = self.queue.get() + clean_list = [str(x) for x in item_ids] + request = { + 'type': "GET", + 'handler': "Users/%s/Items" % self.library.EmbyServer.Data['auth.user_id'], + 'params': { + 'Ids': ','.join(clean_list), + 'Fields': self.info + } + } + result = self.library.EmbyServer.http.request(request) + self.removed.extend(list(set(clean_list) - set([str(x['Id']) for x in result['Items']]))) + + for item in result['Items']: + if item['Type'] in self.output: + self.output[item['Type']].put(item) + + count += len(clean_list) - self.LOG.info("--<[ q:notify/%s ]", id(self)) self.is_done = True diff --git a/database/queries.py b/database/queries.py index 074761ecb..4bad7a299 100644 --- a/database/queries.py +++ b/database/queries.py @@ -21,7 +21,7 @@ get_view_name = """SELECT view_name FROM view WHERE view_id = ?""" get_media_by_id = """SELECT emby_type FROM emby WHERE emby_id = ?""" get_media_by_parent_id = """SELECT emby_id, emby_type, kodi_id, kodi_fileid FROM emby WHERE emby_parent_id = ?""" -get_view = """SELECT view_name, media_type FROM view WHERE view_id = ?""" +get_view = """SELECT * FROM view WHERE view_id = ?""" get_views = """SELECT * FROM view""" get_views_by_media = """SELECT * FROM view WHERE media_type = ?""" get_items_by_media = """SELECT emby_id, checksum FROM emby WHERE media_type = ?""" @@ -35,7 +35,7 @@ get_videostreams = """SELECT * FROM VideoStreams WHERE emby_id = ? AND MediaIndex = ?""" get_mediasourceid = """SELECT Id FROM MediaSources WHERE emby_id = ?""" get_mediasource = """SELECT * FROM MediaSources WHERE emby_id = ?""" -get_kodiid = """SELECT kodi_id, presentation_key FROM emby WHERE emby_id = ?""" +get_kodiid = """SELECT kodi_id, presentation_key, kodi_fileid FROM emby WHERE emby_id = ?""" get_AudioStreams = """SELECT * FROM AudioStreams WHERE emby_id = ? AND MediaIndex = ?""" get_Subtitles = """SELECT * FROM Subtitle WHERE emby_id = ? AND MediaIndex = ?""" get_kodifileid = """SELECT kodi_fileid FROM emby WHERE emby_id = ?""" @@ -47,14 +47,14 @@ add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None, "{PresentationKey}"] add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] -add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, "{LibraryId}", None, "{PresentationKey}"] +add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None, "{PresentationKey}"] add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{EmbyParentId}", "{PresentationKey}"] -add_view = """INSERT OR REPLACE INTO view(view_id, view_name, media_type) VALUES (?, ?, ?)""" +add_view = """INSERT OR REPLACE INTO view(view_id, view_name, media_type, server_id) VALUES (?, ?, ?, ?)""" add_version = """INSERT OR REPLACE INTO version(idVersion) VALUES (?)""" update_reference = """ UPDATE emby SET checksum = ?, presentation_key = ? WHERE emby_id = ?""" update_reference_obj = ["{Checksum}", "{PresentationKey}", "{Id}"] diff --git a/database/sync.py b/database/sync.py index 27056d3c3..54ad34598 100644 --- a/database/sync.py +++ b/database/sync.py @@ -1,229 +1,194 @@ # -*- coding: utf-8 -*- +import _strptime # Workaround for threads using datetime: _striptime is locked import datetime -import logging + import xbmc +import xbmcgui -import helper.translate -import helper.wrapper -import helper.exceptions -import helper.xmls +import core.movies +import core.musicvideos +import core.tvshows +import core.music +import helper.loghandler from . import database from . import emby_db class Sync(): - running = False - - def __init__(self, library, server, Downloader, Utils): - self.LOG = logging.getLogger("EMBY.sync") - self.sync = None -# self.running = False + def __init__(self, EmbyServer, Player, ThreadingLock): + self.LOG = helper.loghandler.LOG('EMBY.database.sync') + self.EmbyServer = EmbyServer + self.Player = Player + self.ThreadingLock = ThreadingLock + self.running = False self.screensaver = None self.update_library = False - self.Downloader = Downloader - self.Utils = Utils - self.xmls = helper.xmls.Xmls(self.Utils) - self.direct_path = self.Utils.settings('useDirectPaths') == "1" if self.running: - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33197)) - raise Exception("Sync is already running.") - - self.library = library - self.server = server - - #Do everything we need before the sync - def __enter__(self): - self.LOG.info("-->[ fullsync ]") - - if not self.Utils.settings('dbSyncScreensaver.bool'): - xbmc.executebuiltin('InhibitIdleShutdown(true)') - self.screensaver = self.Utils.get_screensaver() - self.Utils.set_screensaver(value="") - - self.running = True - self.Utils.window('emby_sync.bool', True) - return self + self.EmbyServer.Utils.dialog("ok", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33197)) + return #Assign the restore point and save the sync status def _restore_point(self, restore): - self.sync['RestorePoint'] = restore - database.save_sync(self.sync) - - #Map the syncing process and start the sync. Ensure only one sync is running - #force to resume any previous sync - def libraries(self, library_id=None, update=False, forced=False): - self.update_library = update - self.sync = database.get_sync() - - if library_id: - libraries = library_id.split(',') - - for selected in libraries: - if selected not in [x.replace('Mixed:', "") for x in self.sync['Libraries']]: - library = self.get_libraries(selected) - - if library: - self.sync['Libraries'].append("Mixed:%s" % selected if library[1] == 'mixed' else selected) - - if library[1] in ('mixed', 'movies'): - self.sync['Libraries'].append('Boxsets:%s' % selected) - else: - self.sync['Libraries'].append(selected) - else: - self.mapping(forced) - - self.xmls.sources() - - if not self.xmls.advanced_settings() and self.sync['Libraries']: - self.start() - - def get_libraries(self, library_id=None): - with database.Database('emby') as embydb: - if library_id is None: - return emby_db.EmbyDatabase(embydb.cursor).get_views() - - return emby_db.EmbyDatabase(embydb.cursor).get_view(library_id) + self.EmbyServer.Utils.SyncData['RestorePoint'] = restore + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, False) #Load the mapping of the full sync. #This allows us to restore a previous sync - def mapping(self, forced=False): - if self.sync['Libraries']: - if not forced and not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33102)): - if not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33173)): - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33122)) - self.Utils.window('emby_sync_skip_resume.bool', True) - raise helper.exceptions.LibraryException("StopWriteCalled") - - self.sync['Libraries'] = [] - self.sync['RestorePoint'] = {} + def mapping(self, forced): + if self.EmbyServer.Utils.SyncData['Libraries']: + if not forced and not self.EmbyServer.Utils.dialog("yesno", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33102)): + if not self.EmbyServer.Utils.dialog("yesno", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33173)): + self.EmbyServer.Utils.dialog("ok", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33122)) + self.Player.SyncPause = True + return False + + self.EmbyServer.Utils.SyncData['Libraries'] = [] + self.EmbyServer.Utils.SyncData['RestorePoint'] = {} else: self.LOG.info("generate full sync") libraries = [] - for library in self.get_libraries(): + with database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: + libraries_DB = emby_db.EmbyDatabase(embydb.cursor).get_views() + + for library in libraries_DB: + if library[2] in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'): libraries.append({'Id': library[0], 'Name': library[1], 'Media': library[2]}) - libraries = self.select_libraries(libraries) + if self.EmbyServer.Utils.dialog("yesno", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33125), nolabel=self.EmbyServer.Utils.Translate(33127), yeslabel=self.EmbyServer.Utils.Translate(33126)): + self.LOG.info("Selected sync later") + return False - if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]: - self.sync['Libraries'].append("Boxsets:") - - database.save_sync(self.sync) + choices = [x['Name'] for x in libraries] + choices.insert(0, self.EmbyServer.Utils.Translate(33121)) + selection = self.EmbyServer.Utils.dialog("multi", self.EmbyServer.Utils.Translate(33120), choices) - #Select all or certain libraries to be whitelisted - def select_libraries(self, libraries): - if self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33125), nolabel=helper.translate._(33127), yeslabel=helper.translate._(33126)): - self.LOG.info("Selected sync later.") - raise helper.exceptions.LibraryException('SyncLibraryLater') + if selection is None: + return False - choices = [x['Name'] for x in libraries] - choices.insert(0, helper.translate._(33121)) - selection = self.Utils.dialog("multi", helper.translate._(33120), choices) + if not selection: + self.LOG.info("Nothing was selected") + return False - if selection is None: - raise helper.exceptions.LibraryException('LibrarySelection') + if 0 in selection: + selection = list(range(1, len(libraries) + 1)) - if not selection: - self.LOG.info("Nothing was selected.") - raise helper.exceptions.LibraryException('SyncLibraryLater') + selected_libraries = [] - if 0 in selection: - selection = list(range(1, len(libraries) + 1)) + for x in selection: + library = libraries[x - 1] - selected_libraries = [] + if library['Media'] != 'mixed': + selected_libraries.append(library['Id']) + else: + selected_libraries.append("Mixed:%s" % library['Id']) - for x in selection: - library = libraries[x - 1] + self.EmbyServer.Utils.SyncData['Libraries'] = selected_libraries + libraries = [libraries[x - 1] for x in selection] - if library['Media'] != 'mixed': - selected_libraries.append(library['Id']) - else: - selected_libraries.append("Mixed:%s" % library['Id']) + if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]: + self.EmbyServer.Utils.SyncData['Libraries'].append("Boxsets:") - self.sync['Libraries'] = selected_libraries - return [libraries[x - 1] for x in selection] + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, True) + return True #Main sync process - def start(self): - self.LOG.info("starting sync with %s", self.sync['Libraries']) - database.save_sync(self.sync) + def FullSync(self): + self.LOG.info("-->[ starting sync with %s ]" % self.EmbyServer.Utils.SyncData['Libraries']) + self.EmbyServer.Utils.Settings.set_settings_bool('ReloadSkin', True) + + if not self.EmbyServer.Utils.Settings.dbSyncScreensaver: + xbmc.executebuiltin('InhibitIdleShutdown(true)') + self.screensaver = self.EmbyServer.Utils.Screensaver + self.EmbyServer.Utils.set_screensaver(value="") + + self.running = True + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, True) start_time = datetime.datetime.now() - for library in list(self.sync['Libraries']): - self.process_library(library) + for library in list(self.EmbyServer.Utils.SyncData['Libraries']): + if not self.process_library(library): + self.running = False + return - if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']: - self.sync['Whitelist'].append(library) + if not library.startswith('Boxsets:') and library not in self.EmbyServer.Utils.SyncData['Whitelist']: + self.EmbyServer.Utils.SyncData['Whitelist'].append(library) - self.sync['Libraries'].pop(self.sync['Libraries'].index(library)) + self.EmbyServer.Utils.SyncData['Libraries'].pop(self.EmbyServer.Utils.SyncData['Libraries'].index(library)) self._restore_point({}) elapsed = datetime.datetime.now() - start_time - self.Utils.settings('SyncInstallRunDone.bool', True) - self.library.save_last_sync() - database.save_sync(self.sync) + self.EmbyServer.Utils.Settings.set_settings_bool('SyncInstallRunDone', True) + self.EmbyServer.Utils.save_last_sync() + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, True) xbmc.executebuiltin('UpdateLibrary(video)') - self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33025), str(elapsed).split('.')[0]), icon="{emby}", sound=False) - self.LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0]) + self.EmbyServer.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.EmbyServer.Utils.Translate(33025), str(elapsed).split('.')[0]), icon="{emby}", sound=False) + self.running = False + + if self.screensaver is not None: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + self.EmbyServer.Utils.set_screensaver(value=self.screensaver) + + xbmc.sleep(1000) + xbmc.executebuiltin('ReloadSkin()') + self.EmbyServer.Utils.Settings.set_settings_bool('ReloadSkin', False) + self.LOG.info("--<[ Full sync completed in: %s ]" % str(elapsed).split('.')[0]) - #Add a library by it's id. Create a node and a playlist whenever appropriate def process_library(self, library_id): - media = { - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows, - 'music': self.music - } - - try: - if library_id.startswith('Boxsets:'): - if library_id.endswith('Refresh'): - self.refresh_boxsets() - else: - self.boxsets(library_id.split('Boxsets:')[1] if len(library_id) > len('Boxsets:') else None) + if library_id.startswith('Boxsets:'): + if library_id.endswith('Refresh'): + self.refresh_boxsets() + else: + self.boxsets(library_id.split('Boxsets:')[1] if len(library_id) > len('Boxsets:') else None) - return + return True - library = self.server['api'].get_item(library_id.replace('Mixed:', "")) + library = self.EmbyServer.API.get_item(library_id.replace('Mixed:', "")) - if library_id.startswith('Mixed:'): - for mixed in ('movies', 'tvshows'): - media[mixed](library, self.Downloader) - self.sync['RestorePoint'] = {} - else: - if library['CollectionType']: - self.Utils.settings('enableMusic.bool', True) - - media[library['CollectionType']](library, self.Downloader) - except helper.exceptions.LibraryException as error: - if error.status in ('StopCalled', 'StopWriteCalled'): - database.save_sync(self.sync) - raise - except Exception as error: - if 'Failed to validate path' not in error.args: - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33119)) - self.LOG.error("full sync exited unexpectedly") - database.save_sync(self.sync) - - raise + if library_id.startswith('Mixed:'): + self.movies(library) + self.EmbyServer.Utils.SyncData['RestorePoint'] = {} + self.tvshows(library) + self.EmbyServer.Utils.SyncData['RestorePoint'] = {} + else: + if library['CollectionType'] == 'movies': + self.movies(library) + elif library['CollectionType'] == 'musicvideos': + self.musicvideos(library) + elif library['CollectionType'] == 'tvshows': + self.tvshows(library) + elif library['CollectionType'] == 'music': + self.music(library) + + if self.Player.SyncPause: + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, True) + return False + + return True #Process movies from a single library - @helper.wrapper.progress() - def movies(self, library, Downloader, dialog): - with self.library.database_lock: - with database.Database() as videodb: - with database.Database('emby') as embydb: - MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) - TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "Movie") - - for items in Downloader.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): + def movies(self, library): + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name'), "%s %s" % (self.EmbyServer.Utils.Translate('gathering'), "Movies")) + + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'video', True) as videodb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + MoviesObject = core.movies.Movies(self.EmbyServer, embydb, videodb) + TotalRecords = self.EmbyServer.API.get_TotalRecordsRegular(library['Id'], "Movie") + + for items in self.EmbyServer.API.get_itemsSync(library['Id'], "Movie", False, self.EmbyServer.Utils.SyncData['RestorePoint'].get('params')): self._restore_point(items['RestorePoint']) start_index = items['RestorePoint']['params']['StartIndex'] for index, movie in enumerate(items['Items']): - dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=movie['Name']) - MoviesObject.movie(movie, library=library) + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library['Name']), message=movie['Name']) + MoviesObject.movie(movie, library) + + if self.Player.SyncPause: + dialog.close() + return #Compare entries from library to what's in the embydb. Remove surplus if self.update_library: @@ -234,28 +199,35 @@ def movies(self, library, Downloader, dialog): if x[0] not in current and x[1] == 'Movie': MoviesObject.remove(x[0]) - #Process tvshows and episodes from a single library - @helper.wrapper.progress() - def tvshows(self, library, Downloader, dialog): - with self.library.database_lock: - with database.Database() as videodb: - with database.Database('emby') as embydb: - TVShowsObject = self.library.MEDIA['TVShow'](self.server, embydb, videodb, self.direct_path, self.Utils, True) - TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "Series") + dialog.close() - for items in Downloader.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): + def tvshows(self, library): + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name'), "%s %s" % (self.EmbyServer.Utils.Translate('gathering'), "TV Shows")) + + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'video', True) as videodb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + TVShowsObject = core.tvshows.TVShows(self.EmbyServer, embydb, videodb, True) + TotalRecords = self.EmbyServer.API.get_TotalRecordsRegular(library['Id'], "Series") + + for items in self.EmbyServer.API.get_itemsSync(library['Id'], "Series", False, self.EmbyServer.Utils.SyncData['RestorePoint'].get('params')): self._restore_point(items['RestorePoint']) start_index = items['RestorePoint']['params']['StartIndex'] for index, show in enumerate(items['Items']): percent = int((float(start_index + index) / TotalRecords)*100) - dialog.update(percent, heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=show['Name']) + dialog.update(percent, heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library['Name']), message=show['Name']) - if TVShowsObject.tvshow(show, library=library): - for episodes in Downloader.get_episode_by_show(show['Id']): + if TVShowsObject.tvshow(show, library, None, None): + for episodes in self.EmbyServer.API.get_episode_by_show(show['Id']): for episode in episodes['Items']: dialog.update(percent, message="%s/%s" % (show['Name'], episode['Name'][:10])) - TVShowsObject.episode(episode, library=library) + TVShowsObject.episode(episode, library) + + if self.Player.SyncPause: + dialog.close() + return #Compare entries from library to what's in the embydb. Remove surplus if self.update_library: @@ -270,22 +242,29 @@ def tvshows(self, library, Downloader, dialog): if x[0] not in current and x[1] == 'Series': TVShowsObject.remove(x[0]) - #Process musicvideos from a single library - @helper.wrapper.progress() - def musicvideos(self, library, Downloader, dialog): - with self.library.database_lock: - with database.Database() as videodb: - with database.Database('emby') as embydb: - MusicVideosObject = self.library.MEDIA['MusicVideo'](self.server, embydb, videodb, self.direct_path, self.Utils) - TotalRecords = Downloader.get_TotalRecordsRegular(library['Id'], "MusicVideo") + dialog.close() + + def musicvideos(self, library): + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name'), "%s %s" % (self.EmbyServer.Utils.Translate('gathering'), "Musicvideos")) - for items in Downloader.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')): + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'video', True) as videodb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + MusicVideosObject = core.musicvideos.MusicVideos(self.EmbyServer, embydb, videodb) + TotalRecords = self.EmbyServer.API.get_TotalRecordsRegular(library['Id'], "MusicVideo") + + for items in self.EmbyServer.API.get_itemsSync(library['Id'], "MusicVideo", False, self.EmbyServer.Utils.SyncData['RestorePoint'].get('params')): self._restore_point(items['RestorePoint']) start_index = items['RestorePoint']['params']['StartIndex'] for index, mvideo in enumerate(items['Items']): - dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=mvideo['Name']) - MusicVideosObject.musicvideo(mvideo, library=library) + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library['Name']), message=mvideo['Name']) + MusicVideosObject.musicvideo(mvideo, library) + + if self.Player.SyncPause: + dialog.close() + return #Compare entries from library to what's in the embydb. Remove surplus if self.update_library: @@ -296,33 +275,43 @@ def musicvideos(self, library, Downloader, dialog): if x[0] not in current and x[1] == 'MusicVideo': MusicVideosObject.remove(x[0]) - #Process artists, album, songs from a single library - @helper.wrapper.progress() - def music(self, library, Downloader, dialog): - self.patch_music() + dialog.close() - with self.library.music_database_lock: - with database.Database('music') as musicdb: - with database.Database('emby') as embydb: - MusicObject = self.library.MEDIA['Music'](self.server, embydb, musicdb, self.direct_path, self.Utils) - TotalRecords = Downloader.get_TotalRecordsArtists(library['Id']) + def music(self, library): + self.patch_music(False) + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name'), "%s %s" % (self.EmbyServer.Utils.Translate('gathering'), "Music")) - for items in Downloader.get_artists(library['Id'], False, self.sync['RestorePoint'].get('params')): + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'music', True) as musicdb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + MusicObject = core.music.Music(self.EmbyServer, embydb, musicdb) + TotalRecords = self.EmbyServer.API.get_TotalRecordsArtists(library['Id']) + + for items in self.EmbyServer.API.get_artists(library['Id'], False, self.EmbyServer.Utils.SyncData['RestorePoint'].get('params')): self._restore_point(items['RestorePoint']) start_index = items['RestorePoint']['params']['StartIndex'] for index, artist in enumerate(items['Items']): percent = int((float(start_index + index) / TotalRecords) * 100) - dialog.update(percent, heading="%s: %s" % (helper.translate._('addon_name'), library['Name']), message=artist['Name']) - MusicObject.artist(artist, library=library) + dialog.update(percent, heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library['Name']), message=artist['Name']) + MusicObject.artist(artist, library) - for albums in Downloader.get_albums_by_artist(library['Id'], artist['Id']): + for albums in self.EmbyServer.API.get_albums_by_artist(library['Id'], artist['Id'], False): for album in albums['Items']: - MusicObject.album(album, library=library) + MusicObject.album(album, library) + + if self.Player.SyncPause: + dialog.close() + return - for songs in Downloader.get_songs_by_artist(library['Id'], artist['Id']): + for songs in self.EmbyServer.API.get_songs_by_artist(library['Id'], artist['Id'], False): for song in songs['Items']: - MusicObject.song(song, library=library) + MusicObject.song(song, library) + + if self.Player.SyncPause: + dialog.close() + return #Compare entries from library to what's in the embydb. Remove surplus if self.update_library: @@ -337,101 +326,102 @@ def music(self, library, Downloader, dialog): if x[0] not in current and x[1] == 'MusicArtist': MusicObject.remove(x[0]) - #Process all boxsets - @helper.wrapper.progress(helper.translate._(33018)) - def boxsets(self, library_id=None, dialog=None): - with self.library.database_lock: - with database.Database() as videodb: - with database.Database('emby') as embydb: - MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) - TotalRecords = self.Downloader.get_TotalRecordsRegular(library_id, "BoxSet") + dialog.close() + + def boxsets(self, library_id): + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name'), "%s %s" % (self.EmbyServer.Utils.Translate('gathering'), "Boxsets")) - for items in self.Downloader.get_items(library_id, "BoxSet", False, self.sync['RestorePoint'].get('params')): + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'video', True) as videodb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + MoviesObject = core.movies.Movies(self.EmbyServer, embydb, videodb) + TotalRecords = self.EmbyServer.API.get_TotalRecordsRegular(library_id, "BoxSet") + + for items in self.EmbyServer.API.get_itemsSync(library_id, "BoxSet", False, self.EmbyServer.Utils.SyncData['RestorePoint'].get('params')): self._restore_point(items['RestorePoint']) start_index = items['RestorePoint']['params']['StartIndex'] for index, boxset in enumerate(items['Items']): - dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (helper.translate._('addon_name'), helper.translate._('boxsets')), message=boxset['Name']) + dialog.update(int((float(start_index + index) / TotalRecords) * 100), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), self.EmbyServer.Utils.Translate('boxsets')), message=boxset['Name']) MoviesObject.boxset(boxset) + if self.Player.SyncPause: + dialog.close() + return + + dialog.close() + #Delete all exisitng boxsets and re-add def refresh_boxsets(self): - with self.library.database_lock: - with database.Database() as videodb: - with database.Database('emby') as embydb: - MoviesObject = self.library.MEDIA['Movie'](self.server, embydb, videodb, self.direct_path, self.Utils) + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'video', True) as videodb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + MoviesObject = core.movies.Movies(self.EmbyServer, embydb, videodb) MoviesObject.boxsets_reset() self.boxsets(None) - #Patch the music database to silence the rescan prompt - def patch_music(self, notification=False): - with self.library.database_lock: - with database.Database('music') as musicdb: - self.library.MEDIA['MusicDisableScan'](musicdb.cursor, int(self.Utils.window('kodidbverion.music'))).disable_rescan() - - self.Utils.settings('MusicRescan.bool', True) + def patch_music(self, notification): + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, 'music', True) as musicdb: + core.music.MusicDBIO(musicdb.cursor, self.EmbyServer.Utils.DatabaseFiles['music-version']).disable_rescan() if notification: - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._('task_success'), icon="{emby}", time=1000, sound=False) + self.EmbyServer.Utils.dialog("notification", heading="{emby}", message=self.EmbyServer.Utils.Translate('task_success'), icon="{emby}", time=1000, sound=False) #Remove library by their id from the Kodi database - @helper.wrapper.progress(helper.translate._(33144)) - def remove_library(self, library_id, dialog=None): - direct_path = self.library.direct_path + def remove_library(self, library_id): + dialog = xbmcgui.DialogProgressBG() + dialog.create(self.EmbyServer.Utils.Translate('addon_name')) - with database.Database('emby') as embydb: + with database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: db = emby_db.EmbyDatabase(embydb.cursor) library = db.get_view(library_id.replace('Mixed:', "")) items = db.get_item_by_media_folder(library_id.replace('Mixed:', "")) - media = 'music' if library[1] == 'music' else 'video' + media = 'music' if library[2] == 'music' else 'video' if items: count = 0 - with self.library.music_database_lock if media == 'music' else self.library.database_lock: - with database.Database(media) as kodidb: - if library[1] == 'mixed': + with self.ThreadingLock: + with database.Database(self.EmbyServer.Utils, media, True) as kodidb: + if library[2] == 'mixed': movies = [x for x in items if x[1] == 'Movie'] tvshows = [x for x in items if x[1] == 'Series'] - MediaObject = self.library.MEDIA['Movie'](self.server, embydb, kodidb, direct_path, self.Utils).remove + MediaObject = core.movies.Movies(self.EmbyServer, embydb, kodidb).remove for item in movies: MediaObject(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library[1])) count += 1 - MediaObject = self.library.MEDIA['Series'](self.server, embydb, kodidb, direct_path, self.Utils).remove + MediaObject = core.tvshows.TVShows(self.EmbyServer, embydb, kodidb).remove for item in tvshows: MediaObject(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library[1])) count += 1 else: - MediaObject = self.library.MEDIA[items[0][1]](self.server, embydb, kodidb, direct_path, self.Utils).remove + if items[0][1] in ('Movie', 'BoxSet'): + MediaObject = core.movies.Movies(self.EmbyServer, embydb, kodidb).remove + if items[0][1] == 'MusicVideo': + MediaObject = core.musicvideos.MusicVideos(self.EmbyServer, embydb, kodidb).remove + if items[0][1] in ('TVShow', 'Series', 'Season', 'Episode'): + MediaObject = core.tvshows.TVShows(self.EmbyServer, embydb, kodidb).remove + if items[0][1] in ('Music', 'MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + MediaObject = core.music.Music(self.EmbyServer, embydb, kodidb).remove for item in items: MediaObject(item[0]) - dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (helper.translate._('addon_name'), library[0])) + dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (self.EmbyServer.Utils.Translate('addon_name'), library[1])) count += 1 - self.sync = database.get_sync() - - if library_id in self.sync['Whitelist']: - self.sync['Whitelist'].remove(library_id) - elif 'Mixed:%s' % library_id in self.sync['Whitelist']: - self.sync['Whitelist'].remove('Mixed:%s' % library_id) - - database.save_sync(self.sync) + dialog.close() - #Exiting sync - def __exit__(self, exc_type, exc_val, exc_tb): - self.running = False - self.Utils.window('emby_sync', clear=True) - - if self.screensaver is not None: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - self.Utils.set_screensaver(value=self.screensaver) - self.screensaver = None + if library_id in self.EmbyServer.Utils.SyncData['Whitelist']: + self.EmbyServer.Utils.SyncData['Whitelist'].remove(library_id) + elif 'Mixed:%s' % library_id in self.EmbyServer.Utils.SyncData['Whitelist']: + self.EmbyServer.Utils.SyncData['Whitelist'].remove('Mixed:%s' % library_id) - self.LOG.info("--<[ fullsync ]") + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData, True) diff --git a/dialogs/context.py b/dialogs/context.py index 9618b2806..f755007f1 100644 --- a/dialogs/context.py +++ b/dialogs/context.py @@ -1,20 +1,22 @@ -import logging +# -*- coding: utf-8 -*- import os import xbmcgui import xbmcaddon + import helper.utils +import helper.loghandler class ContextMenu(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): self._options = [] self.selected_option = None self.list_ = None - self.LOG = logging.getLogger("EMBY.dialogs.context.ContextMenu") + self.LOG = helper.loghandler.LOG('EMBY.dialogs.context.ContextMenu') self.Utils = helper.utils.Utils() xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_options(self, options=[]): + def set_options(self, options): self._options = options def is_selected(self): @@ -24,10 +26,10 @@ def get_selected(self): return self.selected_option def onInit(self): - if self.Utils.window('EmbyUserImage'): - self.getControl(150).setImage(self.Utils.window('EmbyUserImage')) + if self.Utils.Settings.emby_UserImage: + self.getControl(150).setImage(self.Utils.Settings.emby_UserImage) - self.LOG.info("options: %s", self._options) + self.LOG.info("options: %s" % self._options) self.list_ = self.getControl(155) for option in self._options: @@ -43,11 +45,11 @@ def onAction(self, action): if self.getFocusId() == 155: option = self.list_.getSelectedItem() self.selected_option = option.getLabel() - self.LOG.info('option selected: %s', self.selected_option) + self.LOG.info('option selected: %s' % self.selected_option) self.close() def _add_editcontrol(self, x, y, height, width): - media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + media = os.path.join(xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlImage(0, 0, 0, 0, filename=os.path.join(media, "white.png"), aspectRatio=0, colorDiffuse="ff111111") control.setPosition(x, y) control.setHeight(height) @@ -55,6 +57,5 @@ def _add_editcontrol(self, x, y, height, width): self.addControl(control) return control - @classmethod - def _add_listitem(cls, label): + def _add_listitem(self, label): return xbmcgui.ListItem(label) diff --git a/dialogs/loginconnect.py b/dialogs/loginconnect.py index 59d44eb39..1a1b87be9 100644 --- a/dialogs/loginconnect.py +++ b/dialogs/loginconnect.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- -import logging +import os + import xbmcgui +import xbmcaddon + import helper.utils -import helper.translate +import helper.loghandler ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -20,7 +23,7 @@ class LoginConnect(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): self._user = None self.error = None - self.LOG = logging.getLogger("EMBY.dialogs.loginconnect.LoginConnect") + self.LOG = helper.loghandler.LOG('EMBY.dialogs.loginconnect.LoginConnect') self.Utils = helper.utils.Utils() self.user_field = None self.password_field = None @@ -65,7 +68,7 @@ def onClick(self, control): if not user or not password: # Display error - self._error(ERROR['Empty'], helper.translate._('empty_user_pass')) + self._error(ERROR['Empty'], self.Utils.Translate('empty_user_pass')) self.LOG.error("Username or password cannot be null") elif self._login(user, password): self.close() @@ -82,7 +85,7 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width, password=0): -# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + os.path.join(xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('path'), 'resources', 'skins', 'default', 'media') #####control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-", isPassword=password) control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") control.setPosition(x, y) @@ -92,17 +95,17 @@ def _add_editcontrol(self, x, y, height, width, password=0): return control def _login(self, username, password): - result = self.connect_manager['login-connect'](username, password) + result = self.connect_manager.login_to_connect(username, password) if result is False: - self._error(ERROR['Invalid'], helper.translate._('invalid_auth')) + self._error(ERROR['Invalid'], self.Utils.Translate('invalid_auth')) return False self._user = result username = result['User']['Name'] - self.Utils.settings('connectUsername', value=username) - self.Utils.settings('idMethod', value="1") - self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33000), self.Utils.StringMod(username)), icon=result['User'].get('ImageUrl') or "{emby}", time=2000, sound=False) + self.Utils.Settings.set_settings('connectUsername', username) + self.Utils.Settings.set_settings('idMethod', "1") + self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.Utils.Translate(33000), self.Utils.StringMod(username)), icon=result['User'].get('ImageUrl') or "{emby}", time=2000, sound=False) return True def _error(self, state, message): diff --git a/dialogs/loginmanual.py b/dialogs/loginmanual.py index 107daeab3..adb6ed335 100644 --- a/dialogs/loginmanual.py +++ b/dialogs/loginmanual.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- -import logging +import os + import xbmcgui +import xbmcaddon + import helper.utils -import helper.translate +import helper.loghandler ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -18,7 +21,7 @@ def __init__(self, *args, **kwargs): self._user = None self.error = None self.username = None - self.LOG = logging.getLogger("EMBY.dialogs.loginmanual.LoginManual") + self.LOG = helper.loghandler.LOG('EMBY.dialogs.loginmanual.LoginManual') self.Utils = helper.utils.Utils() self.user_field = None self.password_field = None @@ -69,7 +72,7 @@ def onClick(self, control): if not user: # Display error - self._error(ERROR['Empty'], helper.translate._('empty_user')) + self._error(ERROR['Empty'], self.Utils.Translate('empty_user')) self.LOG.error("Username cannot be null") elif self._login(user, password): self.close() @@ -85,7 +88,7 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width, password=0): -# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + os.path.join(xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('path'), 'resources', 'skins', 'default', 'media') #### control = xbmcgui.ControlEdit(0, 0, 0, 0, label="User", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-", isPassword=password) control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") # control.setInputType(xbmcgui.INPUT_TYPE_PASSWORD) @@ -96,11 +99,11 @@ def _add_editcontrol(self, x, y, height, width, password=0): return control def _login(self, username, password): - server = self.connect_manager['server-address'] - result = self.connect_manager['login'](server, username, password) + server = self.connect_manager.get_serveraddress() + result = self.connect_manager.login(server, username, password, True, {}) if not result: - self._error(ERROR['Invalid'], helper.translate._('invalid_auth')) + self._error(ERROR['Invalid'], self.Utils.Translate('invalid_auth')) return False self._user = result diff --git a/dialogs/serverconnect.py b/dialogs/serverconnect.py index 4ccad7bf9..65030dbcb 100644 --- a/dialogs/serverconnect.py +++ b/dialogs/serverconnect.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -import logging import xbmc import xbmcgui -import emby.core.connection_manager -import helper.translate + +import helper.utils +import helper.loghandler ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -23,6 +23,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): self.user_image = None self.servers = [] + self.Utils = helper.utils.Utils() self._selected_server = None self._connect_login = False self._manual_server = False @@ -30,7 +31,7 @@ def __init__(self, *args, **kwargs): self.message_box = None self.busy = None self.list_ = None - self.LOG = logging.getLogger("EMBY.dialogs.serverconnect.ServerConnect") + self.LOG = helper.loghandler.LOG('EMBY.dialogs.serverconnect.ServerConnect') xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) #connect_manager, user_image, servers, emby_connect @@ -64,13 +65,12 @@ def onInit(self): self.getControl(USER_IMAGE).setImage(self.user_image) if not self.emby_connect: # Change connect user - self.getControl(EMBY_CONNECT).setLabel("[B]%s[/B]" % helper.translate._(30618)) + self.getControl(EMBY_CONNECT).setLabel("[B]%s[/B]" % self.Utils.Translate(30618)) if self.servers: self.setFocus(self.list_) - @classmethod - def _add_listitem(cls, label, server_id, server_type): + def _add_listitem(self, label, server_id, server_type): item = xbmcgui.ListItem(label) item.setProperty('id', server_id) item.setProperty('server_type', server_type) @@ -83,10 +83,9 @@ def onAction(self, action): if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): if self.getFocusId() == LIST: server = self.list_.getSelectedItem() - selected_id = server.getProperty('id') - self.LOG.info('Server Id selected: %s', selected_id) + self.LOG.info('Server Id selected: %s' % server.getProperty('id')) - if self._connect_server(selected_id): + if self._connect_server(): self.message_box.setVisibleCondition('false') self.close() @@ -101,16 +100,16 @@ def onClick(self, control): elif control == CANCEL: self.close() - def _connect_server(self, server_id): - server = self.connect_manager.get_server_info(server_id) - self.message.setLabel("%s %s..." % (helper.translate._(30610), server['Name'])) + def _connect_server(self): + server = self.connect_manager.get_server_info() + self.message.setLabel("%s %s..." % (self.Utils.Translate(30610), server['Name'])) self.message_box.setVisibleCondition('true') self.busy.setVisibleCondition('true') - result = self.connect_manager['connect-to-server'](server) + result = self.connect_manager.connect_to_server(server, {}) - if result['State'] == emby.core.connection_manager.CONNECTION_STATE['Unavailable']: + if result['State'] == 0: #Unavailable self.busy.setVisibleCondition('false') - self.message.setLabel(helper.translate._(30609)) + self.message.setLabel(self.Utils.Translate(30609)) return False xbmc.sleep(1000) diff --git a/dialogs/servermanual.py b/dialogs/servermanual.py index 57a453d5c..3b5e01daf 100644 --- a/dialogs/servermanual.py +++ b/dialogs/servermanual.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- -import logging +import os + import xbmcgui -import emby.core.connection_manager +import xbmcaddon + import helper.utils -import helper.translate +import helper.loghandler ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -21,14 +23,14 @@ class ServerManual(xbmcgui.WindowXMLDialog): def __init__(self, *args, **kwargs): self._server = None self.error = None + self.Utils = helper.utils.Utils() self.connect_button = None self.cancel_button = None self.error_toggle = None self.error_msg = None self.host_field = None self.port_field = None - self.LOG = logging.getLogger("EMBY.dialogs.servermanual.ServerManual") - self.Utils = helper.utils.Utils() + self.LOG = helper.loghandler.LOG('EMBY.dialogs.servermanual.ServerManual') xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) #connect_manager, user_image, servers, emby_connect @@ -67,7 +69,7 @@ def onClick(self, control): if not server: # Display error - self._error(ERROR['Empty'], helper.translate._('empty_server')) + self._error(ERROR['Empty'], self.Utils.Translate('empty_server')) self.LOG.error("Server cannot be null") elif self._connect_to_server(server, port): @@ -84,7 +86,7 @@ def onAction(self, action): self.close() def _add_editcontrol(self, x, y, height, width): -# media = os.path.join(xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + os.path.join(xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('path'), 'resources', 'skins', 'default', 'media') control = xbmcgui.ControlEdit(0, 0, 0, 0, label="", font="font13", textColor="FF52b54b", disabledColor="FF888888", focusTexture="-", noFocusTexture="-") control.setPosition(x, y) control.setHeight(height) @@ -94,11 +96,11 @@ def _add_editcontrol(self, x, y, height, width): def _connect_to_server(self, server, port): server_address = "%s:%s" % (server, port) if port else server - self._message("%s %s..." % (helper.translate._(30610), server_address)) - result = self.connect_manager['manual-server'](server_address) + self._message("%s %s..." % (self.Utils.Translate(30610), server_address)) + result = self.connect_manager.connect_to_address(server_address, {}) - if result['State'] == emby.core.connection_manager.CONNECTION_STATE['Unavailable']: - self._message(helper.translate._(30609)) + if result['State'] == 0: #Unavailable + self._message(self.Utils.Translate(30609)) return False self._server = result['Servers'][0] diff --git a/dialogs/usersconnect.py b/dialogs/usersconnect.py index 36175ed5f..4169c6a28 100644 --- a/dialogs/usersconnect.py +++ b/dialogs/usersconnect.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -import logging import xbmcgui +import helper.loghandler + ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -16,7 +17,7 @@ def __init__(self, *args, **kwargs): self._user = None self._manual_login = False self.list_ = None - self.LOG = logging.getLogger("EMBY.dialogs.userconnect.UsersConnect") + self.LOG = helper.loghandler.LOG('EMBY.dialogs.userconnect.UsersConnect') xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) #connect_manager, user_image, servers, emby_connect @@ -56,7 +57,7 @@ def onAction(self, action): if self.getFocusId() == LIST: user = self.list_.getSelectedItem() selected_id = user.getProperty('id') - self.LOG.info('User Id selected: %s', selected_id) + self.LOG.info('User Id selected: %s' % selected_id) for user in self.users: if user['Id'] == selected_id: diff --git a/emby/client.py b/emby/client.py deleted file mode 100644 index e71661a4c..000000000 --- a/emby/client.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -from .core import api -from .core import configuration -from .core import http -from .core import ws_client -from .core import connection_manager - -def callback(message, data): - ''' Callback function should received message, data - message: string - data: json dictionary - ''' - return - -class EmbyClient(): - logged_in = False - - def __init__(self): - self.LOG = logging.getLogger('Emby.emby.client') - self.LOG.debug("EmbyClient initializing...") - self.config = configuration.Config() - self.http = http.HTTP(self) - self.wsc = ws_client.WSClient(self) - self.auth = connection_manager.ConnectionManager(self) - self.emby = api.API(self.http) - self.callback_ws = callback - self.callback = callback - - def set_state(self, state): - if not state: - self.LOG.warning("state cannot be empty") - return - - if state.get('config'): - self.config.__setstate__(state['config']) - - if state.get('credentials'): - self.logged_in = True - self.set_credentials(state['credentials']) - self.auth.server_id = state['credentials']['Servers'][0]['Id'] - - def get_state(self): - state = {'config': self.config.__getstate__(), 'credentials': self.get_credentials()} - return state - - def set_credentials(self, credentials=None): - self.auth.credentials.set_credentials(credentials or {}) - - def get_credentials(self): - return self.auth.credentials.get_credentials() - - def authenticate(self, credentials=None, options=None): - self.set_credentials(credentials or {}) - state = self.auth.connect(options or {}) - - if state['State'] == connection_manager.CONNECTION_STATE['SignedIn']: - self.LOG.info("User is authenticated.") - self['callback']('ServerOnline', {'Id': None if self['config/app.default'] else self['auth/server-id']}) - self.logged_in = True - - state['Credentials'] = self.get_credentials() - return state - - def start(self, websocket=False, keep_alive=True): - if not self.logged_in: - raise ValueError("User is not authenticated.") - - self.http.start_session() - - if keep_alive: - self.http.keep_alive = True - - if websocket: - self.start_wsc() - - def start_wsc(self): - self.wsc.start() - - def stop(self): - self['callback']('StopServer', {'ServerId': None if self['config/app.default'] else self['auth/server-id']}) - self.wsc.stop_client() - self.http.stop_session() - - def __getitem__(self, key): - if key.startswith('config'): - return self.config[key.replace('config/', "", 1)] if "/" in key else self.config - elif key.startswith('http'): - return self.http.__shortcuts__(key.replace('http/', "", 1)) - elif key.startswith('websocket'): - return self.wsc.__shortcuts__(key.replace('websocket/', "", 1)) - elif key.startswith('callback'): - return self.callback_ws if 'ws' in key else self.callback - elif key.startswith('auth'): - return self.auth.__shortcuts__(key.replace('auth/', "", 1)) - elif key.startswith('api'): - return self.emby - elif key == 'connected': - return self.logged_in - - return diff --git a/emby/connect.py b/emby/connect.py index bb36915c8..e0c14ae44 100644 --- a/emby/connect.py +++ b/emby/connect.py @@ -1,34 +1,30 @@ # -*- coding: utf-8 -*- -import logging +import json +import os -import xbmc import xbmcaddon -import database.database +import xbmcvfs + import dialogs.serverconnect import dialogs.usersconnect import dialogs.loginconnect import dialogs.loginmanual import dialogs.servermanual -import helper.translate -import helper.api -from . import main -from .core import connection_manager -from .core import exceptions +import helper.loghandler +import emby.main class Connect(): - pending = [] - def __init__(self, Utils): self.Utils = Utils - self.info = self.Utils.get_info() - self.XML_PATH = (xbmcaddon.Addon(self.Utils.addon_id()).getAddonInfo('path'), "default", "1080i") - self.LOG = logging.getLogger("EMBY.connect.Connect") + self.XML_PATH = (xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('path'), "default", "1080i") + self.LOG = helper.loghandler.LOG('EMBY.emby.connect.Connect') self.user = None self.config = None self.connect_manager = None + self.EmbyServer = emby.main.Emby(self.Utils) - def _save_servers(self, new_servers, default=False): - credentials = database.database.get_credentials() + def _save_servers(self, new_servers, default): + credentials = self.get_credentials() if not new_servers: return credentials @@ -62,279 +58,218 @@ def _save_servers(self, new_servers, default=False): #Login into server. If server is None, then it will show the proper prompts to login, etc. #If a server id is specified then only a login dialog will be shown for that server. - def register(self, server_id=None, options={}): - self.LOG.info("--[ server/%s ]", server_id or 'default') + def register(self, options): + self.LOG.info("--[ server/%s ]" % "DEFAULT") + credentials = self.get_credentials() + new_credentials = self.register_client(credentials, options, not self.Utils.Settings.SyncInstallRunDone) + + if new_credentials: + server_id = new_credentials['Servers'][0]['Id'] + credentials = self._save_servers(new_credentials['Servers'], server_id) + new_credentials.update(credentials) + self.save_credentials(new_credentials) + return server_id, self.EmbyServer - if (server_id) in self.pending: - self.LOG.info("[ server/%s ] is already being registered", server_id or 'default') - return + return False, None - self.pending.append(server_id) - credentials = database.database.get_credentials() + def save_credentials(self, credentials): + path = self.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") - if server_id is None and credentials['Servers']: - credentials['Servers'] = [credentials['Servers'][0]] - elif credentials['Servers']: - for server in credentials['Servers']: - if server['Id'] == server_id: - credentials['Servers'] = [server] + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) - server_select = bool(server_id is None and not self.Utils.settings('SyncInstallRunDone.bool')) + credentials = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False) - try: - new_credentials = self.register_client(credentials, options, server_id, server_select) - credentials = self._save_servers(new_credentials['Servers'], server_id is None) - new_credentials.update(credentials) - database.database.save_credentials(new_credentials) - main.Emby(server_id).start(not bool(server_id), True) - except exceptions.HTTPException as error: - if error.status == 'ServerUnreachable': - self.pending.remove(server_id) - raise - except ValueError as error: - self.LOG.error(error) - - self.pending.remove(server_id) - - #Returns boolean value. - #True: verify connection. - def get_ssl(self): - return self.Utils.settings('sslverify.bool') - - #Get Emby client - def get_client(self, server, server_id=None): - client = main.Emby(server_id) - client['config/app']("Kodi", self.info['Version'], self.info['DeviceName'], self.info['DeviceId']) - client['config']['http.user_agent'] = "Emby-Kodi/%s" % self.info['Version'] - client['config']['auth.ssl'] = server.get('verify', self.get_ssl()) - return client - - def register_client(self, credentials=None, options=None, server_id=None, server_selection=False): - client = self.get_client(credentials['Servers'][0] if credentials['Servers'] else {}, server_id) - self.connect_manager = client.auth - - if server_id is None: - client['config']['app.default'] = True - - try: - state = client.authenticate(credentials or {}, options or {}) - - if state['State'] == connection_manager.CONNECTION_STATE['SignedIn']: - client.callback_ws = self.Utils.event - - if server_id is None: # Only assign for default server - client.callback = self.Utils.event - self.get_user(client) - self.Utils.settings('serverName', client['config/auth.server-name']) - self.Utils.settings('server', client['config/auth.server']) - - self.Utils.event('LoadServer', {'ServerId': server_id}) + with open(os.path.join(path, 'data.json'), 'wb') as outfile: + outfile.write(credentials.encode('utf-8')) + + def get_credentials(self): + path = self.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + if xbmcvfs.exists(os.path.join(path, "data.json")): + with open(os.path.join(path, 'data.json'), 'rb') as infile: + credentials = json.load(infile) + else: + credentials = {} + + credentials['Servers'] = credentials.get('Servers', []) + return credentials + + #Set Emby client + def set_client(self): + self.EmbyServer.Data['app.name'] = "Kodi" + self.EmbyServer.Data['app.version'] = self.Utils.device_info['Version'] + self.EmbyServer.Data['app.device_name'] = self.Utils.device_info['DeviceName'] + self.EmbyServer.Data['app.device_id'] = self.Utils.device_info['DeviceId'] + self.EmbyServer.Data['app.capabilities'] = None + self.EmbyServer.Data['http.user_agent'] = "Emby-Kodi/%s" % self.Utils.device_info['Version'] + self.EmbyServer.Data['auth.ssl'] = self.Utils.Settings.sslverify + + def register_client(self, credentials, options, server_selection): + self.set_client() + self.connect_manager = self.EmbyServer.auth + state = self.EmbyServer.authenticate(credentials or {}, options or {}) + + if state: + if state['State'] == 3: #SignedIn + server_id = state['Servers'][0]['Id'] + self.EmbyServer.server_id = server_id return state['Credentials'] - if (server_selection or state['State'] in (connection_manager.CONNECTION_STATE['ConnectSignIn'], connection_manager.CONNECTION_STATE['ServerSelection']) or state['State'] == connection_manager.CONNECTION_STATE['Unavailable'] and not self.Utils.settings('SyncInstallRunDone.bool')): - self.select_servers(state) - elif state['State'] == connection_manager.CONNECTION_STATE['ServerSignIn']: + if state['State'] == 0: #Unavailable + return False + + if server_selection or state['State'] in (4, 1): #ConnectSignIn or ServerSelection + result = self.select_servers(state) + + if not result: #Cancel + return False + elif state['State'] == 2: #ServerSignIn if 'ExchangeToken' not in state['Servers'][0]: - self.login() - elif state['State'] == connection_manager.CONNECTION_STATE['Unavailable']: - raise exceptions.HTTPException('ServerUnreachable', {}) - - return self.register_client(state['Credentials'], options, server_id, False) - except RuntimeError as error: - self.LOG.exception(error) - xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.Utils.addon_id()) - raise Exception('User sign in interrupted') - except exceptions.HTTPException as error: - if error.status == 'ServerUnreachable': - self.Utils.event('ServerUnreachable', {'ServerId': server_id}) - raise - - return client.get_credentials() + result = self.login() + + if not result: + return False + + elif state['State'] == 0: #Unavailable + return False + + return self.register_client(state['Credentials'], options, False) + + return False #Save user info - def get_user(self, client): - self.user = client['api'].get_user() - self.config = client['api'].get_system_info() - self.Utils.settings('username', self.user['Name']) + def get_user(self): + self.user = self.EmbyServer.API.get_user(None) + self.config = self.EmbyServer.API.get_system_info() + self.Utils.Settings.set_settings('username', self.user['Name']) if 'PrimaryImageTag' in self.user: - self.Utils.window('EmbyUserImage', helper.api.API(self.user, self.Utils, client['auth/server-address']).get_user_artwork(self.user['Id'])) + self.Utils.Settings.emby_UserImage = self.EmbyServer.API.get_user_artwork(self.user['Id']) + + def select_servers(self, state): + if not state: + state = self.connect_manager.connect({'enableAutoLogin': False}) + + if not state: + return False - def select_servers(self, state=None): - state = state or self.connect_manager.connect({'enableAutoLogin': False}) user = state.get('ConnectUser') or {} - self.Utils.dialog = dialogs.serverconnect.ServerConnect("script-emby-connect-server.xml", *self.XML_PATH) - self.Utils.dialog.set_args(**{ + Dialog = dialogs.serverconnect.ServerConnect("script-emby-connect-server.xml", *self.XML_PATH) + Dialog.set_args(**{ 'connect_manager': self.connect_manager, 'username': user.get('DisplayName', ""), 'user_image': user.get('ImageUrl'), 'servers': state.get('Servers', []), 'emby_connect': not user }) - self.Utils.dialog.doModal() + Dialog.doModal() - if self.Utils.dialog.is_server_selected(): - self.LOG.debug("Server selected: %s", self.Utils.dialog.get_server()) - return + if Dialog.is_server_selected(): + self.LOG.debug("Server selected: %s" % Dialog.get_server()) + return True - if self.Utils.dialog.is_connect_login(): + if Dialog.is_connect_login(): self.LOG.debug("Login with emby connect") - - try: - self.login_connect() - except RuntimeError: - pass - elif self.Utils.dialog.is_manual_server(): + self.login_connect(None) + elif Dialog.is_manual_server(): self.LOG.debug("Adding manual server") - - try: - self.manual_server() - except RuntimeError: - pass + self.manual_server(None) else: - raise RuntimeError("No server selected") - - return self.select_servers() - - #Setup manual servers - def setup_manual_server(self): - credentials = database.database.get_credentials() - client = self.get_client(credentials['Servers'][0] if credentials['Servers'] else {}) - client.set_credentials(credentials) - manager = client.auth + return False #"No server selected" - try: - self.manual_server(manager) - except RuntimeError: - return - - new_credentials = client.get_credentials() - credentials = self._save_servers(new_credentials['Servers']) - database.database.save_credentials(credentials) + return self.select_servers(None) #Return server or raise error - def manual_server(self, manager=None): - self.Utils.dialog = dialogs.servermanual.ServerManual("script-emby-connect-server-manual.xml", *self.XML_PATH) - self.Utils.dialog.set_args(**{'connect_manager': manager or self.connect_manager}) - self.Utils.dialog.doModal() - - if self.Utils.dialog.is_connected(): - return self.Utils.dialog.get_server() - - raise RuntimeError("Server is not connected") - - #Setup emby connect by itself - def setup_login_connect(self): - credentials = database.database.get_credentials() - client = self.get_client(credentials['Servers'][0] if credentials['Servers'] else {}) - client.set_credentials(credentials) - manager = client.auth + def manual_server(self, manager): + Dialog = dialogs.servermanual.ServerManual("script-emby-connect-server-manual.xml", *self.XML_PATH) + Dialog.set_args(**{'connect_manager': manager or self.connect_manager}) + Dialog.doModal() - try: - self.login_connect(manager) - except RuntimeError: - return + if Dialog.is_connected(): + return Dialog.get_server() - new_credentials = client.get_credentials() - credentials = self._save_servers(new_credentials['Servers']) - database.database.save_credentials(credentials) + #raise RuntimeError("Server is not connected") + return False #Return connect user or raise error - def login_connect(self, manager=None): - self.Utils.dialog = dialogs.loginconnect.LoginConnect("script-emby-connect-login.xml", *self.XML_PATH) - self.Utils.dialog.set_args(**{'connect_manager': manager or self.connect_manager}) - self.Utils.dialog.doModal() + def login_connect(self, manager): + Dialog = dialogs.loginconnect.LoginConnect("script-emby-connect-login.xml", *self.XML_PATH) + Dialog.set_args(**{'connect_manager': manager or self.connect_manager}) + Dialog.doModal() - if self.Utils.dialog.is_logged_in(): - return self.Utils.dialog.get_user() + if Dialog.is_logged_in(): + return Dialog.get_user() - raise RuntimeError("Connect user is not logged in") + return False #"Connect user is not logged in" def login(self): - users = self.connect_manager['public-users'] - server = self.connect_manager['server-address'] + users = self.EmbyServer.API.get_public_users() + server = self.EmbyServer.auth.get_serveraddress() if not users: - try: - return self.login_manual() - except RuntimeError: - raise RuntimeError("No user selected") + return self.login_manual(None, None) - self.Utils.dialog = dialogs.usersconnect.UsersConnect("script-emby-connect-users.xml", *self.XML_PATH) - self.Utils.dialog.set_args(**{'server': server, 'users': users}) - self.Utils.dialog.doModal() + Dialog = dialogs.usersconnect.UsersConnect("script-emby-connect-users.xml", *self.XML_PATH) + Dialog.set_args(**{'server': server, 'users': users}) + Dialog.doModal() - if self.Utils.dialog.is_user_selected(): - user = self.Utils.dialog.get_user() + if Dialog.is_user_selected(): + user = Dialog.get_user() username = user['Name'] if user['HasPassword']: self.LOG.debug("User has password, present manual login") + Result = self.login_manual(username, None) - try: - return self.login_manual(username) - except RuntimeError: - pass + if Result: + return Result else: - return self.connect_manager['login'](server, username) - elif self.Utils.dialog.is_manual_login(): - try: - return self.login_manual() - except RuntimeError: - pass + return self.connect_manager.login(server, username, None, True, {}) + elif Dialog.is_manual_login(): + Result = self.login_manual(None, None) + + if Result: + return Result else: - raise RuntimeError("No user selected") + return False #"No user selected" return self.login() - #Setup manual login by itself for default server - def setup_login_manual(self): - credentials = database.database.get_credentials() - client = self.get_client(credentials['Servers'][0] if credentials['Servers'] else {}) - client.set_credentials(credentials) - manager = client.auth - - try: - self.login_manual(manager=manager) - except RuntimeError: - return - - new_credentials = client.get_credentials() - credentials = self._save_servers(new_credentials['Servers']) - database.database.save_credentials(credentials) - #Return manual login user authenticated or raise error - def login_manual(self, user=None, manager=None): - self.Utils.dialog = dialogs.loginmanual.LoginManual("script-emby-connect-login-manual.xml", *self.XML_PATH) - self.Utils.dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}}) - self.Utils.dialog.doModal() + def login_manual(self, user, manager): + Dialog = dialogs.loginmanual.LoginManual("script-emby-connect-login-manual.xml", *self.XML_PATH) + Dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}}) + Dialog.doModal() - if self.Utils.dialog.is_logged_in(): - return self.Utils.dialog.get_user() + if Dialog.is_logged_in(): + return Dialog.get_user() - raise RuntimeError("User is not authenticated") + return False #"User is not authenticated" #Stop client and remove server def remove_server(self, server_id): - main.Emby(server_id).close() - credentials = database.database.get_credentials() + credentials = self.get_credentials() for server in credentials['Servers']: if server['Id'] == server_id: credentials['Servers'].remove(server) break - database.database.save_credentials(credentials) - self.LOG.info("[ remove server ] %s", server_id) + self.save_credentials(credentials) + self.LOG.info("[ remove server ] %s" % server_id) #Allow user to setup ssl verification for additional servers def set_ssl(self, server_id): - value = self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33217)) - credentials = database.database.get_credentials() + value = self.Utils.Dialog("yesno", heading="{emby}", line1=self.Utils.Translate(33217)) + credentials = self.get_credentials() for server in credentials['Servers']: if server['Id'] == server_id: server['verify'] = bool(value) - database.database.save_credentials(credentials) - self.LOG.info("[ ssl/%s/%s ]", server_id, server['verify']) + self.save_credentials(credentials) + self.LOG.info("[ ssl/%s/%s ]" % (server_id, server['verify'])) break diff --git a/emby/core/api.py b/emby/core/api.py index 37e227e6e..64ca948fb 100644 --- a/emby/core/api.py +++ b/emby/core/api.py @@ -1,421 +1,365 @@ # -*- coding: utf-8 -*- +import helper.loghandler + class API(): - def __init__(self, client): - self.client = client - self.info = ( - "Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount,Video3DFormat," - "OfficialRating,CumulativeRunTimeTicks,ItemCounts,PremiereDate,ProductionYear," - "Metascore,AirTime,DateCreated,People,Overview,CommunityRating,StartDate," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,Status,EndDate," - "MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio,DisplayOrder," - "PresentationUniqueKey,OriginalTitle,MediaSources,AlternateMediaSources,PartCount" - ) - - def emby_url(self, client, handler): - return "%s/emby/%s" % (client['config/auth.server'], handler) - - def _http(self, action, url, request={}): + def __init__(self, EmbyServer): + self.EmbyServer = EmbyServer + self.LOG = helper.loghandler.LOG('EMBY.emby.api.API') + self.info = "Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount,Video3DFormat,OfficialRating,CumulativeRunTimeTicks,ItemCounts,PremiereDate,ProductionYear,Metascore,AirTime,DateCreated,People,Overview,CommunityRating,StartDate,CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,Status,EndDate,MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio,DisplayOrder,PresentationUniqueKey,OriginalTitle,AlternateMediaSources,PartCount" + self.music_info = "Etag,Genres,SortName,Studios,Writer,PremiereDate,ProductionYear,OfficialRating,CumulativeRunTimeTicks,Metascore,CommunityRating,AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview,ItemCounts,PresentationUniqueKey" + self.browse_info = "Path" + + if self.EmbyServer.Utils.Settings.getDateCreated: + self.browse_info += ",DateCreated" + + if self.EmbyServer.Utils.Settings.getGenres: + self.browse_info += ",Genres" + + if self.EmbyServer.Utils.Settings.getStudios: + self.browse_info += ",Studios" + + if self.EmbyServer.Utils.Settings.getTaglines: + self.browse_info += ",Taglines" + + if self.EmbyServer.Utils.Settings.getOverview: + self.browse_info += ",Overview" + + if self.EmbyServer.Utils.Settings.getProductionLocations: + self.browse_info += ",ProductionLocations" + + if self.EmbyServer.Utils.Settings.getCast: + self.browse_info += ",People" + + def _http(self, action, url, request): request.update({'type': action, 'handler': url}) - return self.client.request(request) - - def _get(self, handler, params=None): - return self._http("GET", handler, {'params': params}) - - def _post(self, handler, json=None, params=None): - return self._http("POST", handler, {'params': params, 'json': json}) - - def _delete(self, handler, params=None): - return self._http("DELETE", handler, {'params': params}) - - ################################################################################################# - # Bigger section of the Emby api - ################################################################################################# - - def try_server(self): - return self._get("System/Info/Public") - - def sessions(self, handler="", action="GET", params=None, json=None): - if action == "POST": - return self._post("Sessions%s" % handler, json, params) - elif action == "DELETE": - return self._delete("Sessions%s" % handler, params) - else: - return self._get("Sessions%s" % handler, params) - - def users(self, handler="", action="GET", params=None, json=None): - if action == "POST": - return self._post("Users/{UserId}%s" % handler, json, params) - elif action == "DELETE": - return self._delete("Users/{UserId}%s" % handler, params) - else: - return self._get("Users/{UserId}%s" % handler, params) - - def items(self, handler="", action="GET", params=None, json=None): - if action == "POST": - return self._post("Items%s" % handler, json, params) - elif action == "DELETE": - return self._delete("Items%s" % handler, params) - else: - return self._get("Items%s" % handler, params) - - def user_items(self, handler="", params=None): - return self.users("/Items%s" % handler, params=params) - - def shows(self, handler, params): - return self._get("Shows%s" % handler, params) - - def videos(self, handler): - return self._get("Videos%s" % handler) - - def artwork(self, item_id, art, max_width, ext="jpg", index=None): - if index is None: - return self.emby_url(self.client, "Items/%s/Images/%s?MaxWidth=%s&format=%s" % (item_id, art, max_width, ext)) + return self.EmbyServer.http.request(request, None) - return self.emby_url(self.client, "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (item_id, art, index, max_width, ext)) + #Get emby user profile picture. + def get_user_artwork(self, user_id): + return "%s/emby/Users/%s/Images/Primary?Format=original" % (self.EmbyServer.Data['auth.server'], user_id) - ################################################################################################# - # More granular api - ################################################################################################# + def browse_MusicByArtistId(self, Artist_id, Parent_id, Media, Extra): + params = { + 'ParentId': Parent_id, + 'ArtistIds': Artist_id, + 'IncludeItemTypes': Media, + 'IsMissing': False, + 'Recursive': True, + 'Fields': self.browse_info + } - def get_users(self, disabled=False, hidden=None): - return self._get("Users", params={ - 'IsDisabled': disabled, - 'IsHidden': hidden - }) + if Extra is not None: + params.update(Extra) - def get_public_users(self): - return self._get("Users/Public") + return self._http("GET", "Users/%s/Items" % self.EmbyServer.Data['auth.user_id'], {'params': params}) - def get_user(self, user_id=None): - return self.users() if user_id is None else self._get("Users/%s" % user_id) + #Get dynamic listings + def get_filtered_section(self, data): + if not 'ViewId' in data: + data['ViewId'] = None - def get_views(self): - return self.users("/Views") + if not 'media' in data: + data['media'] = None - def get_media_folders(self): - return self.users("/Items") + if not 'limit' in data: + data['limit'] = None - def get_item(self, item_id): - return self.users("/Items/%s" % item_id) + if not 'recursive' in data: + data['recursive'] = True - def get_items(self, item_ids): - return self.users("/Items", params={ - 'Ids': ','.join(str(x) for x in item_ids), - 'Fields': self.info - }) - - def get_sessions(self): - return self.sessions(params={'ControllableByUserId': "{UserId}"}) - - def get_device(self, device_id): - return self.sessions(params={'DeviceId': device_id}) - - def post_session(self, session_id, url, params=None, data=None): - return self.sessions("/%s/%s" % (session_id, url), "POST", params, data) - - def get_images(self, item_id): - return self.items("/%s/Images" % item_id) - - def get_suggestion(self, media="Movie,Episode", limit=1): - return self.users("/Suggestions", { - 'Type': media, - 'Limit': limit - }) - - def search(self, term, media=None): - return self._get("Search/Hints", { - 'UserId': "{UserId}", - 'SearchTerm': term.encode('utf-8'), - 'IncludeItemTypes': media - }) - - def get_recently_added(self, media=None, parent_id=None, limit=20): - return self.user_items("/Latest", { + params = { + 'ParentId': data['ViewId'], + 'IncludeItemTypes': data['media'], + 'Recursive': data['recursive'], + 'Limit': data['limit'], + 'SortBy': "Random", + 'Fields': self.browse_info + } + + if 'random' in data: + if data['random']: + params['SortBy'] = "Random" + + if 'filters' in data: + if 'Boxsets' in data['filters']: + data['filters'].remove('Boxsets') + params['CollapseBoxSetItems'] = self.EmbyServer.Utils.Settings.groupedSets + + params['Filters'] = ','.join(data['filters']) + + if data['media'] and 'Photo' in data['media']: + params['Fields'] += ",Width,Height" + + if 'extra' in data: + if data['extra'] is not None: + params.update(data['extra']) + + return self._http("GET", "Users/%s/Items" % self.EmbyServer.Data['auth.user_id'], {'params': params}) + + def get_recently_added(self, media, parent_id, limit): + params = { 'Limit': limit, - 'UserId': "{UserId}", 'IncludeItemTypes': media, 'ParentId': parent_id, - 'Fields': self.info - }) + 'Fields': self.browse_info + } + + if media and 'Photo' in media: + params['Fields'] += ",Width,Height" + + return self._http("GET", "Users/%s/Items/Latest" % self.EmbyServer.Data['auth.user_id'], {'params': params}) + + def get_movies_by_boxset(self, boxset_id): + for items in self.get_itemsSync(boxset_id, "Movie", False, None): + yield items + + def get_episode_by_show(self, show_id): + query = { + 'url': "Shows/%s/Episodes" % show_id, + 'params': { + 'EnableUserData': True, + 'EnableImages': True, + 'UserId': self.EmbyServer.Data['auth.user_id'], + 'Fields': self.info + } + } + + for items in self._get_items(query): + yield items + + def get_episode_by_season(self, show_id, season_id): + query = { + 'url': "Shows/%s/Episodes" % show_id, + 'params': { + 'SeasonId': season_id, + 'EnableUserData': True, + 'EnableImages': True, + 'UserId': self.EmbyServer.Data['auth.user_id'], + 'Fields': self.info + } + } + + for items in self._get_items(query): + yield items + + def get_itemsSync(self, parent_id, item_type, basic, params): + query = { + 'url': "Users/%s/Items" % self.EmbyServer.Data['auth.user_id'], + 'params': { + 'ParentId': parent_id, + 'IncludeItemTypes': item_type, + 'Fields': "Etag,PresentationUniqueKey" if basic else self.info, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'EnableTotalRecordCount': False, + 'LocationTypes': "FileSystem,Remote,Offline", + 'IsMissing': False, + 'Recursive': True + } + } + + if params: + query['params'].update(params) + + for items in self._get_items(query): + yield items + + def get_artists(self, parent_id, basic, params): + query = { + 'url': "Artists", + 'params': { + 'UserId': self.EmbyServer.Data['auth.user_id'], + 'ParentId': parent_id, + 'Fields': "Etag,PresentationUniqueKey" if basic else self.music_info, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'EnableTotalRecordCount': False, + 'LocationTypes': "FileSystem,Remote,Offline", + 'IsMissing': False, + 'Recursive': True + } + } + + if params: + query['params'].update(params) + + for items in self._get_items(query): + yield items + + def get_albums_by_artist(self, parent_id, artist_id, basic): + params = { + 'ParentId': parent_id, + 'ArtistIds': artist_id + } - def get_next(self, index=None, limit=1): - return self.shows("/NextUp", { - 'Limit': limit, - 'UserId': "{UserId}", - 'StartIndex': None if index is None else int(index) - }) - - def get_adjacent_episodes(self, show_id, item_id): - return self.shows("/%s/Episodes" % show_id, { - 'UserId': "{UserId}", - 'AdjacentTo': item_id, - 'Fields': self.info - }) - - def get_genres(self, parent_id=None): - return self._get("Genres", { + for items in self.get_itemsSync(None, "MusicAlbum", basic, params): + yield items + + def get_songs_by_artist(self, parent_id, artist_id, basic): + params = { 'ParentId': parent_id, - 'UserId': "{UserId}", - 'Fields': self.info - }) + 'ArtistIds': artist_id + } + + for items in self.get_itemsSync(None, "Audio", basic, params): + yield items - def get_recommendation(self, parent_id=None, limit=20): - return self._get("Movies/Recommendations", { + def get_TotalRecordsRegular(self, parent_id, item_type): + params = { 'ParentId': parent_id, - 'UserId': "{UserId}", - 'Fields': self.info, - 'Limit': limit - }) + 'IncludeItemTypes': item_type, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'EnableTotalRecordCount': True, + 'LocationTypes': "FileSystem,Remote,Offline", + 'Recursive': True, + 'Limit': 1 + } + return self._http("GET", "Users/%s/Items" % self.EmbyServer.Data['auth.user_id'], {'params': params})['TotalRecordCount'] - def get_items_by_letter(self, parent_id=None, media=None, letter=None): - return self.user_items(params={ + def get_TotalRecordsArtists(self, parent_id): + params = { + 'UserId': self.EmbyServer.Data['auth.user_id'], 'ParentId': parent_id, - 'NameStartsWith': letter, - 'Fields': self.info, + 'CollapseBoxSetItems': False, + 'IsVirtualUnaired': False, + 'IsMissing': False, + 'EnableTotalRecordCount': True, + 'LocationTypes': "FileSystem,Remote,Offline", 'Recursive': True, - 'IncludeItemTypes': media - }) + 'Limit': 1 + } + return self._http("GET", "Artists", {'params': params})['TotalRecordCount'] + + def _get_items(self, query): + LIMIT = int(self.EmbyServer.Utils.Settings.limitIndex) + items = { + 'Items': [], + 'RestorePoint': {} + } + url = query['url'] + params = query.get('params', {}) + index = params.get('StartIndex', 0) + + while True: + params['StartIndex'] = index + params['Limit'] = LIMIT + + try: + result = self._http("GET", url, {'params': params}) or {'Items': []} + except Exception as error: + self.LOG.error("ERROR: %s" % error) + result = {'Items': []} + + if result['Items'] == []: + items['TotalRecordCount'] = index + break + + items['Items'].extend(result['Items']) + items['RestorePoint'] = query + yield items + del items['Items'][:] + index += LIMIT + + def artwork(self, item_id, art, max_width, ext, index): + if index is None: + return "%s/emby/Items/%s/Images/%s?MaxWidth=%s&format=%s" % (self.EmbyServer.Data['auth.server'], item_id, art, max_width, ext) + + return "%s/emby/Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (self.EmbyServer.Data['auth.server'], item_id, art, index, max_width, ext) + + def get_users(self, disabled, hidden): + return self._http("GET", "Users", {'params': {'IsDisabled': disabled, 'IsHidden': hidden}}) + + def get_public_users(self): + return self._http("GET", "Users/Public", {}) + + def get_user(self, user_id): + if user_id is None: + return self._http("GET", "Users/%s" % self.EmbyServer.Data['auth.user_id'], {}) + + return self._http("GET", "Users/%s" % user_id, {}) + + def get_views(self): + return self._http("GET", "Users/%s/Views" % self.EmbyServer.Data['auth.user_id'], {}) + + def get_item(self, item_id): + return self._http("GET", "Users/%s/Items/%s" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) + + def get_items(self, item_ids): + return self._http("GET", "Users/%s/Items" % self.EmbyServer.Data['auth.user_id'], {'params': {'Ids': ','.join(str(x) for x in item_ids), 'Fields': self.info}}) + + def get_device(self): + return self._http("GET", "Sessions", {'params': {'DeviceId': self.EmbyServer.Data['app.device_id']}}) + + def get_genres(self, parent_id): + return self._http("GET", "Genres", {'params': {'ParentId': parent_id, 'UserId': self.EmbyServer.Data['auth.user_id'], 'Fields': self.browse_info}}) def get_channels(self): - return self._get("LiveTv/Channels", { - 'UserId': "{UserId}", - 'EnableImages': True, - 'EnableUserData': True - }) + return self._http("GET", "LiveTv/Channels", {'params': {'UserId': self.EmbyServer.Data['auth.user_id'], 'EnableImages': True, 'EnableUserData': True}}) def get_intros(self, item_id): - return self.user_items("/%s/Intros" % item_id) + return self._http("GET", "Users/%s/Items/%s/Intros" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) def get_additional_parts(self, item_id): - return self.videos("/%s/AdditionalParts" % item_id) + return self._http("GET", "Videos/%s/AdditionalParts" % item_id, {}) def get_local_trailers(self, item_id): - return self.user_items("/%s/LocalTrailers" % item_id) + return self._http("GET", "Users/%s/Items/%s/LocalTrailers" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) def get_ancestors(self, item_id): - return self.items("/%s/Ancestors" % item_id, params={'UserId': "{UserId}"}) - - def get_items_theme_video(self, parent_id): - return self.users("/Items", params={ - 'HasThemeVideo': True, - 'ParentId': parent_id, - 'Recursive': True - }) + return self._http("GET", "Items/%s/Ancestors" % item_id, {'params': {'UserId': self.EmbyServer.Data['auth.user_id']}}) def get_themes(self, item_id): - return self.items("/%s/ThemeMedia" % item_id, params={ - 'UserId': "{UserId}", - 'InheritFromParent': True, - 'EnableThemeSongs': True, - 'EnableThemeVideos': True - }) - - def get_items_theme_song(self, parent_id): - return self.users("/Items", params={ - 'HasThemeSong': True, - 'ParentId': parent_id, - 'Recursive': True - }) + return self._http("GET", "Items/%s/ThemeMedia" % item_id, {'params': {'UserId': self.EmbyServer.Data['auth.user_id'], 'InheritFromParent': True, 'EnableThemeSongs': True, 'EnableThemeVideos': True}}) def get_plugins(self): - return self._get("Plugins") + return self._http("GET", "Plugins", {}) def get_seasons(self, show_id): - return self.shows("/%s/Seasons" % show_id, params={ - 'UserId': "{UserId}", - 'EnableImages': True, - 'Fields': self.info - }) - - def get_date_modified(self, date, parent_id, media=None): - return self.users("/Items", params={ - 'ParentId': parent_id, - 'Recursive': True, - 'IsMissing': False, - 'IsVirtualUnaired': False, - 'IncludeItemTypes': media or None, - 'MinDateLastSaved': date, - 'Fields': self.info - }) - - def get_userdata_date_modified(self, date, parent_id, media=None): - return self.users("/Items", params={ - 'ParentId': parent_id, - 'Recursive': True, - 'IsMissing': False, - 'IsVirtualUnaired': False, - 'IncludeItemTypes': media or None, - 'MinDateLastSavedForUser': date, - 'Fields': self.info - }) + return self._http("GET", "Shows/%s/Seasons" % show_id, {'params': {'UserId': self.EmbyServer.Data['auth.user_id'], 'EnableImages': True, 'Fields': self.info}}) def refresh_item(self, item_id): - return self.items("/%s/Refresh" % item_id, "POST", json={ - 'Recursive': True, - 'ImageRefreshMode': "FullRefresh", - 'MetadataRefreshMode': "FullRefresh", - 'ReplaceAllImages': False, - 'ReplaceAllMetadata': True - }) + return self._http("POST", "Items/%s/Refresh" % item_id, {'params': {'Recursive': True, 'ImageRefreshMode': "FullRefresh", 'MetadataRefreshMode': "FullRefresh", 'ReplaceAllImages': False, 'ReplaceAllMetadata': True}}) + + def favorite(self, item_id, option): + if option: + return self._http("POST", "Users/%s/FavoriteItems/%s" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) - def favorite(self, item_id, option=True): - return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE") + return self._http("DELETE", "Users/%s/FavoriteItems/%s" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) def get_system_info(self): - return self._get("System/Configuration") + return self._http("GET", "System/Configuration", {}) def post_capabilities(self, data): - return self.sessions("/Capabilities/Full", "POST", json=data) + return self._http("POST", "Sessions/Capabilities/Full", {'params': data}) - def session_add_user(self, session_id, user_id, option=True): - return self.sessions("/%s/Users/%s" % (session_id, user_id), "POST" if option else "DELETE") + def session_add_user(self, session_id, user_id, option): + if option: + return self._http("POST", "Sessions/%s/Users/%s" % (session_id, user_id), {}) + + return self._http("DELETE", "Sessions/%s/Users/%s" % (session_id, user_id), {}) def session_playing(self, data): - return self.sessions("/Playing", "POST", json=data) + return self._http("POST", "Sessions/Playing", {'params': data}) def session_progress(self, data): - return self.sessions("/Playing/Progress", "POST", json=data) + return self._http("POST", "Sessions/Playing/Progress", {'params': data}) def session_stop(self, data): - return self.sessions("/Playing/Stopped", "POST", json=data) + return self._http("POST", "Sessions/Playing/Stopped", {'params': data}) def item_played(self, item_id, watched): - return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE") - - def get_sync_queue(self, date, filters=None): - return self._get("Emby.Kodi.SyncQueue/{UserId}/GetItems", params={ - 'LastUpdateDT': date, - 'filter': filters or None - }) - - def get_server_time(self): - return self._get("Emby.Kodi.SyncQueue/GetServerDateTime") - - def get_play_info(self, item_id, profile, source_id=None, is_playback=False): - return self.items("/%s/PlaybackInfo" % item_id, "POST", json={ - 'UserId': "{UserId}", - 'DeviceProfile': profile, - 'AutoOpenLiveStream': is_playback, - 'IsPlayback': is_playback, - 'MediaSourceId': source_id - }) - - def get_playbackinfo(self, item_id): - return self.items("/%s/PlaybackInfo" % item_id, "POST", json={ - 'UserId': "{UserId}" - }) - - def get_live_stream(self, item_id, play_id, token, profile): - return self._post("LiveStreams/Open", json={ - 'UserId': "{UserId}", - 'DeviceProfile': profile, - 'OpenToken': token, - 'PlaySessionId': play_id, - 'ItemId': item_id - }) - - def close_live_stream(self, live_id): - return self._post("LiveStreams/Close", json={'LiveStreamId': live_id}) - - def close_transcode(self, device_id): - return self._delete("Videos/ActiveEncodings", params={'DeviceId': device_id}) + if watched: + return self._http("POST", "Users/%s/PlayedItems/%s" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) + + return self._http("DELETE", "Users/%s/PlayedItems/%s" % (self.EmbyServer.Data['auth.user_id'], item_id), {}) + + def get_sync_queue(self, date, filters): + return self._http("GET", "Emby.Kodi.SyncQueue/%s/GetItems" % self.EmbyServer.Data['auth.user_id'], {'params': {'LastUpdateDT': date, 'filter': filters or None}}) + + def close_transcode(self): + return self._http("DELETE", "Videos/ActiveEncodings", {'params': {'DeviceId': self.EmbyServer.Data['app.device_id']}}) def delete_item(self, item_id): - return self.items("/%s" % item_id, "DELETE") - - def is_valid_episode(self, parent_id, name, item_id): - ''' Special function to detect if episodes are displayed in emby. - Detect stacked versions, etc. - ''' - try: - result = self.shows("/%s/Episodes" % parent_id, { - 'UserId': "{UserId}", - 'AdjacentTo': item_id, - 'EnableImages': False, - 'EnableUserData': False, - 'EnableTotalRecordCount': False - }) - - for item in result['Items']: - if str(item['Id']) == item_id: - return str(item['Id']) - - for item in result['Items']: - if item['Name'] == name: - return str(item['Id']) - - raise Exception("NotFound") - except: - return item_id - - def is_valid_series(self, parent_id, name, item_id): - ''' Special function to detect if series is displayed or pooled. - Use series name. I coudln't find another way. - Returns main series id found. - ''' - try: - result = self.search(name, "Series")['SearchHints'] - - if len(result) == 1: - return str(result[0]['Id']) - - for item in result: - - if str(item['Id']) == item_id: - return str(item['Id']) - - parent_id = parent_id or self.get_library_by_item_id(item_id)['Id'] - - for item in result: - if item['Name'] == name: - try: - if self.get_library_by_item_id(item['Id'])['Id'] == parent_id: - return str(item['Id']) - except Exception: - pass - - raise Exception("NotFound") - except: - return item_id - - def is_valid_movie(self, parent_id, name, item_id): - ''' Special function to detect if movies should be displayed. - Returns movie id found or itself. - ''' - try: - result = self.search(name, "Movie")['SearchHints'] - - if len(result) == 1: - return str(result[0]['Id']) - - for item in result: - if str(item['Id']) == item_id: - return str(item['Id']) - - parent_id = parent_id or self.get_library_by_item_id(item_id)['Id'] - - for item in result: - if item['Name'] == name: - try: - if self.get_library_by_item_id(item['Id'])['Id'] == parent_id: - return str(item['Id']) - except: - pass - - raise Exception("NotFound") - except: - return item_id - - def get_library_by_item_id(self, item_id): - ''' Only applies for video content. Music content has no ancestors. - Return the library dictionary {'Id': string, 'Name': string} - ''' - ancestors = self.get_ancestors(item_id) - - if not ancestors: - raise Exception("EmptyAncestors") - - for ancestor in ancestors: - if ancestor['Type'] == 'CollectionFolder': - return ancestor + return self._http("DELETE", "Items/%s" % item_id, {}) diff --git a/emby/core/configuration.py b/emby/core/configuration.py deleted file mode 100644 index d246f79c7..000000000 --- a/emby/core/configuration.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -''' This will hold all configs from the client. - Configuration set here will be used for the HTTP client. -''' -import logging -DEFAULT_HTTP_MAX_RETRIES = 3 -DEFAULT_HTTP_TIMEOUT = 30 -LOG = logging.getLogger('Emby.emby.core.configuration') - -class Config(): - def __init__(self): - LOG.debug("Configuration initializing...") - self.data = {} - self.http() - - def __shortcuts__(self, key): - if key == "auth": - return self.auth - elif key == "app": - return self.app - elif key == "http": - return self.http - elif key == "data": - return self - - return - - def __setstate__(self, data): - self.data = data - - def __getstate__(self): - return self.data - - def __setitem__(self, key, value): - self.data[key] = value - - def __getitem__(self, key): - return self.data.get(key, self.__shortcuts__(key)) - - def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): - LOG.debug("Begin app constructor") - self.data['app.name'] = name - self.data['app.version'] = version - self.data['app.device_name'] = device_name - self.data['app.device_id'] = device_id - self.data['app.capabilities'] = capabilities - self.data['app.device_pixel_ratio'] = device_pixel_ratio - self.data['app.default'] = False - - def auth(self, server, user_id, token=None, ssl=None): - LOG.debug("Begin auth constructor") - self.data['auth.server'] = server - self.data['auth.user_id'] = user_id - self.data['auth.token'] = token - self.data['auth.ssl'] = ssl - - def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT): - LOG.debug("Begin http constructor") - self.data['http.max_retries'] = max_retries - self.data['http.timeout'] = timeout - self.data['http.user_agent'] = user_agent diff --git a/emby/core/connection_manager.py b/emby/core/connection_manager.py index f63cd784d..870d5434c 100644 --- a/emby/core/connection_manager.py +++ b/emby/core/connection_manager.py @@ -1,118 +1,50 @@ # -*- coding: utf-8 -*- import json -import logging -import hashlib import socket -import time +import _strptime # Workaround for threads using datetime: _striptime is locked import datetime -import distutils.version + +import helper.loghandler import emby.core.credentials -import emby.core.http -import emby.core.exceptions - -LOG = logging.getLogger('Emby.' + __name__) -CONNECTION_STATE = { - 'Unavailable': 0, - 'ServerSelection': 1, - 'ServerSignIn': 2, - 'SignedIn': 3, - 'ConnectSignIn': 4, - 'ServerUpdateNeeded': 5 -} -CONNECTION_MODE = { - 'Local': 0, - 'Remote': 1, - 'Manual': 2 -} - -def get_server_address(server, mode): - modes = { - CONNECTION_MODE['Local']: server.get('LocalAddress'), - CONNECTION_MODE['Remote']: server.get('RemoteAddress'), - CONNECTION_MODE['Manual']: server.get('ManualAddress') - } - - return modes.get(mode) or server.get('ManualAddress', server.get('LocalAddress', server.get('RemoteAddress'))) class ConnectionManager(): - min_server_version = "3.0.5930" - server_version = min_server_version - user = {} - server_id = None - timeout = 10 - - def __init__(self, client): - LOG.debug("ConnectionManager initializing...") - self.client = client - self.config = client.config + def __init__(self, EmbyServer): + self.LOG = helper.loghandler.LOG('EMBY.core.connection_manager') + self.LOG.debug("ConnectionManager initializing...") + self.LOG.info("Begin connectToServer") + self.user = {} + self.EmbyServer = EmbyServer self.credentials = emby.core.credentials.Credentials() - self.http = emby.core.http.HTTP(client) - - def __shortcuts__(self, key): - if key == "clear": - return self.clear_data - elif key == "servers": - return self.get_available_servers() - elif key in ("reconnect", "refresh"): - return self.connect - elif key == "login": - return self.login - elif key == "login-connect": - return self.login_to_connect - elif key == "connect-user": - return self.connect_user() - elif key == "connect-token": - return self.connect_token() - elif key == "connect-user-id": - return self.connect_user_id() - elif key == "server": - return self.get_server_info(self.server_id) - elif key == "server-id": - return self.server_id - elif key == "server-version": - return self.server_version - elif key == "user-id": - return self.emby_user_id() - elif key == "public-users": - return self.get_public_users() - elif key == "token": - return self.emby_token() - elif key == "manual-server": - return self.connect_to_address - elif key == "connect-to-server": - return self.connect_to_server - elif key == "server-address": - server = self.get_server_info(self.server_id) - return get_server_address(server, server['LastConnectionMode']) - elif key == "revoke-token": - return self.revoke_token() - elif key == "server-mode": - server = self.get_server_info(self.server_id) - return server['LastConnectionMode'] - - return - - def __getitem__(self, key): - return self.__shortcuts__(key) + + def get_server_address(self, server, mode): + modes = {0: server.get('LocalAddress'), 1: server.get('RemoteAddress'), 2: server.get('ManualAddress')} #Local...Remote...Manual + return modes.get(mode) or server.get('ManualAddress', server.get('LocalAddress', server.get('RemoteAddress'))) + + def get_serveraddress(self): + server = self.get_server_info() + return self.get_server_address(server, server['LastConnectionMode']) def clear_data(self): - LOG.info("connection manager clearing data") + self.LOG.info("connection manager clearing data") self.user = None credentials = self.credentials.get_credentials() credentials['ConnectAccessToken'] = None credentials['ConnectUserId'] = None credentials['Servers'] = list() self.credentials.get_credentials(credentials) - self.config.auth(None, None) + self.EmbyServer.Data['auth.server'] = None + self.EmbyServer.Data['auth.user_id'] = None + self.EmbyServer.Data['auth.token'] = None + self.EmbyServer.Data['auth.ssl'] = None def revoke_token(self): - LOG.info("revoking token") - self.get_server_info(self.server_id)['AccessToken'] = None - self.credentials.get_credentials(self.credentials.get_credentials()) - self.config['auth.token'] = None + self.LOG.info("revoking token") +# self.get_server_info()['AccessToken'] = None +# self.credentials.get_credentials(self.credentials.get_credentials()) +# self.EmbyServer.Data['auth.token'] = None def get_available_servers(self): - LOG.debug("Begin getAvailableServers") + self.LOG.debug("Begin getAvailableServers") # Clone the credentials credentials = self.credentials.get_credentials() @@ -120,198 +52,152 @@ def get_available_servers(self): found_servers = self._find_servers(self._server_discovery()) if not connect_servers and not found_servers and not credentials['Servers']: # back out right away, no point in continuing - LOG.info("Found no servers") + self.LOG.info("Found no servers") return list() servers = list(credentials['Servers']) self._merge_servers(servers, found_servers) self._merge_servers(servers, connect_servers) servers = self._filter_servers(servers, connect_servers) - - try: - servers.sort(key=lambda x: datetime.datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) - except TypeError: - servers.sort(key=lambda x: datetime.datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) - credentials['Servers'] = servers self.credentials.get_credentials(credentials) return servers def login_to_connect(self, username, password): if not username: - raise AttributeError("username cannot be empty") + return False #"username cannot be empty" if not password: - raise AttributeError("password cannot be empty") - - try: - result = self._request_url({ - 'type': "POST", - 'url': self.get_connect_url("user/authenticate"), - 'data': { - 'nameOrEmail': username, - 'rawpw': password - }, - 'dataType': "json" - }) - except Exception as error: # Failed to login - LOG.error(error) + return False #"password cannot be empty" + + result = self._request_url({ + 'type': "POST", + 'url': self.get_connect_url("user/authenticate"), + 'data': { + 'nameOrEmail': username, + 'rawpw': password + }, + 'dataType': "json" + }, True, False) + + if not result: #Failed to login return False - else: - credentials = self.credentials.get_credentials() - credentials['ConnectAccessToken'] = result['AccessToken'] - credentials['ConnectUserId'] = result['User']['Id'] - credentials['ConnectUser'] = result['User']['Name'] - self.credentials.get_credentials(credentials) - # Signed in - self._on_connect_user_signin(result['User']) + credentials = self.credentials.get_credentials() + credentials['ConnectAccessToken'] = result['AccessToken'] + credentials['ConnectUserId'] = result['User']['Id'] + credentials['ConnectUser'] = result['User']['Name'] + self.credentials.get_credentials(credentials) + # Signed in + self._on_connect_user_signin(result['User']) return result - def login(self, server, username, password=None, clear=True, options={}): + def login(self, server, username, password, clear, options): if not username: - raise AttributeError("username cannot be empty") - - if not server: - raise AttributeError("server cannot be empty") + self.LOG.error("username cannot be empty") + return False - try: - request = { - 'type': "POST", - 'url': self.get_emby_url(server, "Users/AuthenticateByName"), - 'json': { - 'username': username, - 'pw': password or "", - } + request = { + 'type': "POST", + 'url': self.get_emby_url(server, "Users/AuthenticateByName"), + 'params': { + 'username': username, + 'pw': password or "", } - if clear: - request['json']['pw'] = password or "" + } + + if clear: + request['params']['pw'] = password or "" - result = self._request_url(request, False) - except Exception as error: # Failed to login - LOG.error(error) + result = self._request_url(request, False, True) + + if not result: return False self._on_authenticated(result, options) return result - def connect_to_address(self, address, options={}): + def connect_to_address(self, address, options): if not address: return False address = self._normalize_address(address) + public_info = self._try_connect(address, None, options) + self.LOG.info("connectToAddress %s succeeded" % address) + server = { + 'ManualAddress': address, + 'LastConnectionMode': 2 #Manual + } + self._update_server_info(server, public_info) + server = self.connect_to_server(server, options) - def _on_fail(): - LOG.error("connectToAddress %s failed", address) - return self._resolve_failure() - - try: - public_info = self._try_connect(address, options=options) - except Exception: - return _on_fail() - else: - LOG.info("connectToAddress %s succeeded", address) - server = { - 'ManualAddress': address, - 'LastConnectionMode': CONNECTION_MODE['Manual'] - } - self._update_server_info(server, public_info) - server = self.connect_to_server(server, options) - - if server is False: - return _on_fail() + if not server: + return False - return server + return server - def connect_to_server(self, server, options={}): - LOG.debug("Begin connectToServer") + def connect_to_server(self, server, options): + self.LOG.debug("Begin connectToServer") tests = [] - if server.get('LastConnectionMode') != CONNECTION_MODE['Remote'] and server.get('AccessToken'): + if server.get('LastConnectionMode') != 1 and server.get('AccessToken'): #Remote tests.append(server['LastConnectionMode']) - if CONNECTION_MODE['Manual'] not in tests: - tests.append(CONNECTION_MODE['Manual']) + if 2 not in tests: #Manual + tests.append(2) - if CONNECTION_MODE['Local'] not in tests: - tests.append(CONNECTION_MODE['Local']) + if 0 not in tests: #Local + tests.append(0) - if CONNECTION_MODE['Remote'] not in tests: - tests.append(CONNECTION_MODE['Remote']) + if 1 not in tests: #Remote + tests.append(1) - # TODO: begin to wake server return self._test_next_connection_mode(tests, 0, server, options) - def connect(self, options={}): - LOG.info("Begin connect") + def connect(self, options): + self.LOG.info("Begin connect") return self._connect_to_servers(self.get_available_servers(), options) - def connect_user(self): - return self.user - - def connect_user_id(self): - return self.credentials.get_credentials().get('ConnectUserId') - - def connect_token(self): - return self.credentials.get_credentials().get('ConnectAccessToken') - - def emby_user_id(self): - return self.get_server_info(self.server_id)['UserId'] - - def emby_token(self): - return self.get_server_info(self.server_id)['AccessToken'] - - def get_server_info(self, server_id): - - if server_id is None: - LOG.info("server_id is empty") - return {} - + def get_server_info(self): servers = self.credentials.get_credentials()['Servers'] + for server in servers: - if server['Id'] == server_id: + if server['Id'] == self.EmbyServer.server_id: return server - def get_public_users(self): - return self.client.emby.get_public_users() - def get_connect_url(self, handler): return "https://connect.emby.media/service/%s" % handler def get_emby_url(self, base, handler): return "%s/emby/%s" % (base, handler) - def _request_url(self, request, headers=True): - request['timeout'] = request.get('timeout') or self.timeout + def _request_url(self, request, headers, MSGs): + request['timeout'] = request.get('timeout') or 10 if headers: self._get_headers(request) - try: - return self.http.request(request) - except Exception as error: - LOG.error(error) - raise - def _add_app_info(self): - return "%s/%s" % (self.config['app.name'], self.config['app.version']) + return self.EmbyServer.http.request(request, MSGs) def _get_headers(self, request): headers = request.setdefault('headers', {}) if request.get('dataType') == "json": headers['Accept'] = "application/json" + headers['Accept-Charset'] = "UTF-8,*" + headers['Accept-encoding'] = "gzip" request.pop('dataType') - headers['X-Application'] = self._add_app_info() + headers['X-Application'] = "%s/%s" % (self.EmbyServer.Data['app.name'], self.EmbyServer.Data['app.version']) headers['Content-type'] = request.get('contentType', 'application/x-www-form-urlencoded; charset=UTF-8') def _connect_to_servers(self, servers, options): - LOG.info("Begin connectToServers, with %s servers", len(servers)) + self.LOG.info("Begin connectToServers, with %s servers" % len(servers)) result = {} if len(servers) == 1: result = self.connect_to_server(servers[0], options) - LOG.debug("resolving connectToServers with result['State']: %s", result) + self.LOG.debug("resolving connectToServers with result['State']: %s" % result) return result first_server = self._get_last_used_server() @@ -320,22 +206,24 @@ def _connect_to_servers(self, servers, options): if first_server is not None and first_server['DateLastAccessed'] != "2001-01-01T00:00:00Z": result = self.connect_to_server(first_server, options) - if result['State'] in (CONNECTION_STATE['SignedIn'], CONNECTION_STATE['Unavailable']): + if not result: + return False + + if result['State'] in (3, 0): #SignedIn or Unavailable return result # Return loaded credentials if exists credentials = self.credentials.get_credentials() - self._ensure_connect_user(credentials) + self._ensure_connect_user(credentials) return { 'Servers': servers, - 'State': CONNECTION_STATE['ConnectSignIn'] if (not len(servers) and not self.connect_user()) else (result.get('State') or CONNECTION_STATE['ServerSelection']), 'ConnectUser': self.connect_user() + 'State': 4 if (not len(servers) and not self.user) else (result.get('State') or 1), 'ConnectUser': self.user #ConnectSignIn...ServerSelection } - def _try_connect(self, url, timeout=None, options={}): + def _try_connect(self, url, timeout, options, MSGs=False): url = self.get_emby_url(url, "system/info/public") - LOG.info("tryConnect url: %s", url) - + self.LOG.info("tryConnect url: %s" % url) return self._request_url({ 'type': "GET", 'url': url, @@ -343,57 +231,34 @@ def _try_connect(self, url, timeout=None, options={}): 'timeout': timeout, 'verify': options.get('ssl'), 'retry': False - }) + }, True, MSGs) def _test_next_connection_mode(self, tests, index, server, options): if index >= len(tests): - LOG.info("Tested all connection modes. Failing server connection.") - return self._resolve_failure() + self.LOG.info("Tested all connection modes. Failing server connection.") + return False mode = tests[index] - address = get_server_address(server, mode) - enable_retry = False + address = self.get_server_address(server, mode) skip_test = False - timeout = self.timeout - LOG.info("testing connection mode %s with server %s", mode, server.get('Name')) - - if mode == CONNECTION_MODE['Local']: - enable_retry = True - timeout = 8 + self.LOG.info("testing connection mode %s with server %s" % (str(mode), server.get('Name'))) + if mode == 0: #Local if self._string_equals_ignore_case(address, server.get('ManualAddress')): - LOG.info("skipping LocalAddress test because it is the same as ManualAddress") + self.LOG.info("skipping LocalAddress test because it is the same as ManualAddress") skip_test = True - elif mode == CONNECTION_MODE['Manual']: - if self._string_equals_ignore_case(address, server.get('LocalAddress')): - enable_retry = True - timeout = 8 - if skip_test or not address: - LOG.info("skipping test at index: %s", index) + self.LOG.info("skipping test at index: %s" % index) return self._test_next_connection_mode(tests, index + 1, server, options) - try: - result = self._try_connect(address, timeout, options) - except Exception: - LOG.error("test failed for connection mode %s with server %s", mode, server.get('Name')) - - if enable_retry: - # TODO: wake on lan and retry - return self._test_next_connection_mode(tests, index + 1, server, options) - else: - return self._test_next_connection_mode(tests, index + 1, server, options) - else: - if self._compare_versions(self._get_min_server_version(), result['Version']) == 1: - LOG.warning("minServerVersion requirement not met. Server version: %s", result['Version']) - return { - 'State': CONNECTION_STATE['ServerUpdateNeeded'], - 'Servers': [server] - } - else: - LOG.info("calling onSuccessfulConnection with connection mode %s with server %s", mode, server.get('Name')) - return self._on_successful_connection(server, result, mode, options) + result = self._try_connect(address, 10, options, False) + + if not result: + return self._test_next_connection_mode(tests, index + 1, server, options) + + self.LOG.info("calling onSuccessfulConnection with connection mode %s with server %s" % (mode, server.get('Name'))) + return self._on_successful_connection(server, result, mode, options) def _on_successful_connection(self, server, system_info, connection_mode, options): credentials = self.credentials.get_credentials() @@ -405,43 +270,15 @@ def _on_successful_connection(self, server, system_info, connection_mode, option return self._after_connect_validated(server, credentials, system_info, connection_mode, True, options) - def _resolve_failure(self): - return { - 'State': CONNECTION_STATE['Unavailable'], - 'ConnectUser': self.connect_user() - } - - def _get_min_server_version(self, val=None): - if val is not None: - self.min_server_version = val - - return self.min_server_version - - def _compare_versions(self, a, b): - ''' -1 a is smaller - 1 a is larger - 0 equal - ''' - a = distutils.version.LooseVersion(a) - b = distutils.version.LooseVersion(b) - - if a < b: - return -1 - - if a > b: - return 1 - - return 0 - def _string_equals_ignore_case(self, str1, str2): return (str1 or "").lower() == (str2 or "").lower() def _get_connect_user(self, user_id, access_token): if not user_id: - raise AttributeError("null userId") + return False #"null userId" if not access_token: - raise AttributeError("null accessToken") + return False #"null accessToken" return self._request_url({ 'type': "GET", @@ -450,7 +287,7 @@ def _get_connect_user(self, user_id, access_token): 'headers': { 'X-Connect-UserToken': access_token } - }) + }, True, False) def _server_discovery(self): MULTI_GROUP = ("", 7359) @@ -460,33 +297,31 @@ def _server_discovery(self): sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - LOG.debug("MultiGroup : %s", str(MULTI_GROUP)) - LOG.debug("Sending UDP Data: %s", MESSAGE) + self.LOG.debug("MultiGroup : %s" % str(MULTI_GROUP)) + self.LOG.debug("Sending UDP Data: %s" % MESSAGE) servers = [] try: sock.sendto(MESSAGE, MULTI_GROUP) except Exception as error: - LOG.error(error) - + self.LOG.error("ERROR: %s" % error) return servers while True: data = None - addr = None try: - data, addr = sock.recvfrom(1024) # buffer size + data, _ = sock.recvfrom(1024) # buffer size servers.append(json.loads(data)) except socket.timeout: - LOG.info("Found Servers: %s", servers) + self.LOG.info("Found Servers: %s" % servers) return servers - except Exception as e: - LOG.error("Error trying to find servers: %s", e) + except Exception as Error: + self.LOG.error("Error trying to find servers: %s" % Error) return servers def _get_connect_servers(self, credentials): - LOG.debug("Begin getConnectServers") + self.LOG.debug("Begin getConnectServers") servers = list() if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): @@ -502,7 +337,12 @@ def _get_connect_servers(self, credentials): } } - for server in self._request_url(request): + result = self._request_url(request, True, False) + + if not result: + return [] + + for server in result: servers.append({ 'ExchangeToken': server['AccessKey'], 'ConnectServerId': server['Id'], @@ -521,19 +361,11 @@ def _get_last_used_server(self): if not len(servers): return - try: - servers.sort(key=lambda x: datetime.datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) - except TypeError: - servers.sort(key=lambda x: datetime.datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) - return servers[0] def _merge_servers(self, list1, list2): for i in range(0, len(list2), 1): - try: - self.credentials.add_update_server(list1, list2[i]) - except KeyError: - continue + self.credentials.add_update_server(list1, list2[i]) return list1 @@ -544,7 +376,7 @@ def _find_servers(self, found_servers): server = self._convert_endpoint_address_to_manual_address(found_server) if not server and not found_server.get('Address'): - LOG.warning("Server %s has no address.", found_server) + self.LOG.warning("Server %s has no address." % found_server) continue info = { @@ -580,12 +412,8 @@ def _convert_endpoint_address_to_manual_address(self, info): if len(parts) > 1: port_string = parts[len(parts)-1] - - try: - address += ":%s" % int(port_string) - return self._normalize_address(address) - except ValueError: - pass + address += ":%s" % int(port_string) + return self._normalize_address(address) return None @@ -599,37 +427,18 @@ def _normalize_address(self, address): return address - def _get_connect_password_hash(self, password): - password = self._clean_connect_password(password) - return hashlib.md5(password).hexdigest() - - def _clean_connect_password(self, password): - password = password or "" - password = password.replace("&", '&') - password = password.replace("/", '\') - password = password.replace("!", '!') - password = password.replace("$", '$') - password = password.replace("\"", '"') - password = password.replace("<", '<') - password = password.replace(">", '>') - password = password.replace("'", ''') - return password - def _ensure_connect_user(self, credentials): if self.user and self.user['Id'] == credentials['ConnectUserId']: return - elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): - self.user = None - try: - result = self._get_connect_user(credentials['ConnectUserId'], credentials['ConnectAccessToken']) - self._on_connect_user_signin(result) - except Exception: - return False + if credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): + self.user = None + result = self._get_connect_user(credentials['ConnectUserId'], credentials['ConnectAccessToken']) + self._on_connect_user_signin(result) def _on_connect_user_signin(self, user): self.user = user - LOG.info("connectusersignedin %s", user) + self.LOG.info("connectusersignedin %s" % user) def _save_user_info_into_credentials(self, server, user): info = { @@ -638,56 +447,57 @@ def _save_user_info_into_credentials(self, server, user): } self.credentials.add_update_user(server, info) - def _add_authentication_info_from_connect(self, server, connection_mode, credentials, options={}): + def _add_authentication_info_from_connect(self, server, connection_mode, credentials, options): if not server.get('ExchangeToken'): - raise KeyError("server['ExchangeToken'] cannot be null") + self.LOG.error('server ExchangeToken cannot be null') + return False if not credentials.get('ConnectUserId'): - raise KeyError("credentials['ConnectUserId'] cannot be null") + self.LOG.error('credentials ConnectUserId cannot be null') + return False - auth = "MediaBrowser " - auth += "Client=%s, " % self.config['app.name'] - auth += "Device=%s, " % self.config['app.device_name'] - auth += "DeviceId=%s, " % self.config['app.device_id'] - auth += "Version=%s " % self.config['app.version'] + auth = "Emby " + auth += "Client=%s, " % self.EmbyServer.Data['app.name'] + auth += "Device=%s, " % self.EmbyServer.Data['app.device_name'] + auth += "DeviceId=%s, " % self.EmbyServer.Data['app.device_id'] + auth += "Version=%s " % self.EmbyServer.Data['app.version'] + auth = self._request_url({ + 'url': self.get_emby_url(self.get_server_address(server, connection_mode), "Connect/Exchange"), + 'type': "GET", + 'dataType': "json", + 'verify': options.get('ssl'), + 'params': { + 'ConnectUserId': credentials['ConnectUserId'] + }, + 'headers': { + 'X-Emby-Token': server['ExchangeToken'], + 'Authorization': auth + } + }, True, False) - try: - auth = self._request_url({ - 'url': self.get_emby_url(get_server_address(server, connection_mode), "Connect/Exchange"), - 'type': "GET", - 'dataType': "json", - 'verify': options.get('ssl'), - 'params': { - 'ConnectUserId': credentials['ConnectUserId'] - }, - 'headers': { - 'X-MediaBrowser-Token': server['ExchangeToken'], - 'X-Emby-Authorization': auth - } - }) - except Exception: + if not auth: server['UserId'] = None server['AccessToken'] = None return False - else: - server['UserId'] = auth['LocalUserId'] - server['AccessToken'] = auth['AccessToken'] - return auth + + server['UserId'] = auth['LocalUserId'] + server['AccessToken'] = auth['AccessToken'] + return auth def _after_connect_validated(self, server, credentials, system_info, connection_mode, verify_authentication, options): if options.get('enableAutoLogin') == False: - self.config['auth.user_id'] = server.pop('UserId', None) - self.config['auth.token'] = server.pop('AccessToken', None) + self.EmbyServer.Data['auth.user_id'] = server.pop('UserId', None) + self.EmbyServer.Data['auth.token'] = server.pop('AccessToken', None) elif verify_authentication and server.get('AccessToken'): if self._validate_authentication(server, connection_mode, options) is not False: - self.config['auth.user_id'] = server['UserId'] - self.config['auth.token'] = server['AccessToken'] + self.EmbyServer.Data['auth.user_id'] = server['UserId'] + self.EmbyServer.Data['auth.token'] = server['AccessToken'] return self._after_connect_validated(server, credentials, system_info, connection_mode, False, options) - elif server.get('AccessToken'): - return self._resolve_failure() + + if server.get('AccessToken'): + return False self._update_server_info(server, system_info) - self.server_version = system_info['Version'] server['LastConnectionMode'] = connection_mode if options.get('updateDateLastAccessed') is not False: @@ -695,40 +505,31 @@ def _after_connect_validated(self, server, credentials, system_info, connection_ self.credentials.add_update_server(credentials['Servers'], server) self.credentials.get_credentials(credentials) - self.server_id = server['Id'] # Update configs - self.config['auth.server'] = get_server_address(server, connection_mode) - self.config['auth.server-name'] = server['Name'] - self.config['auth.server=id'] = server['Id'] - self.config['auth.ssl'] = options.get('ssl', self.config['auth.ssl']) - - result = { - 'Servers': [server], - 'ConnectUser': self.connect_user() - } - result['State'] = CONNECTION_STATE['SignedIn'] if server.get('AccessToken') else CONNECTION_STATE['ServerSignIn'] + self.EmbyServer.Data['auth.server'] = self.get_server_address(server, connection_mode) + self.EmbyServer.Data['auth.server-name'] = server['Name'] + self.EmbyServer.server_id = server['Id'] + self.EmbyServer.Data['auth.ssl'] = options.get('ssl', self.EmbyServer.Data['auth.ssl']) + result = {'Servers': [server], 'ConnectUser': self.user} + result['State'] = 3 if server.get('AccessToken') else 2 #SignedIn...ServerSignIn # Connected return result - def _validate_authentication(self, server, connection_mode, options={}): - try: - system_info = self._request_url({ - 'type': "GET", - 'url': self.get_emby_url(get_server_address(server, connection_mode), "System/Info"), - 'verify': options.get('ssl'), - 'dataType': "json", - 'headers': { - 'X-MediaBrowser-Token': server['AccessToken'] - } - }) + def _validate_authentication(self, server, connection_mode, options): + system_info = self._request_url({ + 'type': "GET", + 'url': self.get_emby_url(self.get_server_address(server, connection_mode), "System/Info"), + 'verify': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-Emby-Token': server['AccessToken'] + } + }, True, False) + + if system_info: self._update_server_info(server, system_info) return True - except emby.core.exceptions.HTTPException as error: - if error.status == 'Unauthorized': - server['AccessToken'] = None - except Exception as error: - LOG.error(error) return False @@ -746,10 +547,10 @@ def _update_server_info(self, server, system_info): if 'MacAddress' in system_info: server['WakeOnLanInfos'] = [{'MacAddress': system_info['MacAddress']}] - def _on_authenticated(self, result, options={}): + def _on_authenticated(self, result, options): credentials = self.credentials.get_credentials() - self.config['auth.user_id'] = result['User']['Id'] - self.config['auth.token'] = result['AccessToken'] + self.EmbyServer.Data['auth.user_id'] = result['User']['Id'] + self.EmbyServer.Data['auth.token'] = result['AccessToken'] for server in credentials['Servers']: if server['Id'] == result['ServerId']: diff --git a/emby/core/credentials.py b/emby/core/credentials.py index 7930116d0..692691422 100644 --- a/emby/core/credentials.py +++ b/emby/core/credentials.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- -import logging import time +import _strptime # Workaround for threads using datetime: _striptime is locked import datetime -LOG = logging.getLogger('Emby.' + __name__) + +import helper.loghandler class Credentials(): credentials = None def __init__(self): - LOG.debug("Credentials initializing...") + self.LOG = helper.loghandler.LOG('Emby.core.credentials') + self.LOG.debug("Credentials initializing...") def set_credentials(self, credentials): self.credentials = dict(credentials) @@ -22,14 +24,16 @@ def get_credentials(self, data=None): def _ensure(self): if not self.credentials: try: - LOG.info(self.credentials) + self.LOG.info(self.credentials) + if not isinstance(self.credentials, dict): - raise ValueError("invalid credentials format") - except Exception as e: # File is either empty or missing - LOG.warning(e) + self.LOG.error("invalid credentials format") + return False #"invalid credentials format" + except Exception as error: #File is either empty or missing + self.LOG.warning(error) self.credentials = {} - LOG.debug("credentials initialized with: %s", self.credentials) + self.LOG.debug("credentials initialized with: %s" % self.credentials) self.credentials['Servers'] = self.credentials.setdefault('Servers', []) def _get(self): @@ -42,7 +46,7 @@ def _set(self, data): else: self._clear() - LOG.debug("credentialsupdated") + self.LOG.debug("credentialsupdated") def _clear(self): self.credentials.clear() @@ -58,7 +62,8 @@ def add_update_user(self, server, user): def add_update_server(self, servers, server): if server.get('Id') is None: - raise KeyError("Server['Id'] cannot be null or empty") + self.LOG.error("Server['Id'] cannot be null or empty") + return False # Add default DateLastAccessed if doesn't exist. server.setdefault('DateLastAccessed', "1970-01-01T00:00:00Z") diff --git a/emby/core/exceptions.py b/emby/core/exceptions.py deleted file mode 100644 index b8b34ab7e..000000000 --- a/emby/core/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -class HTTPException(Exception): - # Emby HTTP exception - def __init__(self, status, message): - self.status = status - self.message = message diff --git a/emby/core/http.py b/emby/core/http.py index ac96bc543..cbd5bc732 100644 --- a/emby/core/http.py +++ b/emby/core/http.py @@ -1,77 +1,47 @@ # -*- coding: utf-8 -*- import json -import logging import time import requests -import emby.core.exceptions + import xbmc +import helper.loghandler + if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 19: unicode = str class HTTP(): - def __init__(self, client): - self.LOG = logging.getLogger('emby.core.HTTP') + def __init__(self, EmbyServer): + self.LOG = helper.loghandler.LOG('EMBY.core.HTTP') self.session = None - self.keep_alive = False - self.client = client - self.config = client['config'] - - def __shortcuts__(self, key): - if key == "request": - return self.request - - return + self.EmbyServer = EmbyServer def start_session(self): self.session = requests.Session() - """ - max_retries = self.config['http.max_retries'] - self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries)) - self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries)) - """ def stop_session(self): if self.session is None: return try: - self.LOG.warning("--<[ session/%s ]", id(self.session)) + self.LOG.warning("--<[ session/%s ]" % id(self.session)) self.session.close() + self.session = None except Exception as error: - self.LOG.warning("The requests session could not be terminated: %s", error) + self.LOG.warning("The requests session could not be terminated: %s" % error) - def _replace_user_info(self, string): - if '{server}' in string: - if self.config['auth.server']: - string = string.replace("{server}", self.config['auth.server']) - else: - raise Exception("Server address not set.") + def request(self, data, MSGs=True, Binary=False): #MSGs are disabled on initial sync and reconnection. Only send msgs if connection is unexpectly interrupted + if 'url' not in data: + data['url'] = "%s/emby/%s" % (self.EmbyServer.Data['auth.server'], data.pop('handler', "")) + + data = self.get_header(data) - if '{UserId}'in string: - if self.config['auth.user_id']: - string = string.replace("{UserId}", self.config['auth.user_id']) - else: - raise Exception("UserId is not set.") - - return string - - def request(self, data, session=None): - ''' Give a chance to retry the connection. Emby sometimes can be slow to answer back - data dictionary can contain: - type: GET, POST, etc. - url: (optional) - handler: not considered when url is provided (optional) - params: request parameters (optional) - json: request body (optional) - headers: (optional), - verify: ssl certificate, True (verify using device built-in library) or False - ''' if not data: - raise AttributeError("Request cannot be empty") + return False - data = self._request(data) - self.LOG.debug("--->[ http ] %s", json.dumps(data, indent=4)) + data['timeout'] = self.EmbyServer.Data['http.timeout'] + data['verify'] = self.EmbyServer.Data['auth.ssl'] + self.LOG.debug("--->[ http ] %s" % json.dumps(data, indent=4)) retry = data.pop('retry', 5) def _retry(current): @@ -83,52 +53,61 @@ def _retry(current): while True: try: - r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data) + r = self._requests(self.session or requests, data.pop('type', "GET"), **data) r.content # release the connection - - if not self.keep_alive and self.session is not None: - self.stop_session() - r.raise_for_status() - except requests.exceptions.ConnectionError as error: + except requests.exceptions.ConnectionError: retry = _retry(retry) if retry: continue - self.LOG.error(error) - self.client['callback']("ServerUnreachable", {'ServerId': self.config['auth.server-id']}) - raise emby.core.exceptions.HTTPException("ServerUnreachable", error) - except requests.exceptions.ReadTimeout as error: + self.LOG.error("[ ServerUnreachable ]") + + if MSGs: + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % ("plugin.video.emby-next-gen", "ServerUnreachable", '"[%s]"' % json.dumps({'ServerId': self.EmbyServer.server_id}).replace('"', '\\"'))) + + return False + except requests.exceptions.ReadTimeout: retry = _retry(retry) if retry: continue - self.LOG.error(error) - raise emby.core.exceptions.HTTPException("ReadTimeout", error) + self.LOG.error("[ ServerTimeout ]") - except requests.exceptions.HTTPError as error: - self.LOG.error(error) + if MSGs: + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % ("plugin.video.emby-next-gen", "ServerTimeout", '"[%s]"' % json.dumps({'ServerId': self.EmbyServer.server_id}).replace('"', '\\"'))) + + return False + except requests.exceptions.HTTPError: +# self.LOG.error(error) if r.status_code == 401: if 'X-Application-Error-Code' in r.headers: - self.client['callback']("AccessRestricted", {'ServerId': self.config['auth.server-id']}) - raise emby.core.exceptions.HTTPException("AccessRestricted", error) - - self.client['callback']("Unauthorized", {'ServerId': self.config['auth.server-id']}) - self.client['auth/revoke-token'] - raise emby.core.exceptions.HTTPException("Unauthorized", error) - elif r.status_code == 500: # log and ignore. - self.LOG.error("--[ 500 response ] %s", error) - return - elif r.status_code == 400: # log and ignore. - self.LOG.error("--[ 400 response ] %s", error) - return - elif r.status_code == 404: # log and ignore. - self.LOG.error("--[ 404 response ] %s", error) - return - elif r.status_code == 502: + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % ("plugin.video.emby-next-gen", "AccessRestricted", '"[%s]"' % json.dumps({'ServerId': self.EmbyServer.server_id}).replace('"', '\\"'))) + return False + + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % ("plugin.video.emby-next-gen", "Unauthorized", '"[%s]"' % json.dumps({'ServerId': self.EmbyServer.server_id}).replace('"', '\\"'))) + self.EmbyServer.auth.revoke_token() + return False + + if r.status_code == 500: # log and ignore. +# self.LOG.error("--[ 500 response ] %s" % error) + self.LOG.error("[ 500 response ]") + return False + + if r.status_code == 400: # log and ignore. +# self.LOG.error("--[ 400 response ] %s" % error) + self.LOG.error("[ 400 response ]") + return False + + if r.status_code == 404: # log and ignore. +# self.LOG.error("--[ 404 response ] %s" % error) + self.LOG.error("[ 404 response ]") + return False + + if r.status_code == 502: retry = _retry(retry) if retry: @@ -139,62 +118,39 @@ def _retry(current): if retry: continue - raise emby.core.exceptions.HTTPException(r.status_code, error) - except requests.exceptions.MissingSchema as error: - raise emby.core.exceptions.HTTPException("MissingSchema", {'ServerId': self.config['auth.server']}) + return False + except requests.exceptions.MissingSchema: + self.LOG.error("[ MissingSchema ]") + return False except Exception as error: - raise + self.LOG.error(error) + return False else: elapsed = int(r.elapsed.total_seconds() * 1000) - self.LOG.debug("---<[ http ][%s ms]", elapsed) - - try: - self.config['server-time'] = r.headers['Date'] - - if r.status_code == 204: - # return, because there is no response - return - - response = r.json() + self.LOG.debug("---<[ http ][%s ms]" % elapsed) + if Binary: + return r.content + else: try: - self.LOG.debug(json.dumps(response, indent=4)) - except Exception: - self.LOG.debug(response) - - return response - except ValueError: - return - - def _request(self, data): - if 'url' not in data: - data['url'] = "%s/emby/%s" % (self.config['auth.server'], data.pop('handler', "")) - - self._get_header(data) - data['timeout'] = data.get('timeout') or self.config['http.timeout'] - data['url'] = self._replace_user_info(data['url']) + self.EmbyServer.Data['server-time'] = r.headers['Date'] - if data.get('verify') is None: - if self.config['auth.ssl'] is None: - data['verify'] = data['url'].startswith('https') - else: - data['verify'] = self.config['auth.ssl'] - - self._process_params(data.get('params') or {}) - self._process_params(data.get('json') or {}) - return data + if r.status_code == 204: + # return, because there is no response + return - def _process_params(self, params): - for key in params: - value = params[key] + response = r.json() - if isinstance(value, dict): - self._process_params(value) + try: + self.LOG.debug(json.dumps(response, indent=4)) + except Exception: + self.LOG.debug(response) - if isinstance(value, (str, unicode)): - params[key] = self._replace_user_info(value) + return response + except ValueError: + return r.content - def _get_header(self, data): + def get_header(self, data): data['headers'] = data.setdefault('headers', {}) if not data['headers']: @@ -202,37 +158,42 @@ def _get_header(self, data): 'Content-type': "application/json", 'Accept-Charset': "UTF-8,*", 'Accept-encoding': "gzip", - 'User-Agent': self.config['http.user_agent'] or "%s/%s" % (self.config['app.name'], self.config['app.version']) + 'User-Agent': self.EmbyServer.Data['http.user_agent'] or "%s/%s" % (self.EmbyServer.Data['app.name'], self.EmbyServer.Data['app.version']) }) if 'Authorization' not in data['headers']: - self._authorization(data) + data = self._authorization(data) return data def _authorization(self, data): - if not self.config['app.device_name']: - raise KeyError("Device name cannot be null") - - auth = "MediaBrowser " - auth += "Client=%s, " % self.config['app.name'] - auth += "Device=%s, " % self.config['app.device_name'] - auth += "DeviceId=%s, " % self.config['app.device_id'] - auth += "Version=%s" % self.config['app.version'] + if not self.EmbyServer.Data['app.device_name']: + return False #Device name cannot be null + + auth = "Emby " + auth += "Client=%s, " % self.EmbyServer.Data['app.name'] + auth += "Device=%s, " % self.EmbyServer.Data['app.device_name'] + auth += "DeviceId=%s, " % self.EmbyServer.Data['app.device_id'] + auth += "Version=%s" % self.EmbyServer.Data['app.version'] data['headers'].update({'Authorization': auth}) - if self.config['auth.token'] and self.config['auth.user_id']: - auth += ', UserId=%s' % self.config['auth.user_id'] - data['headers'].update({'Authorization': auth, 'X-MediaBrowser-Token': self.config['auth.token']}) + if self.EmbyServer.Data['auth.token'] and self.EmbyServer.Data['auth.user_id']: + auth += ', UserId=%s' % self.EmbyServer.Data['auth.user_id'] + data['headers'].update({'Authorization': auth, 'X-Emby-Token': self.EmbyServer.Data['auth.token']}) return data def _requests(self, session, action, **kwargs): if action == "GET": return session.get(**kwargs) - elif action == "POST": + + if action == "POST": return session.post(**kwargs) - elif action == "HEAD": + + if action == "HEAD": return session.head(**kwargs) - elif action == "DELETE": + + if action == "DELETE": return session.delete(**kwargs) + + return None diff --git a/emby/core/websocket.py b/emby/core/websocket.py deleted file mode 100644 index 40d0e2cbb..000000000 --- a/emby/core/websocket.py +++ /dev/null @@ -1,651 +0,0 @@ -# -*- coding: utf-8 -*- -#websocket - WebSocket client library for Python - -#Copyright (C) 2010 Hiroki Ohtani(liris) - -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. - -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. - -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import os -import array -import struct -import uuid -import hashlib -import base64 -import threading -import time -import logging -import traceback -import sys -import socket - -import xbmc - -if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 19: - unicode = str - -if sys.version_info[0] < 3: - Python3 = False - from urlparse import urlparse -else: - Python3 = True - from urllib.parse import urlparse - -try: - import ssl - from ssl import SSLError - HAVE_SSL = True -except ImportError: - # dummy class of SSLError for ssl none-support environment. - class SSLError(Exception): - pass - - HAVE_SSL = False - -# websocket supported version. -VERSION = 13 - -# closing frame status codes. -STATUS_NORMAL = 1000 -STATUS_GOING_AWAY = 1001 -STATUS_PROTOCOL_ERROR = 1002 -STATUS_UNSUPPORTED_DATA_TYPE = 1003 -STATUS_STATUS_NOT_AVAILABLE = 1005 -STATUS_ABNORMAL_CLOSED = 1006 -STATUS_INVALID_PAYLOAD = 1007 -STATUS_POLICY_VIOLATION = 1008 -STATUS_MESSAGE_TOO_BIG = 1009 -STATUS_INVALID_EXTENSION = 1010 -STATUS_UNEXPECTED_CONDITION = 1011 -STATUS_TLS_HANDSHAKE_ERROR = 1015 -logger = logging.getLogger() - -class WebSocketException(Exception): - """ - websocket exeception class. - """ - -class WebSocketConnectionClosedException(WebSocketException): - """ - If remote host closed the connection or some network error happened, - this exception will be raised. - """ - -class WebSocketTimeoutException(WebSocketException): - """ - WebSocketTimeoutException will be raised at socket timeout during read/write data. - """ - -def _wrap_sni_socket(sock, sslopt, hostname): - context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) - - if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: - capath = ssl.get_default_verify_paths().capath - context.load_verify_locations(cafile=sslopt.get('ca_certs', None), capath=sslopt.get('ca_cert_path', capath)) - - return context.wrap_socket( - sock, - do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), - suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), - server_hostname=hostname, - ) - -def _parse_url(url): - if ":" not in url: - raise ValueError("url is invalid") - - scheme, url = url.split(":", 1) - parsed = urlparse(url, scheme="http") - - if parsed.hostname: - hostname = parsed.hostname - else: - raise ValueError("hostname is invalid") - - port = 0 - - if parsed.port: - port = parsed.port - - is_secure = False - - if scheme == "ws": - if not port: - port = 80 - elif scheme == "wss": - is_secure = True - if not port: - port = 443 - else: - raise ValueError("scheme %s is invalid" % scheme) - - if parsed.path: - resource = parsed.path - else: - resource = "/" - - if parsed.query: - resource += "?" + parsed.query - - return (hostname, port, resource, is_secure) - -def create_connection(url, timeout=None, **options): - sockopt = options.get("sockopt", []) - sslopt = options.get("sslopt", {}) - websock = WebSocket(sockopt=sockopt, sslopt=sslopt) - websock.settimeout(timeout) - websock.connect(url, **options) - return websock - -_MAX_INTEGER = (1 << 32) -1 -_MAX_CHAR_BYTE = (1<<8) -1 - -def _create_sec_websocket_key(): - uid = uuid.uuid4() - return base64.b64encode(uid.bytes).strip() - -_HEADERS_TO_CHECK = { - "upgrade": "websocket", - "connection": "upgrade", - } - -class ABNF(): - # operation code values. - OPCODE_CONT = 0x0 - OPCODE_TEXT = 0x1 - OPCODE_BINARY = 0x2 - OPCODE_CLOSE = 0x8 - OPCODE_PING = 0x9 - OPCODE_PONG = 0xa - - # available operation code value tuple - OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG) - - # opcode human readable string - OPCODE_MAP = { - OPCODE_CONT: "cont", - OPCODE_TEXT: "text", - OPCODE_BINARY: "binary", - OPCODE_CLOSE: "close", - OPCODE_PING: "ping", - OPCODE_PONG: "pong" - } - - # data length threashold. - LENGTH_7 = 0x7d - LENGTH_16 = 1 << 16 - LENGTH_63 = 1 << 63 - - def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, opcode=OPCODE_TEXT, mask=1, data=""): - self.fin = fin - self.rsv1 = rsv1 - self.rsv2 = rsv2 - self.rsv3 = rsv3 - self.opcode = opcode - self.mask = mask - self.data = data - self.get_mask_key = os.urandom - - def __str__(self): - return "fin=" + str(self.fin) + " opcode=" + str(self.opcode) + " data=" + str(self.data) - - @staticmethod - def create_frame(data, opcode): - if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): - data = data.encode("utf-8") - # mask must be set if send data from client - return ABNF(1, 0, 0, 0, opcode, 1, data) - - def format(self): - if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): - raise ValueError("not 0 or 1") - if self.opcode not in ABNF.OPCODES: - raise ValueError("Invalid OPCODE") - length = len(self.data) - - if length >= ABNF.LENGTH_63: - raise ValueError("data is too long") - - frame_header = struct.pack("B", (self.fin << 7 | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 | self.opcode)) - - if length < ABNF.LENGTH_7: - frame_header += struct.pack("B", (self.mask << 7 | length)) - elif length < ABNF.LENGTH_16: - frame_header += struct.pack("B", (self.mask << 7 | 0x7e)) - frame_header += struct.pack("!H", length) - else: - frame_header += struct.pack("B", (self.mask << 7 | 0x7f)) - frame_header += struct.pack("!Q", length) - - if not self.mask: - return frame_header + self.data - - mask_key = self.get_mask_key(4) - return frame_header + self._get_masked(mask_key) - - def _get_masked(self, mask_key): - s = ABNF.maskData(mask_key, self.data) - return mask_key + b"".join(s) - - @staticmethod - 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] - - return _d.tostring() - -class WebSocket(): - def __init__(self, get_mask_key=None, sockopt=None, sslopt=None): - if sockopt is None: - sockopt = [] - - if sslopt is None: - sslopt = {} - - self.connected = False - self.sock = socket.socket() - - for opts in sockopt: - self.sock.setsockopt(*opts) - - self.sslopt = sslopt - self.get_mask_key = get_mask_key - # Buffers over the packets from the layer beneath until desired amount - # bytes of bytes are received. - self._recv_buffer = [] - # These buffer over the build-up of a single frame. - self._frame_header = None - self._frame_length = None - self._frame_mask = None - self._cont_data = None - - def fileno(self): - return self.sock.fileno() - - def set_mask_key(self, func): - self.get_mask_key = func - - def gettimeout(self): - return self.sock.gettimeout() - - def settimeout(self, timeout): - self.sock.settimeout(timeout) - - timeout = property(gettimeout, settimeout) - - def connect(self, url, **options): - hostname, port, resource, is_secure = _parse_url(url) - self.sock.connect((hostname, port)) - - if is_secure: - if HAVE_SSL: - if self.sslopt is None: - sslopt = {} - else: - sslopt = self.sslopt - - if ssl.HAS_SNI: - self.sock = _wrap_sni_socket(self.sock, sslopt, hostname) - else: - self.sock = ssl.wrap_socket(self.sock, **sslopt) - else: - raise WebSocketException("SSL not available.") - - self._handshake(hostname, port, resource, **options) - - def _handshake(self, host, port, resource, **options): - headers = [] - headers.append("GET %s HTTP/1.1" % resource) - headers.append("Upgrade: websocket") - headers.append("Connection: Upgrade") - - if port == 80: - hostport = host - else: - hostport = "%s:%d" % (host, port) - - headers.append("Host: %s" % hostport) - - if "origin" in options: - headers.append("Origin: %s" % options["origin"]) - else: - headers.append("Origin: http://%s" % hostport) - - key = _create_sec_websocket_key().decode('utf-8') - headers.append("Sec-WebSocket-Key: %s" % key) - headers.append("Sec-WebSocket-Version: %s" % str(VERSION)) - - if "header" in options: - headers.extend(options["header"]) - - headers.append("") - headers.append("") - header_str = "\r\n".join(headers) - self._send(header_str.encode('utf-8')) - status, resp_headers = self._read_headers() - - if status != 101: - self.close() - raise WebSocketException("Handshake Status %d" % status) - - success = self._validate_header(resp_headers, key) - - if not success: - self.close() - raise WebSocketException("Invalid WebSocket Header") - - self.connected = True - - def _validate_header(self, headers, key): - result = headers.get("sec-websocket-accept", None) - - if not result: - return False - - value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - value = value.encode("utf-8") - hashed = base64.b64encode(hashlib.sha1(value).digest()).decode() - return hashed == result - - def _read_headers(self): - status = None - headers = {} - - while True: - line = self._recv_line() - - if line == "\r\n": - break - - line = line.strip() - - if not status: - status_info = line.split(" ", 2) - status = int(status_info[1]) - else: - kv = line.split(":", 1) - - if len(kv) == 2: - key, value = kv - headers[key.lower()] = value.strip() - else: - raise WebSocketException("Invalid header") - - return status, headers - - def send(self, payload, opcode=ABNF.OPCODE_TEXT): - frame = ABNF.create_frame(payload, opcode) - - if self.get_mask_key: - frame.get_mask_key = self.get_mask_key - - data = frame.format() - length = len(data) - - while data: - l = self._send(data) - data = data[l:] - - return length - - def send_binary(self, payload): - return self.send(payload, ABNF.OPCODE_BINARY) - - def ping(self, payload=b""): - self.send(payload, ABNF.OPCODE_PING) - - def pong(self, payload): - self.send(payload, ABNF.OPCODE_PONG) - - def recv(self): - opcode, data = self.recv_data() - return data - - def recv_data(self): - while True: - frame = self.recv_frame() - - if not frame: - # handle error: - # 'NoneType' object has no attribute 'opcode' - raise WebSocketException("Not a valid frame %s" % frame) - elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): - if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data: - raise WebSocketException("Illegal frame") - - if self._cont_data: - self._cont_data[1] += frame.data - else: - self._cont_data = [frame.opcode, frame.data] - - if frame.fin: - data = self._cont_data - self._cont_data = None - return data - elif frame.opcode == ABNF.OPCODE_CLOSE: - self.send_close() - return (frame.opcode, None) - elif frame.opcode == ABNF.OPCODE_PING: - self.pong(frame.data) - - def recv_frame(self): - # Header - if self._frame_header is None: - self._frame_header = self._recv_strict(2) - - if Python3: - b1 = self._frame_header[0] - b2 = self._frame_header[1] - else: - b1 = ord(self._frame_header[0]) - b2 = ord(self._frame_header[1]) - - fin = b1 >> 7 & 1 - rsv1 = b1 >> 6 & 1 - rsv2 = b1 >> 5 & 1 - rsv3 = b1 >> 4 & 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 - payload = self._recv_strict(self._frame_length) - - if has_mask: - payload = ABNF.maskData(self._frame_mask, payload) - - # Reset for next frame - self._frame_header = None - self._frame_length = None - self._frame_mask = None - return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) - - def send_close(self, status=STATUS_NORMAL, reason=b""): - if status < 0 or status >= ABNF.LENGTH_16: - raise ValueError("code is invalid range") - - self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) - - def close(self, status=STATUS_NORMAL, reason=""): - try: - self.sock.shutdown(socket.SHUT_RDWR) - except: - pass - - self._closeInternal() - - def _closeInternal(self): - self.connected = False - self.sock.close() - - def _send(self, data): - try: - return self.sock.send(data) - except socket.timeout as e: - raise WebSocketTimeoutException(e.args[0]) - except Exception as e: - if "timed out" in e.args[0]: - raise WebSocketTimeoutException(e.args[0]) - else: - raise e - - def _recv(self, bufsize): - try: - bytesData = self.sock.recv(bufsize) - except socket.timeout as e: - raise WebSocketTimeoutException(e.args[0]) - except SSLError as e: - if e.args[0] == "The read operation timed out": - raise WebSocketTimeoutException(e.args[0]) - else: - raise - - if not bytesData: - raise WebSocketConnectionClosedException() - - return bytesData - - def _recv_strict(self, bufsize): - shortage = bufsize - sum(len(x) for x in self._recv_buffer) - - while shortage > 0: - bytesData = self._recv(shortage) - self._recv_buffer.append(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 _recv_line(self): - line = [] - - while True: - c = self._recv(1).decode() - line.append(c) - - if c == "\n": - break - - return "".join(line) - -class WebSocketApp(): - def __init__(self, url, header=[], on_open=None, on_message=None, on_error=None, on_close=None, keep_running=True, get_mask_key=None): - - self.url = url - self.header = header - self.on_open = on_open - self.on_message = on_message - self.on_error = on_error - self.on_close = on_close - self.keep_running = keep_running - self.get_mask_key = get_mask_key - self.sock = None - - def send(self, data, opcode=ABNF.OPCODE_TEXT): - if self.sock.send(data, opcode) == 0: - raise WebSocketConnectionClosedException() - - def close(self): - self.keep_running = False - if self.sock != None: - self.sock.close() - - def _send_ping(self, interval): - while True: - for i in range(interval): - time.sleep(1) - if not self.keep_running: - return - self.sock.ping() - - def run_forever(self, sockopt=None, sslopt=None, ping_interval=0): - if sockopt is None: - sockopt = [] - - if sslopt is None: - sslopt = {} - - if self.sock: - raise WebSocketException("socket is already opened") - - thread = None - self.keep_running = True - - try: - self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt) - self.sock.settimeout(None) - self.sock.connect(self.url, header=self.header) - self._callback(self.on_open) - - if ping_interval: - thread = threading.Thread(target=self._send_ping, args=(ping_interval,)) - thread.setDaemon(True) - thread.start() - - while self.keep_running: - try: - data = self.sock.recv() - - if data is None or self.keep_running == False: - break - - self._callback(self.on_message, data) - except Exception as e: - if "timed out" not in e.args[0]: - raise e - - except Exception as e: - self._callback(self.on_error, e) - finally: - if thread: - self.keep_running = False - - self.sock.close() - self._callback(self.on_close) - self.sock = None - - def _callback(self, callback, *args): - if callback: - try: - callback(self, *args) - except Exception as e: - logger.error(e) - - if logger.isEnabledFor(logging.DEBUG): - _, _, tb = sys.exc_info() - traceback.print_tb(tb) diff --git a/emby/core/ws_client.py b/emby/core/ws_client.py index 99f29a2ec..47b2beeb6 100644 --- a/emby/core/ws_client.py +++ b/emby/core/ws_client.py @@ -1,76 +1,337 @@ # -*- coding: utf-8 -*- import json -import logging import threading -import time -import emby.core.websocket -LOG = logging.getLogger('Emby.core.ws_client') +import os +import array +import struct +import uuid +import hashlib +import base64 +import socket +import ssl -class WSClient(threading.Thread): - wsc = None - stop = False +import xbmc - def __init__(self, client): - LOG.debug("WSClient initializing...") - self.client = client - threading.Thread.__init__(self) +import helper.loghandler + +if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 19: + unicode = str + Python3 = True + from urllib.parse import urlparse +else: + Python3 = False + from urlparse import urlparse + +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 + + if Python3: + return _d.tobytes() + + return _d.tostring() + +class ABNF(): + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, opcode=0x1, mask=1, data=""): + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + self.data = data + self.get_mask_key = os.urandom + + def __str__(self): + return "fin=" + str(self.fin) + " opcode=" + str(self.opcode) + " data=" + str(self.data) - def __shortcuts__(self, key): - if key == "send": - return self.send - elif key == "stop": - return self.stop_client() + def format(self): + length = len(self.data) + frame_header = struct.pack("B", (self.fin << 7 | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 | self.opcode)) - return + if length < 0x7d: + frame_header += struct.pack("B", (self.mask << 7 | length)) + elif length < 1 << 16: #LENGTH_16 + frame_header += struct.pack("B", (self.mask << 7 | 0x7e)) + frame_header += struct.pack("!H", length) + else: + frame_header += struct.pack("B", (self.mask << 7 | 0x7f)) + frame_header += struct.pack("!Q", length) - def send(self, message, data=""): - if self.wsc is None: - raise ValueError("The websocket client is not started.") + if not self.mask: + return frame_header + self.data - self.wsc.send(json.dumps({'MessageType': message, "Data": data})) + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = maskData(mask_key, self.data) + return mask_key + b"".join(s) + +class WSClient(threading.Thread): + def __init__(self, Server, DeviceID, Token, server_id): + self.Server = Server + self.DeviceID = DeviceID + self.Token = Token + self.server_id = server_id + self.wsc = None + self.stop = False + self.LOG = helper.loghandler.LOG('Emby.core.ws_client') + self.LOG.debug("WSClient initializing...") + self.stop = False + self.connected = False + self.sock = socket.socket() + self.get_mask_key = None + self._recv_buffer = [] + self._frame_header = None + self._frame_length = None + self._frame_mask = None + self._cont_data = None + threading.Thread.__init__(self) def run(self): - token = self.client['config/auth.token'] - device_id = self.client['config/app.device_id'] - server = self.client['config/auth.server'] - server = server.replace('https', "wss") if server.startswith('https') else server.replace('http', "ws") - wsc_url = "%s/embywebsocket?api_key=%s&device_id=%s" % (server, token, device_id) - LOG.info("Websocket url: %s", wsc_url) - self.wsc = emby.core.websocket.WebSocketApp(wsc_url, on_message=self.on_message, on_error=self.on_error) - self.wsc.on_open = self.on_open + self.LOG.info("--->[ websocket ]") + server = self.Server.replace('https', "wss") if self.Server.startswith('https') else self.Server.replace('http', "ws") + self.connect("%s/embywebsocket?api_key=%s&device_id=%s" % (server, self.Token, self.DeviceID)) + thread = threading.Thread(target=self.ping) + thread.start() + data = None while not self.stop: - self.wsc.run_forever(ping_interval=10) - - if self.stop: + try: + data = self.recv() + except: break - time.sleep(5) - self.client['callback_ws']('WebSocketRestarting') - - LOG.info("---<[ websocket ]") + if data is None or self.stop: + break - def on_error(self, ws, error): - LOG.error(error) + self.on_message(data) - def on_open(self, ws): - LOG.info("--->[ websocket ]") + self.LOG.info("---<[ websocket ]") - def on_message(self, ws, message): + def on_message(self, message): message = json.loads(message) data = message.get('Data', {}) + if not data: #check if data is an empty string + data = {} + + data['ServerId'] = self.server_id + if message['MessageType'] == 'RefreshProgress': - LOG.debug("Ignoring %s", message) + self.LOG.debug("Ignoring %s" % message) return - if not self.client['config/app.default']: - data['ServerId'] = self.client['auth/server-id'] + data = '"[%s]"' % json.dumps(data).replace('"', '\\"') + xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % ("plugin.video.emby-next-gen", message['MessageType'], data)) - self.client['callback_ws'](message['MessageType'], data) + def send(self, message, data): + self.sock.send(json.dumps({'MessageType': message, "Data": data}), 0x1) - def stop_client(self): + def close(self): self.stop = True - if self.wsc is not None: - self.wsc.close() + try: + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + self.connected = False + self.sock.close() + + def connect(self, url): + #Parse URL + scheme, url = url.split(":", 1) + parsed = urlparse(url, scheme="http") + resource = parsed.path + + if parsed.query: + resource += "?" + parsed.query + + hostname = parsed.hostname + port = parsed.port + is_secure = scheme == "wss" + + #Connect + self.sock.connect((hostname, port)) + self.sock.settimeout(None) + + 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 + headers = [] + headers.append("GET %s HTTP/1.1" % resource) + headers.append("Upgrade: websocket") + headers.append("Connection: Upgrade") + hostport = "%s:%d" % (hostname, port) + headers.append("Host: %s" % hostport) + headers.append("Origin: http://%s" % hostport) + uid = uuid.uuid4() + key = base64.b64encode(uid.bytes).strip().decode('utf-8') + headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Version: 13") + headers.append("") + headers.append("") + header_str = "\r\n".join(headers) + self.sock.send(header_str.encode('utf-8')) + + #Read Headers + status = None + headers = {} + + while True: + line = [] + + while True: + c = self.sock.recv(1).decode() + line.append(c) + + if c == "\n": + break + + line = "".join(line) + + if line == "\r\n": + break + + line = line.strip() + + if not status: + status_info = line.split(" ", 2) + status = int(status_info[1]) + else: + kv = line.split(":", 1) + + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip() + + #Validate Headers + result = headers.get("sec-websocket-accept", None) + + if not result: + return False + + value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + value = value.encode("utf-8") + hashed = base64.b64encode(hashlib.sha1(value).digest()).decode() + + #Set connected state + self.connected = hashed == result + + def sendCommands(self, payload, opcode): + if opcode == 0x1 and isinstance(payload, unicode): + frame = payload.encode("utf-8") + else: + frame = ABNF(1, 0, 0, 0, opcode, 1, payload) + + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + + data = frame.format() + length = len(data) + + while data: + try: + l = self.sock.send(data) + data = data[l:] + except: #Offline + break + + def ping(self): + while True: + if xbmc.Monitor().waitForAbort(10): + return + + if self.stop: + return + + self.sendCommands(b"", 0x9) + + def recv(self): + while True: + # Header + if self._frame_header is None: + self._frame_header = self._recv_strict(2) + + if Python3: + b1 = self._frame_header[0] + b2 = self._frame_header[1] + else: + b1 = ord(self._frame_header[0]) + b2 = ord(self._frame_header[1]) + + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 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 + payload = self._recv_strict(self._frame_length) + + if has_mask: + payload = maskData(self._frame_mask, payload) + + # Reset for next frame + self._frame_header = None + self._frame_length = None + self._frame_mask = None + frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + + if frame.opcode in (0x1, 0x0): + if self._cont_data: + self._cont_data[1] += frame.data + else: + self._cont_data = [frame.opcode, frame.data] + + if frame.fin: + data = self._cont_data + self._cont_data = None + return data[1] + + elif frame.opcode == 0x8: + self.sendCommands(struct.pack('!H', 1000) + b"", 0x8) + return None #(frame.opcode, None) + elif frame.opcode == 0x9: + self.sendCommands(frame.data, 0xa) #Pong + + def _recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self._recv_buffer) + + while shortage > 0: + bytesData = self.sock.recv(shortage) + self._recv_buffer.append(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] diff --git a/emby/downloader.py b/emby/downloader.py deleted file mode 100644 index 460a60a80..000000000 --- a/emby/downloader.py +++ /dev/null @@ -1,335 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -try: - import queue as Queue -except ImportError: - import Queue - -import threading -import requests -import helper.wrapper -from . import main -from .core import exceptions - -class Downloader(): - def __init__(self, Utils): - self.Utils = Utils - self.LOG = logging.getLogger("EMBY.downloader.Downloader") - self.LIMIT = min(int(self.Utils.settings('limitIndex') or 50), 50) - self.info = ("Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount,Video3DFormat,OfficialRating,CumulativeRunTimeTicks,ItemCounts,PremiereDate,ProductionYear,Metascore,AirTime,DateCreated,People,Overview,CommunityRating,StartDate,CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,Status,EndDate,MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio,DisplayOrder,PresentationUniqueKey,OriginalTitle,MediaSources,AlternateMediaSources,PartCount") - self.browse_info = ("DateCreated,EpisodeCount,SeasonCount,Path,Genres,Studios,Taglines,MediaStreams,Overview,Etag,ProductionLocations,Width,Height,RecursiveItemCount,ChildCount") - - def get_embyserver_url(self, handler): - if handler.startswith('/'): - handler = handler[1:] - self.LOG.warning("handler starts with /: %s", handler) - - return "{server}/emby/%s" % handler - - def _http(self, action, url, request={}, server_id=None): - request.update({'url': url, 'type': action}) - return main.Emby(server_id)['http/request'](request) - - def _get(self, handler, params=None, server_id=None): - return self._http("GET", self.get_embyserver_url(handler), {'params': params}, server_id) - - def _post(self, handler, json=None, params=None, server_id=None): - return self._http("POST", self.get_embyserver_url(handler), {'params': params, 'json': json}, server_id) - - def _delete(self, handler, params=None, server_id=None): - return self._http("DELETE", self.get_embyserver_url(handler), {'params': params}, server_id) - - #This confirms a single item from the library matches the view it belongs to. - #Used to detect grouped libraries. - def validate_view(self, library_id, item_id): - try: - result = self._get("Users/{UserId}/Items", {'ParentId': library_id, 'Recursive': True, 'Ids': item_id}) - except Exception: - return False - - return bool(len(result['Items'])) - - #Get dynamic listings - def get_filtered_section(self, parent_id=None, media=None, limit=None, recursive=None, sort=None, sort_order=None, filters=None, extra=None, server_id=None, ForceNoSort=None): - if ForceNoSort: - params = { - 'ParentId': parent_id, - 'IncludeItemTypes': media, - 'IsMissing': False, - 'Recursive': recursive if recursive is not None else True, - 'Limit': limit, - 'ImageTypeLimit': 1, - 'IsVirtualUnaired': False, - 'Fields': self.browse_info - } - else: - params = { - 'ParentId': parent_id, - 'IncludeItemTypes': media, - 'IsMissing': False, - 'Recursive': recursive if recursive is not None else True, - 'Limit': limit, - 'SortBy': sort or "SortName", - 'SortOrder': sort_order or "Ascending", - 'ImageTypeLimit': 1, - 'IsVirtualUnaired': False, - 'Fields': self.browse_info - } - - if filters: - if 'Boxsets' in filters: - filters.remove('Boxsets') - params['CollapseBoxSetItems'] = self.Utils.settings('groupedSets.bool') - - params['Filters'] = ','.join(filters) - - if self.Utils.settings('getCast.bool'): - params['Fields'] += ",People" - - if media and 'Photo' in media: - params['Fields'] += ",Width,Height" - - if extra is not None: - params.update(extra) - - return self._get("Users/{UserId}/Items", params, server_id) - - def get_movies_by_boxset(self, boxset_id): - for items in self.get_items(boxset_id, "Movie"): - yield items - - def get_episode_by_show(self, show_id): - query = { - 'url': "Shows/%s/Episodes" % show_id, - 'params': { - 'EnableUserData': True, - 'EnableImages': True, - 'UserId': "{UserId}", - 'Fields': self.info - } - } - - for items in self._get_items(query, self.LIMIT): - yield items - - def get_episode_by_season(self, show_id, season_id): - query = { - 'url': "Shows/%s/Episodes" % show_id, - 'params': { - 'SeasonId': season_id, - 'EnableUserData': True, - 'EnableImages': True, - 'UserId': "{UserId}", - 'Fields': self.info - } - } - - for items in self._get_items(query, self.LIMIT): - yield items - - def get_items(self, parent_id, item_type=None, basic=False, params=None): - query = { - 'url': "Users/{UserId}/Items", - 'params': { - 'ParentId': parent_id, - 'IncludeItemTypes': item_type, - 'Fields': "Etag,PresentationUniqueKey" if basic else self.info, - 'SortOrder': "Ascending", - 'SortBy': "SortName", - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True - } - } - - if params: - query['params'].update(params) - - for items in self._get_items(query, self.LIMIT): - yield items - - def get_artists(self, parent_id=None, basic=False, params=None, server_id=None): - music_info = ( - "Etag,Genres,SortName,Studios,Writer,PremiereDate,ProductionYear," - "OfficialRating,CumulativeRunTimeTicks,Metascore,CommunityRating," - "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview,ItemCounts," - "PresentationUniqueKey" - ) - query = { - 'url': "Artists", - 'params': { - 'UserId': "{UserId}", - 'ParentId': parent_id, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': "Etag,PresentationUniqueKey" if basic else music_info, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'EnableTotalRecordCount': False, - 'LocationTypes': "FileSystem,Remote,Offline", - 'IsMissing': False, - 'Recursive': True - } - } - - if params: - query['params'].update(params) - - for items in self._get_items(query, self.LIMIT, server_id): - yield items - - def get_albums_by_artist(self, parent_id, artist_id, basic=False): - params = { - 'SortBy': "DateCreated", - 'ParentId': parent_id, - 'ArtistIds': artist_id - } - - for items in self.get_items(None, "MusicAlbum", basic, params): - yield items - - def get_songs_by_artist(self, parent_id, artist_id, basic=False): - params = { - 'SortBy': "DateCreated", - 'ParentId': parent_id, - 'ArtistIds': artist_id - } - - for items in self.get_items(None, "Audio", basic, params): - yield items - - def get_TotalRecordsRegular(self, parent_id, item_type=None, server_id=None): - Params = { - 'ParentId': parent_id, - 'IncludeItemTypes': item_type, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'EnableTotalRecordCount': True, - 'LocationTypes': "FileSystem,Remote,Offline", - 'Recursive': True, - 'Limit': 1 - } - - return self._get("Users/{UserId}/Items", Params, server_id=server_id)['TotalRecordCount'] - - def get_TotalRecordsArtists(self, parent_id=None, server_id=None): - Params = { - 'UserId': "{UserId}", - 'ParentId': parent_id, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'EnableTotalRecordCount': True, - 'LocationTypes': "FileSystem,Remote,Offline", - 'Recursive': True, - 'Limit': 1 - } - - return self._get("Artists", Params, server_id=server_id)['TotalRecordCount'] - - @helper.wrapper.stop - def _get_items(self, query, LIMIT, server_id=None): - items = { - 'Items': [], - 'RestorePoint': {} - } - - url = query['url'] - params = query.get('params', {}) - index = params.get('StartIndex', 0) - - while True: - params['StartIndex'] = index - params['Limit'] = LIMIT - - try: - result = self._get(url, params, server_id=server_id) or {'Items': []} - except Exception as error: - self.LOG.error(error) - result = {'Items': []} - - if result['Items'] == []: - items['TotalRecordCount'] = index - break - - items['Items'].extend(result['Items']) - items['RestorePoint'] = query - yield items - del items['Items'][:] - index += LIMIT - -class GetItemWorker(threading.Thread): - def __init__(self, server, queue, output, Utils): - self.server = server - self.queue = queue - self.output = output - self.removed = [] - self.Utils = Utils - self.is_done = False - self.LOG = logging.getLogger("EMBY.downloader.GetItemWorker") - self.info = ( - "Path,Genres,SortName,Studios,Writer,Taglines,LocalTrailerCount,Video3DFormat," - "OfficialRating,CumulativeRunTimeTicks,ItemCounts,PremiereDate,ProductionYear," - "Metascore,AirTime,DateCreated,People,Overview,CommunityRating,StartDate," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,Status,EndDate," - "MediaSources,VoteCount,RecursiveItemCount,PrimaryImageAspectRatio,DisplayOrder," - "PresentationUniqueKey,OriginalTitle,MediaSources,AlternateMediaSources,PartCount" - ) - threading.Thread.__init__(self) - self.start() - - def Done(self): - return self.is_done - - def run(self): - count = 0 - - with requests.Session() as s: - while True: - try: - item_ids = self.queue.get(timeout=1) - except Queue.Empty: - self.LOG.info("--<[ q:download/%s/%s ]", id(self), count) - self.LOG.info("--[ q:download/remove ] %s", self.removed) - break - - clean_list = [str(x) for x in item_ids] - request = { - 'type': "GET", - 'handler': "Users/{UserId}/Items", - 'params': { - 'Ids': ','.join(clean_list), - 'Fields': self.info - } - } - - try: - result = self.server['http/request'](request, s) - self.removed.extend(list(set(clean_list) - set([str(x['Id']) for x in result['Items']]))) - - for item in result['Items']: - if item['Type'] in self.output: - self.output[item['Type']].put(item) - - except exceptions.HTTPException as error: - self.LOG.error("--[ http status: %s ]", error.status) - - if error.status in ('ServerUnreachable', 'ReadTimeout', 503): - self.queue.put(item_ids) - break - except Exception as error: - self.LOG.exception(error) - - count += len(clean_list) - self.queue.task_done() - - if self.Utils.window('emby_should_stop.bool'): - break - - self.is_done = True diff --git a/emby/main.py b/emby/main.py index b72e58bdc..813cac7d8 100644 --- a/emby/main.py +++ b/emby/main.py @@ -1,75 +1,87 @@ -import logging -from . import client - -LOG = logging.getLogger('EMBY') - -def has_attribute(obj, name): - try: - object.__getattribute__(obj, name) - return True - except AttributeError: - return False - -def ensure_client(): - def decorator(func): - def wrapper(self, *args, **kwargs): - if self.client.get(self.server_id) is None: - self.construct() - - return func(self, *args, **kwargs) - - return wrapper - return decorator +# -*- coding: utf-8 -*- +import helper.loghandler +from .core import api +from .core import http +from .core import ws_client +from .core import connection_manager class Emby(): - client = {} - server_id = "default" - - def __init__(self, server_id=None): - self.server_id = server_id or "default" - - def get_client(self): - return self.client[self.server_id] - - def close(self): - if self.server_id not in self.client: - return - - self.client[self.server_id].stop() - self.client.pop(self.server_id, None) - LOG.info("---[ STOPPED EMBYCLIENT: %s ]---", self.server_id) - - @classmethod - def close_all(cls): - for clientData in dict(cls.client): - cls.client[clientData].stop() - - cls.client = {} - LOG.info("---[ STOPPED ALL EMBYCLIENTS ]---") - - @classmethod - def get_active_clients(cls): - return cls.client - - @ensure_client() - def __setattr__(self, name, value): - if has_attribute(self, name): - return super(Emby, self).__setattr__(name, value) - - setattr(self.client[self.server_id], name, value) - - @ensure_client() - def __getattr__(self, name): - return getattr(self.client[self.server_id], name) - - @ensure_client() - def __getitem__(self, key): - return self.client[self.server_id][key] - - def construct(self): - self.client[self.server_id] = client.EmbyClient() - - if self.server_id == 'default': - LOG.info("---[ START EMBYCLIENT ]---") - else: - LOG.info("---[ START EMBYCLIENT: %s ]---", self.server_id) + def __init__(self, Utils): + self.LOG = helper.loghandler.LOG('EMBY.emby.main') + self.logged_in = False + self.wsock = None + self.http = http.HTTP(self) + self.auth = connection_manager.ConnectionManager(self) + self.server_id = None + self.Online = False + + + self.Nodes = [] + + + self.Utils = Utils + self.API = api.API(self) + self.Data = {'http.user_agent': None, 'http.timeout': 30, 'http.max_retries': 3, 'auth.server': None, 'auth.user_id': None, 'auth.token': None, 'auth.ssl': None, 'app.name': None, 'app.version': None, 'app.device_name': None, 'app.device_id': self.Utils.get_device_id(False), 'app.capabilities': None, 'auth.server-name': None} + self.LOG.info("---[ INIT EMBYCLIENT: ]---") + + def authenticate(self, credentials, options): + self.auth.credentials.set_credentials(credentials or {}) + state = self.auth.connect(options or {}) + + if not state: + return False + + if state['State'] == 3: #SignedIn + self.logged_in = True + self.LOG.info("User is authenticated.") + + state['Credentials'] = self.auth.credentials.get_credentials() + return state + + def start(self): + if not self.logged_in: + self.LOG.error("User is not logged in.") + return False #"User is not authenticated." + + self.LOG.info("---[ START EMBYCLIENT: %s ]---" % self.server_id) + self.http.start_session() + self.API.post_capabilities({ + 'PlayableMediaTypes': "Audio,Video", + 'SupportsMediaControl': 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," + "Play,Playstate,PlayNext,PlayMediaSource" + ), + 'IconUrl': "https://raw.githubusercontent.com/MediaBrowser/plugin.video.emby/master/kodi_icon.png" + }) + + if self.Utils.Settings.Users: + session = self.API.get_device() + users = self.Utils.Settings.Users.split(',') + hidden = None if self.Utils.Settings.addUsersHidden else False + all_users = self.API.get_users(False, hidden) + + for additional in users: + for user in all_users: + if user['Id'] == additional: + self.API.session_add_user(session[0]['Id'], user['Id'], True) + + #Websocket + self.wsock = ws_client.WSClient(self.Data['auth.server'], self.Data['app.device_id'], self.Data['auth.token'], self.server_id) + self.wsock.start() + + def stop(self): + self.LOG.info("---[ STOP EMBYCLIENT: %s ]---" % self.server_id) + + if self.wsock: + self.wsock.close() + + self.wsock = None + self.http.stop_session() diff --git a/emby/views.py b/emby/views.py index 35d4c9d0c..f6f97cd4d 100644 --- a/emby/views.py +++ b/emby/views.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import logging import os -import shutil try: from urllib import urlencode @@ -10,193 +8,300 @@ import xml.etree.ElementTree import xbmcvfs +import xbmcgui import database.database import database.emby_db -import helper.translate import helper.api -from . import main +import helper.loghandler class Views(): - def __init__(self, Utils): + def __init__(self, Embyserver): + self.EmbyServer = Embyserver self.limit = 25 self.media_folders = None - self.sync = database.database.get_sync() - self.server = main.Emby() - self.Utils = Utils - self.LOG = logging.getLogger("EMBY.views.Views") + self.LOG = helper.loghandler.LOG('EMBY.emby.views.Views') + self.APIHelper = helper.api.API(self.EmbyServer.Utils) + self.LibraryIcons = {} self.NODES = { 'tvshows': [ - ('all', None), - ('recent', helper.translate._(30170)), - ('recentepisodes', helper.translate._(30175)), - ('inprogress', helper.translate._(30171)), - ('inprogressepisodes', helper.translate._(30178)), - ('nextepisodes', helper.translate._(30179)), - ('genres', 135), - ('random', helper.translate._(30229)), - ('recommended', helper.translate._(30230)), - ('years', helper.translate._(33218)), - ('actors', helper.translate._(33219)), - ('tags', helper.translate._(33220)) + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultTVShows.png'), + ('recentlyadded', self.EmbyServer.Utils.Translate(30170), 'DefaultRecentlyAddedEpisodes.png'), + ('recentlyaddedepisodes', self.EmbyServer.Utils.Translate(30175), 'DefaultRecentlyAddedEpisodes.png'), + ('inprogress', self.EmbyServer.Utils.Translate(30171), 'DefaultInProgressShows.png'), + ('inprogressepisodes', self.EmbyServer.Utils.Translate(30178), 'DefaultInProgressShows.png'), + ('genres', "Genres", 'DefaultGenre.png'), + ('random', self.EmbyServer.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png'), + ('recommended', self.EmbyServer.Utils.Translate(30230), 'DefaultFavourites.png'), + ('years', self.EmbyServer.Utils.Translate(33218), 'DefaultYear.png'), + ('actors', self.EmbyServer.Utils.Translate(33219), 'DefaultActor.png'), + ('tags', self.EmbyServer.Utils.Translate(33220), 'DefaultTags.png'), + ('unwatched', "Unwatched TV Shows", 'OverlayUnwatched.png'), + ('unwatchedepisodes', "Unwatched Episodes", 'OverlayUnwatched.png'), + ('studios', "Studios", 'DefaultStudios.png'), + ('recentlyplayed', 'Recently played TV Show', 'DefaultMusicRecentlyPlayed.png'), + ('recentlyplayedepisode', 'Recently played Episode', 'DefaultMusicRecentlyPlayed.png'), + ('nextepisodes', self.EmbyServer.Utils.Translate(30179), 'DefaultInProgressShows.png') ], 'movies': [ - ('all', None), - ('recent', helper.translate._(30174)), - ('inprogress', helper.translate._(30177)), - ('unwatched', helper.translate._(30189)), - ('sets', 20434), - ('genres', 135), - ('random', helper.translate._(30229)), - ('recommended', helper.translate._(30230)), - ('years', helper.translate._(33218)), - ('actors', helper.translate._(33219)), - ('tags', helper.translate._(33220)) + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultMovies.png'), + ('recentlyadded', self.EmbyServer.Utils.Translate(30174), 'DefaultRecentlyAddedMovies.png'), + ('inprogress', self.EmbyServer.Utils.Translate(30177), 'DefaultInProgressShows.png'), + ('unwatched', self.EmbyServer.Utils.Translate(30189), 'OverlayUnwatched.png'), + ('sets', "Sets", 'DefaultSets.png'), + ('genres', "Genres", 'DefaultGenre.png'), + ('random', self.EmbyServer.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png'), + ('recommended', self.EmbyServer.Utils.Translate(30230), 'DefaultFavourites.png'), + ('years', self.EmbyServer.Utils.Translate(33218), 'DefaultYear.png'), + ('actors', self.EmbyServer.Utils.Translate(33219), 'DefaultActor.png'), + ('tags', self.EmbyServer.Utils.Translate(33220), 'DefaultTags.png'), + ('studios', "Studios", 'DefaultStudios.png'), + ('recentlyplayed', 'Recently played', 'DefaultMusicRecentlyPlayed.png'), + ('directors', 'Directors', 'DefaultDirector.png'), + ('countries', 'Countries', 'DefaultCountry.png') ], 'musicvideos': [ - ('all', None), - ('recent', helper.translate._(30256)), - ('inprogress', helper.translate._(30257)), - ('unwatched', helper.translate._(30258)) + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultMusicVideos.png'), + ('recentlyadded', self.EmbyServer.Utils.Translate(30256), 'DefaultRecentlyAddedMusicVideos.png'), + ('years', self.EmbyServer.Utils.Translate(33218), 'DefaultMusicYears.png'), + ('genres', "Genres", 'DefaultGenre.png'), + ('inprogress', self.EmbyServer.Utils.Translate(30257), 'DefaultInProgressShows.png'), + ('random', self.EmbyServer.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png'), + ('unwatched', self.EmbyServer.Utils.Translate(30258), 'OverlayUnwatched.png'), + ('artists', "Artists", 'DefaultMusicArtists.png'), + ('albums', "Albums", 'DefaultMusicAlbums.png'), + ('recentlyplayed', 'Recently played', 'DefaultMusicRecentlyPlayed.png') + ], + 'music': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultAddonMusic.png'), + ('years', self.EmbyServer.Utils.Translate(33218), 'DefaultMusicYears.png'), + ('genres', "Genres", 'DefaultMusicGenres.png'), + ('artists', "Artists", 'DefaultMusicArtists.png'), + ('albums', "Albums", 'DefaultMusicAlbums.png'), + ('recentlyaddedalbums', 'Recently added albums', 'DefaultMusicRecentlyAdded.png'), + ('recentlyaddedsongs', 'Recently added songs', 'DefaultMusicRecentlyAdded.png'), + ('recentlyplayed', 'Recently played', 'DefaultMusicRecentlyPlayed.png'), + ('randomalbums', 'Random albums', 'special://home/addons/plugin.video.emby-next-gen/resources/random.png'), + ('randomsongs', 'Random songs', 'special://home/addons/plugin.video.emby-next-gen/resources/random.png') ] } - #Make sure we have the kodi default folder in place - def verify_kodi_defaults(self): - node_path = self.Utils.translatePath("special://profile/library/video") - - if not xbmcvfs.exists(node_path): - try: - shutil.copytree(src=self.Utils.translatePath("special://xbmc/system/library/video"), dst=self.Utils.translatePath("special://profile/library/video")) - except Exception as error: - xbmcvfs.mkdir(node_path) - - for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): - filename = os.path.join(node_path, node, "index.xml") - - if xbmcvfs.exists(filename): - try: - xmlData = xml.etree.ElementTree.parse(filename).getroot() - except Exception as error: - self.LOG.error(error) - continue - - xmlData.set('order', str(17 + index)) - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filename) - - playlist_path = self.Utils.translatePath("special://profile/playlists/video") - - if not xbmcvfs.exists(playlist_path): - xbmcvfs.mkdirs(playlist_path) - - #Add entry to view table in emby database - def add_library(self, view): - with database.database.Database('emby') as embydb: - database.emby_db.EmbyDatabase(embydb.cursor).add_view(view['Id'], view['Name'], view['Media']) - - #Remove entry from view table in emby database - def remove_library(self, view_id): - with database.database.Database('emby') as embydb: - database.emby_db.EmbyDatabase(embydb.cursor).remove_view(view_id) - - self.delete_playlist_by_id(view_id) - self.delete_node_by_id(view_id) + def IconDownload(self, URL, FileID): + request = {'type': "GET", 'url': URL, 'params': {}} + Filename = self.EmbyServer.Utils.PathToFilenameReplaceSpecialCharecters(FileID)# + ".jpg") + FilePath = self.EmbyServer.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/temp/") + Filename - def get_libraries(self): - try: - if not self.server['connected']: - raise Exception("NotConnected") + if not xbmcvfs.exists(FilePath): + return self.EmbyServer.Utils.download_file_from_Embyserver(request, Filename, self.EmbyServer) - libraries = self.server['api'].get_media_folders()['Items'] - views = self.server['api'].get_views()['Items'] - except Exception as error: - raise IndexError("Unable to retrieve libraries: %s" % error) + return FilePath - libraries.extend([x for x in views if x['Id'] not in [y['Id'] for y in libraries]]) - return libraries + def add_favorites(self, index, view): + path = self.EmbyServer.Utils.translatePath("special://profile/library/video") + filepath = os.path.join(path, "emby_%s.xml" % view['Tag'].replace(" ", "")) - #Get the media folders. Add or remove them. Do not proceed if issue getting libraries - def get_views(self): try: - libraries = self.get_libraries() - except IndexError as error: - self.LOG.error(error) - return - - self.sync['SortedViews'] = [x['Id'] for x in libraries] - - for library in libraries: - if library['Type'] == 'Channel': - library['Media'] = "channels" + xmlData = xml.etree.ElementTree.parse(filepath).getroot() + except Exception: + if view['Media'] == 'episodes': + xmlData = xml.etree.ElementTree.Element('node', {'order': str(index), 'type': "folder"}) else: - library['Media'] = library.get('OriginalCollectionType', library.get('CollectionType', "mixed")) + xmlData = xml.etree.ElementTree.Element('node', {'order': str(index), 'type': "filter"}) - self.add_library(library) - - with database.database.Database('emby') as embydb: - views = database.emby_db.EmbyDatabase(embydb.cursor).get_views() - sorted_views = self.sync['SortedViews'] - whitelist = self.sync['Whitelist'] - removed = [] - - for view in views: - if view[0] not in sorted_views: - removed.append(view[0]) + xml.etree.ElementTree.SubElement(xmlData, 'icon').text = self.EmbyServer.Utils.translatePath("special://home/addons/plugin.video.emby-next-gen/resources/DefaultFavourites.png") + xml.etree.ElementTree.SubElement(xmlData, 'label') + xml.etree.ElementTree.SubElement(xmlData, 'match') + xml.etree.ElementTree.SubElement(xmlData, 'content') - if removed: - self.Utils.event('RemoveLibrary', {'Id': ','.join(removed)}) + label = xmlData.find('label') + label.text = view['Name'] + content = xmlData.find('content') + content.text = view['Media'] + match = xmlData.find('match') + match.text = "all" - for library_id in removed: - if library_id in sorted_views: - sorted_views.remove(library_id) + if view['Media'] != 'episodes': + for rule in xmlData.findall('.//value'): + if rule.text == view['Tag']: + break + else: + rule = xml.etree.ElementTree.SubElement(xmlData, 'rule', {'field': "tag", 'operator': "is"}) + xml.etree.ElementTree.SubElement(rule, 'value').text = view['Tag'] - if library_id in whitelist: - whitelist.remove(library_id) + self.node_all(xmlData) + else: + params = { + 'mode': "browse", + 'type': "Episode", + 'folder': 'FavEpisodes' + } + path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) + self.node_favepisodes(xmlData, path) - database.database.save_sync(self.sync) + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) - #Set up playlists, video nodes, window prop - def get_nodes(self): - node_path = self.Utils.translatePath("special://profile/library/video") - playlist_path = self.Utils.translatePath("special://profile/playlists/video") + def update_nodes(self): index = 0 - with database.database.Database('emby') as embydb: + #Favorites + for single in [{'Name': self.EmbyServer.Utils.Translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, {'Name': self.EmbyServer.Utils.Translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, {'Name': self.EmbyServer.Utils.Translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: + self.add_favorites(index, single) + index += 1 + + #Specific nodes + with database.database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: db = database.emby_db.EmbyDatabase(embydb.cursor) - for library in self.sync['Whitelist']: + #update nodes and playlist + for library in self.EmbyServer.Utils.SyncData['Whitelist']: library = library.replace('Mixed:', "") view = db.get_view(library) if view: - view = {'Id': library, 'Name': view[0], 'Tag': view[0], 'Media': view[1]} + view = {'LibraryId': library, 'Name': view[1], 'Tag': view[1], 'Media': view[2], "Icon": self.LibraryIcons[library], 'NameClean': self.EmbyServer.Utils.StringDecode(view[1]).replace(" ", ""), 'MediaClean': view[2].replace(" ", "")} + + if view['Media'] == 'music': + node_path = self.EmbyServer.Utils.translatePath("special://profile/library/music") + playlist_path = self.EmbyServer.Utils.translatePath("special://profile/playlists/music") + else: + node_path = self.EmbyServer.Utils.translatePath("special://profile/library/video") + playlist_path = self.EmbyServer.Utils.translatePath("special://profile/playlists/video") if view['Media'] == 'mixed': for media in ('movies', 'tvshows'): - temp_view = dict(view) - temp_view['Media'] = media - self.add_playlist(playlist_path, temp_view, True) - self.add_nodes(node_path, temp_view, True) + view['Media'] = media + view['MediaClean'] = media.replace(" ", "") + self.add_playlist(playlist_path, view, True) + self.add_nodes(node_path, view) index += 1 # Compensate for the duplicate. else: - if view['Media'] in ('movies', 'tvshows', 'musicvideos'): - self.add_playlist(playlist_path, view) - - if view['Media'] not in ('music'): - self.add_nodes(node_path, view) + self.add_playlist(playlist_path, view, False) + self.add_nodes(node_path, view) index += 1 - for single in [{'Name': helper.translate._('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, {'Name': helper.translate._('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, {'Name': helper.translate._('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: - self.add_single_node(node_path, index, "favorites", single) - index += 1 - + node_path = self.EmbyServer.Utils.translatePath("special://profile/library/video") + playlist_path = self.EmbyServer.Utils.translatePath("special://profile/playlists/video") self.window_nodes() + def window_nodes(self): + with database.database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: + libraries = database.emby_db.EmbyDatabase(embydb.cursor).get_views() + + for library in libraries: + if library[0] in self.LibraryIcons: + icon = self.LibraryIcons[library[0]] + else: + icon = None + + if not icon: + if library[2] == 'tvshows': + icon = 'DefaultTVShows.png' + elif library[2] == 'movies': + icon = 'DefaultMovies.png' + elif library[2] == 'musicvideos': + icon = 'DefaultMusicVideos.png' + elif library[2] == 'music': + icon = 'DefaultMusicVideos.png' + else: + icon = self.EmbyServer.Utils.translatePath("special://home/addons/plugin.video.emby-next-gen/resources/icon.png") + + view = {'LibraryId': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2], 'Icon': icon, 'NameClean': self.EmbyServer.Utils.StringDecode(library[1]).replace(" ", ""), 'MediaClean': library[2].replace(" ", "")} + + if library[0] in [x.replace('Mixed:', "") for x in self.EmbyServer.Utils.SyncData['Whitelist']]: # Synced libraries + if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): + if view['Media'] == 'mixed': + for media in ('movies', 'tvshows'): + temp_view = view + temp_view['Media'] = media + temp_view['MediaClean'] = media.replace(" ", "") + self.window_node(temp_view, False, True) + else: + self.window_node(view, False, False) + else: #Dynamic entry + self.window_node(view, True, False) + + #Leads to another listing of nodes + def window_node(self, view, dynamic, mixed): + NodeData = {} + + if dynamic: + params = { + 'mode': "browse", + 'type': view['Media'], + 'name': view['Name'].encode('utf-8'), + 'server': self.EmbyServer.server_id + } + + if view.get('LibraryId'): + params['id'] = view['LibraryId'] + + path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) + NodeData['title'] = "%s (%s)" % (view['Name'], self.EmbyServer.Data['auth.server-name']) + else: + if view['Media'] == 'music': + path = "library://music/emby_%s_%s" % (view['MediaClean'], view['NameClean']) + else: + path = "library://video/emby_%s_%s" % (view['MediaClean'], view['NameClean']) + + if mixed: + NodeData['title'] = "%s (%s)" % (view['Name'], view['Media']) + else: + NodeData['title'] = view['Name'] + + NodeData['path'] = path + NodeData['id'] = view['LibraryId'] + NodeData['type'] = view['Media'] + NodeData['icon'] = view['Icon'] + self.EmbyServer.Nodes.append(NodeData) + + def update_views(self): + ViewsData = self.EmbyServer.API.get_views()['Items'] + Total = len(ViewsData) + Counter = 1 + Progress = xbmcgui.DialogProgressBG() + Progress.create(self.EmbyServer.Utils.Translate('addon_name'), "Update views") + self.EmbyServer.Utils.SyncData['SortedViews'] = [x['Id'] for x in ViewsData] + + with database.database.Database(self.EmbyServer.Utils, 'emby', True) as embydb: + for library in ViewsData: + Percent = int(Counter / Total * 100) + Counter += 1 + Progress.update(Percent, message="Update views") + + if library['Type'] == 'Channel': + library['Media'] = "channels" + else: + library['Media'] = library.get('CollectionType', "mixed") + + database.emby_db.EmbyDatabase(embydb.cursor).add_view(library['Id'], library['Name'], library['Media'], self.EmbyServer.server_id) + + #Cache artwork + icon = self.APIHelper.get_artwork(library['Id'], 'Primary', None, [('Index', 0), ('api_key', self.EmbyServer.Data['auth.token'])], self.EmbyServer.Data['auth.server']) + iconpath = self.IconDownload(icon, "%s_%s" % (self.EmbyServer.Data['auth.server-name'], library['Id'])) + self.LibraryIcons[library['Id']] = iconpath + + Progress.close() + + def remove_library(self, view_id): + self.delete_playlist_by_id(view_id) + self.delete_node_by_id(view_id) + whitelist = self.EmbyServer.Utils.SyncData['Whitelist'] + + if view_id in whitelist: + whitelist.remove(view_id) + + self.EmbyServer.Utils.save_sync(self.EmbyServer.Utils.SyncData) + self.update_nodes() + #Create or update the xps file - def add_playlist(self, path, view, mixed=False): - filepath = os.path.join(path, "emby%s%s.xsp" % (view['Media'], view['Id'])) + def add_playlist(self, path, view, mixed): + filepath = os.path.join(path, "emby_%s.xsp" % (view['Name'].replace(" ", "_"))) try: xmlData = xml.etree.ElementTree.parse(filepath).getroot() @@ -217,147 +322,294 @@ def add_playlist(self, path, view, mixed=False): rule = xml.etree.ElementTree.SubElement(xmlData, 'rule', {'field': "tag", 'operator': "is"}) xml.etree.ElementTree.SubElement(rule, 'value').text = view['Tag'] - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) #Create or update the video node file - def add_nodes(self, path, view, mixed=False): - folder = os.path.join(path, "emby%s%s" % (view['Media'], view['Id'])) + def add_nodes(self, path, view): + folder = os.path.join(path, "emby_%s_%s" % (view['MediaClean'], view['NameClean'])) if not xbmcvfs.exists(folder): xbmcvfs.mkdir(folder) - self.node_index(folder, view, mixed) + #index.xml (root) + filepath = os.path.join(folder, "index.xml") - if view['Media'] == 'tvshows': - self.node_tvshow(folder, view) - else: - self.node(folder, view) + if not xbmcvfs.exists(filepath): + xmlData = xml.etree.ElementTree.Element('node', {'order': "0"}) + xml.etree.ElementTree.SubElement(xmlData, 'label').text = "EMBY: %s (%s)" % (view['Name'], view['Media']) - def add_single_node(self, path, index, item_type, view): - filepath = os.path.join(path, "emby_%s.xml" % view['Tag'].replace(" ", "")) + if view['Icon']: + Icon = view['Icon'] + else: + if view['Media'] == 'tvshows': + Icon = 'DefaultTVShows.png' + elif view['Media'] == 'movies': + Icon = 'DefaultMovies.png' + elif view['Media'] == 'musicvideos': + Icon = 'DefaultMusicVideos.png' + elif view['Media'] == 'music': + Icon = 'DefaultMusicVideos.png' + else: + Icon = self.EmbyServer.Utils.translatePath("special://home/addons/plugin.video.emby-next-gen/resources/icon.png") - try: - xmlData = xml.etree.ElementTree.parse(filepath).getroot() - except Exception: - xmlData = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) - xml.etree.ElementTree.SubElement(xmlData, 'label') - xml.etree.ElementTree.SubElement(xmlData, 'match') - xml.etree.ElementTree.SubElement(xmlData, 'content') + xml.etree.ElementTree.SubElement(xmlData, 'icon').text = Icon + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) - label = xmlData.find('label') - label.text = view['Name'] - content = xmlData.find('content') - content.text = view['Media'] - match = xmlData.find('match') - match.text = "all" + #specific nodes + for node in self.NODES[view['Media']]: + if node[1]: + xml_label = node[1] #Specific + else: + xml_label = view['Name'] #All - if view['Media'] != 'episodes': - for rule in xmlData.findall('.//value'): - if rule.text == view['Tag']: - break + if node[0] == "letter": + self.node_letter(view, folder, node) else: - rule = xml.etree.ElementTree.SubElement(xmlData, 'rule', {'field': "tag", 'operator': "is"}) - xml.etree.ElementTree.SubElement(rule, 'value').text = view['Tag'] + filepath = os.path.join(folder, "%s.xml" % node[0]) - if item_type == 'favorites' and view['Media'] == 'episodes': - path = self.window_browse(view, 'FavEpisodes') - self.node_favepisodes(xmlData, path) - else: - self.node_all(xmlData) + if not xbmcvfs.exists(filepath): + if node[0] == 'nextepisodes': + NodeType = 'folder' + else: + NodeType = 'filter' + + xmlData = xml.etree.ElementTree.Element('node', {'order': str(self.NODES[view['Media']].index(node)), 'type': NodeType}) + xml.etree.ElementTree.SubElement(xmlData, 'label').text = xml_label + xml.etree.ElementTree.SubElement(xmlData, 'match').text = "all" + xml.etree.ElementTree.SubElement(xmlData, 'content') + xml.etree.ElementTree.SubElement(xmlData, 'icon').text = node[2] + operator = "is" + field = "tag" + content = xmlData.find('content') + + if view['Media'] == "music": + if node[0] in ("genres", "artists"): + content.text = "artists" + field = "disambiguation" + + elif node[0] in ("years", "recentlyaddedalbums", "randomalbums", "albums"): + content.text = "albums" + field = "type" + + elif node[0] in ("recentlyaddedsongs", "randomsongs", "all", "recentlyplayed"): + content.text = "songs" + operator = "contains" + field = "comment" + else: + if node[0] in ("recentlyaddedepisodes", "inprogressepisodes", "recentlyplayedepisode"): + content.text = "episodes" + else: + content.text = view['Media'] + + for rule in xmlData.findall('.//value'): + if rule.text == view['Tag']: + break + else: + rule = xml.etree.ElementTree.SubElement(xmlData, 'rule', {'field': field, 'operator': operator}) + xml.etree.ElementTree.SubElement(rule, 'value').text = view['Tag'] - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + if node[0] == 'nextepisodes': + self.node_nextepisodes(xmlData, view['Name']) + else: + getattr(self, 'node_' + node[0])(xmlData) # get node function based on node type + + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + + def node_letter(self, View, folder, node): + Index = 1 + FolderPath = os.path.join(folder, "letter/") + + if not xbmcvfs.exists(FolderPath): + xbmcvfs.mkdir(FolderPath) + + #index.xml + FileName = os.path.join(FolderPath, "index.xml") + + if not xbmcvfs.exists(FileName): + xmlData = xml.etree.ElementTree.Element('node') + xmlData.set('order', '0') + xmlData.set('type', "folder") + xml.etree.ElementTree.SubElement(xmlData, "label").text = node[1] + xml.etree.ElementTree.SubElement(xmlData, 'icon').text = self.EmbyServer.Utils.translatePath(node[2]) + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), FileName) + + #0-9.xml + FileName = os.path.join(FolderPath, "0-9.xml") + + if not xbmcvfs.exists(FileName): + xmlData = xml.etree.ElementTree.Element('node') + xmlData.set('order', str(Index)) + xmlData.set('type', "filter") + xml.etree.ElementTree.SubElement(xmlData, "label").text = "0-9" + xml.etree.ElementTree.SubElement(xmlData, "match").text = "all" + + if View['Media'] == "music": + xml.etree.ElementTree.SubElement(xmlData, "content").text = "artists" + else: + xml.etree.ElementTree.SubElement(xmlData, "content").text = View['Media'] - #Create the root element - def node_root(self, root, index): - if root == 'main': - element = xml.etree.ElementTree.Element('node', {'order': str(index)}) - elif root == 'filter': - element = xml.etree.ElementTree.Element('node', {'order': str(index), 'type': "filter"}) - else: - element = xml.etree.ElementTree.Element('node', {'order': str(index), 'type': "folder"}) + xmlRule = xml.etree.ElementTree.SubElement(xmlData, "rule") + xmlRule.text = View['Tag'] - xml.etree.ElementTree.SubElement(element, 'icon').text = "special://home/addons/plugin.video.emby-next-gen/resources/icon.png" - return element + if View['Media'] == "music": + xmlRule.set('field', "disambiguation") + else: + xmlRule.set('field', "tag") - def node_index(self, folder, view, mixed=False): - filepath = os.path.join(folder, "index.xml") - index = self.sync['SortedViews'].index(view['Id']) + xmlRule.set('operator', "is") + xmlRule = xml.etree.ElementTree.SubElement(xmlData, "rule") - try: - xmlData = xml.etree.ElementTree.parse(filepath).getroot() - xmlData.set('order', str(index)) - except Exception: - xmlData = self.node_root('main', index) - xml.etree.ElementTree.SubElement(xmlData, 'label') + if View['Media'] == "music": + xmlRule.set('field', "artist") + else: + xmlRule.set('field', "sorttitle") + + xmlRule.set('operator', "startswith") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "0" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "1" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "2" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "3" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "4" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "5" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "6" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "7" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "8" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = "9" + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("&") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("Ä") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("Ö") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("Ü") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("!") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("(") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(")") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("@") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("#") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("$") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("^") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("*") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("-") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("=") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("+") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("{") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("}") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("[") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("]") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("?") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(":") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(";") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("'") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(",") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(".") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("<") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode(">") + xml.etree.ElementTree.SubElement(xmlRule, "value").text = self.EmbyServer.Utils.StringDecode("~") + xml.etree.ElementTree.SubElement(xmlData, 'order', {'direction': "ascending"}).text = "sorttitle" + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), FileName) + + #Alphabetically + FileNames = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] + + for FileName in FileNames: + Index += 1 + FilePath = os.path.join(FolderPath, "%s.xml" % FileName) + + if not xbmcvfs.exists(FilePath): + xmlData = xml.etree.ElementTree.Element('node') + xmlData.set('order', str(Index)) + xmlData.set('type', "filter") + xml.etree.ElementTree.SubElement(xmlData, "label").text = FileName + xml.etree.ElementTree.SubElement(xmlData, "match").text = "all" + + if View['Media'] == "music": + xml.etree.ElementTree.SubElement(xmlData, "content").text = "artists" + else: + xml.etree.ElementTree.SubElement(xmlData, "content").text = View['Media'] - label = xmlData.find('label') - label.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], helper.translate._(view['Media'])) - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + xmlRule = xml.etree.ElementTree.SubElement(xmlData, "rule") + xmlRule.text = View['Tag'] - def node(self, folder, view): - for node in self.NODES[view['Media']]: - xml_name = node[0] - xml_label = node[1] or view['Name'] - filepath = os.path.join(folder, "%s.xml" % xml_name) - self.add_node(self.NODES[view['Media']].index(node), filepath, view, xml_name, xml_label) + if View['Media'] == "music": + xmlRule.set('field', "disambiguation") + else: + xmlRule.set('field', "tag") - def node_tvshow(self, folder, view): - for node in self.NODES[view['Media']]: - xml_name = node[0] - xml_label = node[1] or view['Name'] - xml_index = self.NODES[view['Media']].index(node) - filepath = os.path.join(folder, "%s.xml" % xml_name) - - if xml_name == 'nextepisodes': - path = self.window_nextepisodes(view) - self.add_dynamic_node(xml_index, filepath, xml_name, xml_label, path) - else: - self.add_node(xml_index, filepath, view, xml_name, xml_label) + xmlRule.set('operator', "is") + xmlRule = xml.etree.ElementTree.SubElement(xmlData, "rule") + xmlRule.text = FileName - def add_node(self, index, filepath, view, node, name): - try: - xmlData = xml.etree.ElementTree.parse(filepath).getroot() - except Exception: - xmlData = self.node_root('filter', index) - xml.etree.ElementTree.SubElement(xmlData, 'label') - xml.etree.ElementTree.SubElement(xmlData, 'match') - xml.etree.ElementTree.SubElement(xmlData, 'content') + if View['Media'] == "music": + xmlRule.set('field', "artist") + else: + xmlRule.set('field', "sorttitle") - label = xmlData.find('label') - label.text = str(name) if isinstance(name, int) else name - content = xmlData.find('content') - content.text = view['Media'] - match = xmlData.find('match') - match.text = "all" + xmlRule.set('operator', "startswith") + xml.etree.ElementTree.SubElement(xmlData, 'order', {'direction': "ascending"}).text = "sorttitle" + self.EmbyServer.Utils.indent(xmlData, 0) + self.EmbyServer.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), FilePath) - for rule in xmlData.findall('.//value'): - if rule.text == view['Tag']: - break - else: - rule = xml.etree.ElementTree.SubElement(xmlData, 'rule', {'field': "tag", 'operator': "is"}) - xml.etree.ElementTree.SubElement(rule, 'value').text = view['Tag'] + def delete_playlist(self, path): + xbmcvfs.delete(path) + self.LOG.info("DELETE playlist %s" % path) - getattr(self, 'node_' + node)(xmlData) # get node function based on node type - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + #Remove all emby playlists + def delete_playlists(self): + path = self.EmbyServer.Utils.translatePath("special://profile/playlists/video/") + _, files = xbmcvfs.listdir(path) - def add_dynamic_node(self, index, filepath, node, name, path): - try: - xmlData = xml.etree.ElementTree.parse(filepath).getroot() - except Exception: - xmlData = self.node_root('folder', index) - xml.etree.ElementTree.SubElement(xmlData, 'label') - xml.etree.ElementTree.SubElement(xmlData, 'content') + for filename in files: + if filename.startswith('emby'): + self.delete_playlist(os.path.join(path, filename)) - label = xmlData.find('label') - label.text = name - getattr(self, 'node_' + node)(xmlData, path) - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filepath) + #Remove playlist based based on view_id + def delete_playlist_by_id(self, view_id): + path = self.EmbyServer.Utils.translatePath("special://profile/playlists/video/") + _, files = xbmcvfs.listdir(path) + + for filename in files: + if filename.startswith('emby') and filename.endswith('%s.xsp' % view_id): + self.delete_playlist(os.path.join(path, filename)) + + def delete_node(self, path): + xbmcvfs.delete(path) + self.LOG.info("DELETE node %s" % path) + + #Remove node and children files + def delete_nodes(self): + path = self.EmbyServer.Utils.translatePath("special://profile/library/video/") + dirs, files = xbmcvfs.listdir(path) + for filename in files: + if filename.startswith('emby'): + self.delete_node(os.path.join(path, filename)) + + for directory in dirs: + if directory.startswith('emby'): + _, files = xbmcvfs.listdir(os.path.join(path, directory)) + + for filename in files: + self.delete_node(os.path.join(path, directory, filename)) + + xbmcvfs.rmdir(os.path.join(path, directory)) + + def delete_node_by_id(self, view_id): + path = self.EmbyServer.Utils.translatePath("special://profile/library/video/") + dirs, files = xbmcvfs.listdir(path) + + for directory in dirs: + if directory.startswith('emby') and directory.endswith(view_id): + _, files = xbmcvfs.listdir(os.path.join(path, directory)) + + for filename in files: + self.delete_node(os.path.join(path, directory, filename)) + + xbmcvfs.rmdir(os.path.join(path, directory)) + + #Nodes def node_all(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": @@ -365,7 +617,47 @@ def node_all(self, root): else: xml.etree.ElementTree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - def node_nextepisodes(self, root, path): + def node_recentlyplayed(self, root): + for rule in root.findall('.//order'): + if rule.text == "lastplayed": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "lastplayed" + + def node_directors(self, root): + for rule in root.findall('.//order'): + if rule.text == "directors": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "directors" + + for rule in root.findall('.//group'): + rule.text = "directors" + break + else: + xml.etree.ElementTree.SubElement(root, 'group').text = "directors" + + def node_countries(self, root): + for rule in root.findall('.//order'): + if rule.text == "countries": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "countries" + + for rule in root.findall('.//group'): + rule.text = "countries" + break + else: + xml.etree.ElementTree.SubElement(root, 'group').text = "countries" + + def node_nextepisodes(self, root, LibraryName): + params = { + 'libraryname': LibraryName, + 'mode': "nextepisodes", + 'limit': 25 + } + path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) + for rule in root.findall('.//path'): rule.text = path break @@ -404,6 +696,45 @@ def node_actors(self, root): else: xml.etree.ElementTree.SubElement(root, 'group').text = "actors" + def node_artists(self, root): + for rule in root.findall('.//order'): + if rule.text == "artists": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "artists" + + for rule in root.findall('.//group'): + rule.text = "artists" + break + else: + xml.etree.ElementTree.SubElement(root, 'group').text = "artists" + + def node_albums(self, root): + for rule in root.findall('.//order'): + if rule.text == "albums": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "albums" + + for rule in root.findall('.//group'): + rule.text = "albums" + break + else: + xml.etree.ElementTree.SubElement(root, 'group').text = "albums" + + def node_studios(self, root): + for rule in root.findall('.//order'): + if rule.text == "title": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "title" + + for rule in root.findall('.//group'): + rule.text = "studios" + break + else: + xml.etree.ElementTree.SubElement(root, 'group').text = "studios" + def node_tags(self, root): for rule in root.findall('.//order'): if rule.text == "title": @@ -417,7 +748,7 @@ def node_tags(self, root): else: xml.etree.ElementTree.SubElement(root, 'group').text = "tags" - def node_recent(self, root): + def node_recentlyadded(self, root): for rule in root.findall('.//order'): if rule.text == "dateadded": break @@ -446,9 +777,6 @@ def node_inprogress(self, root): xml.etree.ElementTree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "lastplayed" - - - for rule in root.findall('.//limit'): rule.text = str(self.limit) break @@ -483,6 +811,24 @@ def node_unwatched(self, root): rule = xml.etree.ElementTree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) xml.etree.ElementTree.SubElement(rule, 'value').text = "0" + def node_unwatchedepisodes(self, root): + for rule in root.findall('.//order'): + if rule.text == "sorttitle": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = xml.etree.ElementTree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) + xml.etree.ElementTree.SubElement(rule, 'value').text = "0" + + content = root.find('content') + content.text = "episodes" + def node_sets(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": @@ -538,7 +884,7 @@ def node_recommended(self, root): rule = xml.etree.ElementTree.SubElement(root, 'rule', {'field': "rating", 'operator': "greaterthan"}) xml.etree.ElementTree.SubElement(rule, 'value').text = "7" - def node_recentepisodes(self, root): + def node_recentlyepisodes(self, root): for rule in root.findall('.//order'): if rule.text == "dateadded": break @@ -592,296 +938,82 @@ def node_favepisodes(self, root, path): else: xml.etree.ElementTree.SubElement(root, 'content').text = "episodes" - #Returns a list of sorted media folders based on the Emby views. - #Insert them in SortedViews and remove Views that are not in media folders - def order_media_folders(self, folders): - if not folders: - return folders - - sorted_views = list(self.sync['SortedViews']) - unordered = [x[0] for x in folders] - grouped = [x for x in unordered if x not in sorted_views] - - for library in grouped: - sorted_views.append(library) - - sorted_folders = [x for x in sorted_views if x in unordered] - return [folders[unordered.index(x)] for x in sorted_folders] - - #Just read from the database and populate based on SortedViews - #Setup the window properties that reflect the emby server views and more - def window_nodes(self): - self.window_clear() - self.window_clear('Emby.wnodes') - - with database.database.Database('emby') as embydb: - libraries = database.emby_db.EmbyDatabase(embydb.cursor).get_views() - - libraries = self.order_media_folders(libraries or []) - index = 0 - windex = 0 - - try: - self.media_folders = self.get_libraries() - except IndexError as error: - self.LOG.warning(error) - - for library in (libraries or []): - view = {'Id': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2]} - - if library[0] in [x.replace('Mixed:', "") for x in self.sync['Whitelist']]: # Synced libraries - if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed'): - if view['Media'] == 'mixed': - for media in ('movies', 'tvshows'): - for node in self.NODES[media]: - temp_view = dict(view) - temp_view['Media'] = media - temp_view['CleanName'] = view['Name'] - temp_view['Name'] = "%s (%s)" % (view['Name'], helper.translate._(media)) - self.window_node(index, temp_view, *node) - self.window_wnode(windex, temp_view, *node) - - # Add one to compensate for the duplicate. - index += 1 - windex += 1 - else: - for node in self.NODES[view['Media']]: - self.window_node(index, view, *node) - - if view['Media'] in ('movies', 'tvshows'): - self.window_wnode(windex, view, *node) - - if view['Media'] in ('movies', 'tvshows'): - windex += 1 - - elif view['Media'] == 'music': - self.window_node(index, view, 'music') - else: # Dynamic entry - if view['Media'] in ('homevideos', 'books', 'playlists'): - self.window_wnode(windex, view, 'browse') - windex += 1 - - self.window_node(index, view, 'browse') - - index += 1 - - for single in [{'Name': helper.translate._('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, {'Name': helper.translate._('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, {'Name': helper.translate._('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: - self.window_single_node(index, "favorites", single) - index += 1 - - self.Utils.window('Emby.nodes.total', str(index)) - self.Utils.window('Emby.wnodes.total', str(windex)) - - #Leads to another listing of nodes - def window_node(self, index, view, node=None, node_label=None): - if view['Media'] in ('homevideos', 'photos'): - path = self.window_browse(view, None if node in ('all', 'browse') else node) - elif node == 'nextepisodes': - path = self.window_nextepisodes(view) - elif node == 'music': - path = self.window_music() - elif node == 'browse': - path = self.window_browse(view) - else: - path = self.window_path(view, node) - - if node == 'music': - window_path = "ActivateWindow(Music,%s,return)" % path - elif node in ('browse', 'homevideos', 'photos'): - window_path = path - else: - window_path = "ActivateWindow(Videos,%s,return)" % path - - node_label = helper.translate._(node_label) if isinstance(node_label, int) else node_label - node_label = node_label or view['Name'] - - if node in ('all', 'music'): - window_prop = "Emby.nodes.%s" % index - self.Utils.window('%s.index' % window_prop, path.replace('all.xml', "")) # dir - self.Utils.window('%s.title' % window_prop, view['Name'].encode('utf-8')) - self.Utils.window('%s.content' % window_prop, path) - elif node == 'browse': - window_prop = "Emby.nodes.%s" % index - self.Utils.window('%s.title' % window_prop, view['Name'].encode('utf-8')) - else: - window_prop = "Emby.nodes.%s.%s" % (index, node) - self.Utils.window('%s.title' % window_prop, node_label.encode('utf-8')) - self.Utils.window('%s.content' % window_prop, path) - - self.Utils.window('%s.id' % window_prop, view['Id']) - self.Utils.window('%s.path' % window_prop, window_path) - self.Utils.window('%s.type' % window_prop, view['Media']) - self.window_artwork(window_prop, view['Id']) - - #Single destination node - def window_single_node(self, index, item_type, view): - path = "library://video/emby_%s.xml" % view['Tag'].replace(" ", "") - window_path = "ActivateWindow(Videos,%s,return)" % path - window_prop = "Emby.nodes.%s" % index - self.Utils.window('%s.title' % window_prop, view['Name']) - self.Utils.window('%s.path' % window_prop, window_path) - self.Utils.window('%s.content' % window_prop, path) - self.Utils.window('%s.type' % window_prop, item_type) - - #Similar to window_node, but does not contain music, musicvideos. - #Contains books, audiobooks - def window_wnode(self, index, view, node=None, node_label=None): - if view['Media'] in ('homevideos', 'photos', 'books', 'playlists'): - path = self.window_browse(view, None if node in ('all', 'browse') else node) - else: - path = self.window_path(view, node) - - if node in ('browse', 'homevideos', 'photos', 'books', 'playlists'): - window_path = path - else: - window_path = "ActivateWindow(Videos,%s,return)" % path - - node_label = helper.translate._(node_label) if isinstance(node_label, int) else node_label - node_label = node_label or view['Name'] - clean_title = view.get('CleanName', node_label) - - if node == 'all': - window_prop = "Emby.wnodes.%s" % index - self.Utils.window('%s.index' % window_prop, path.replace('all.xml', "")) # dir - self.Utils.window('%s.title' % window_prop, view['Name'].encode('utf-8')) - self.Utils.window('%s.cleantitle' % window_prop, clean_title.encode('utf-8')) - self.Utils.window('%s.content' % window_prop, path) - elif node == 'browse': - window_prop = "Emby.wnodes.%s" % index - self.Utils.window('%s.title' % window_prop, view['Name'].encode('utf-8')) - self.Utils.window('%s.cleantitle' % window_prop, clean_title.encode('utf-8')) - self.Utils.window('%s.content' % window_prop, path) - else: - window_prop = "Emby.wnodes.%s.%s" % (index, node) - self.Utils.window('%s.title' % window_prop, node_label.encode('utf-8')) - self.Utils.window('%s.cleantitle' % window_prop, clean_title.encode('utf-8')) - self.Utils.window('%s.content' % window_prop, path) - - self.Utils.window('%s.id' % window_prop, view['Id']) - self.Utils.window('%s.path' % window_prop, window_path) - self.Utils.window('%s.type' % window_prop, view['Media']) - self.window_artwork(window_prop, view['Id']) - self.LOG.debug("--[ wnode/%s/%s ] %s", index, self.Utils.window('%s.title' % window_prop), self.Utils.window('%s.artwork' % window_prop)) - - def window_artwork(self, prop, view_id): - if not self.server['connected']: - self.Utils.window('%s.artwork' % prop, clear=True) - elif self.server['connected'] and self.media_folders is not None: - for library in self.media_folders: - if library['Id'] == view_id and 'Primary' in library.get('ImageTags', {}): - artwork = helper.api.API(None, self.Utils, self.server['auth/server-address']).get_artwork(view_id, 'Primary') - self.Utils.window('%s.artwork' % prop, artwork) - break - else: - self.Utils.window('%s.artwork' % prop, clear=True) - - def window_path(self, view, node): - return "library://video/emby%s%s/%s.xml" % (view['Media'], view['Id'], node) - - def window_music(self): - return "library://music/" - - def window_nextepisodes(self, view): - params = { - 'id': view['Id'], - 'mode': "nextepisodes", - 'limit': self.limit - } - return "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - - def window_browse(self, view, node=None): - params = { - 'mode': "browse", - 'type': view['Media'] - } - - if view.get('Id'): - params['id'] = view['Id'] - - if node: - params['folder'] = node - - return "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - - #Clearing window prop setup for Views - def window_clear(self, name=None): - name = name or 'Emby.nodes' - total = int(self.Utils.window(name + '.total') or 0) - props = [ - - "index", "id", "path", "artwork", "title", "cleantitle", "content", "type" - "inprogress.content", "inprogress.title", - "inprogress.content", "inprogress.path", - "nextepisodes.title", "nextepisodes.content", - "nextepisodes.path", "unwatched.title", - "unwatched.content", "unwatched.path", - "recent.title", "recent.content", "recent.path", - "recentepisodes.title", "recentepisodes.content", - "recentepisodes.path", "inprogressepisodes.title", - "inprogressepisodes.content", "inprogressepisodes.path" - ] - - for i in range(total): - for prop in props: - self.Utils.window(name + '.%s.%s' % (str(i), prop), clear=True) - - for prop in props: - self.Utils.window(name + '.%s' % prop, clear=True) - - def delete_playlist(self, path): - xbmcvfs.delete(path) - self.LOG.info("DELETE playlist %s", path) - - #Remove all emby playlists - def delete_playlists(self): - path = self.Utils.translatePath("special://profile/playlists/video/") - _, files = xbmcvfs.listdir(path) - - for filename in files: - if filename.startswith('emby'): - self.delete_playlist(os.path.join(path, filename)) - - #Remove playlist based based on view_id - def delete_playlist_by_id(self, view_id): - path = self.Utils.translatePath("special://profile/playlists/video/") - _, files = xbmcvfs.listdir(path) + def node_randomalbums(self, root): + for rule in root.findall('.//order'): + if rule.text == "random": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" - for filename in files: - if filename.startswith('emby') and filename.endswith('%s.xsp' % view_id): - self.delete_playlist(os.path.join(path, filename)) + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + xml.etree.ElementTree.SubElement(root, 'limit').text = str(self.limit) - def delete_node(self, path): - xbmcvfs.delete(path) - self.LOG.info("DELETE node %s", path) + def node_randomsongs(self, root): + for rule in root.findall('.//order'): + if rule.text == "random": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" - #Remove node and children files - def delete_nodes(self): - path = self.Utils.translatePath("special://profile/library/video/") - dirs, files = xbmcvfs.listdir(path) + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + xml.etree.ElementTree.SubElement(root, 'limit').text = str(self.limit) - for filename in files: - if filename.startswith('emby'): - self.delete_node(os.path.join(path, filename)) + def node_recentlyaddedsongs(self, root): + for rule in root.findall('.//order'): + if rule.text == "dateadded": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - for directory in dirs: - if directory.startswith('emby'): - _, files = xbmcvfs.listdir(os.path.join(path, directory)) + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + xml.etree.ElementTree.SubElement(root, 'limit').text = str(self.limit) - for filename in files: - self.delete_node(os.path.join(path, directory, filename)) + def node_recentlyaddedalbums(self, root): + for rule in root.findall('.//order'): + if rule.text == "dateadded": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - xbmcvfs.rmdir(os.path.join(path, directory)) + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + xml.etree.ElementTree.SubElement(root, 'limit').text = str(self.limit) - #Remove node and children files based on view_id - def delete_node_by_id(self, view_id): - path = self.Utils.translatePath("special://profile/library/video/") - dirs, files = xbmcvfs.listdir(path) + def node_recentlyaddedepisodes(self, root): + for rule in root.findall('.//order'): + if rule.text == "dateadded": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" - for directory in dirs: - if directory.startswith('emby') and directory.endswith(view_id): - _, files = xbmcvfs.listdir(os.path.join(path, directory)) + for rule in root.findall('.//limit'): + rule.text = str(self.limit) + break + else: + xml.etree.ElementTree.SubElement(root, 'limit').text = str(self.limit) - for filename in files: - self.delete_node(os.path.join(path, directory, filename)) + for rule in root.findall('.//rule'): + if rule.attrib['field'] == 'playcount': + rule.find('value').text = "0" + break + else: + rule = xml.etree.ElementTree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + xml.etree.ElementTree.SubElement(rule, 'value').text = "0" - xbmcvfs.rmdir(os.path.join(path, directory)) + def node_recentlyplayedepisode(self, root): + for rule in root.findall('.//order'): + if rule.text == "lastplayed": + break + else: + xml.etree.ElementTree.SubElement(root, 'order', {'direction': "descending"}).text = "lastplayed" diff --git a/events.py b/events.py index f29648026..74b8e4172 100644 --- a/events.py +++ b/events.py @@ -1,993 +1,150 @@ # -*- coding: utf-8 -*- import json -import logging +import sys +import time try: from urlparse import parse_qsl - from urllib import urlencode except: - from urllib.parse import parse_qsl, urlencode + from urllib.parse import parse_qsl -import os -import sys import xbmc -import xbmcvfs import xbmcgui -import xbmcplugin -import xbmcaddon -import core.obj_ops -import core.listitem -import core.artwork -import emby.main -import emby.downloader -import database.database -import database.emby_db -import helper.translate -import helper.utils -import helper.api import helper.loghandler -import helper.xmls -import context class Events(): #Parse the parameters. Reroute to our service.py #where user is fully identified already def __init__(self, Parameter): - helper.loghandler.reset() - helper.loghandler.config() - #Emby.set_loghandler(helper.loghandler.LogHandler, logging.DEBUG) - self.LOG = logging.getLogger("EMBY.entrypoint.evens.Events") - self.Utils = helper.utils.Utils() - self.xmls = helper.xmls.Xmls(self.Utils) - self.CONTENT_TYPE = None - self.EMBY = None - self.LOG = logging.getLogger("EMBY.entrypoint.evens.Events") - self.Downloader = emby.downloader.Downloader(self.Utils) - self.DYNNODES = { - 'tvshows': [ - ('all', None), - ('RecentlyAdded', helper.translate._(30170)), - ('recentepisodes', helper.translate._(30175)), - ('InProgress', helper.translate._(30171)), - ('inprogressepisodes', helper.translate._(30178)), - ('nextepisodes', helper.translate._(30179)), - ('Genres', helper.translate._(135)), - ('Random', helper.translate._(30229)), - ('recommended', helper.translate._(30230)) - ], - 'movies': [ - ('all', None), - ('RecentlyAdded', helper.translate._(30174)), - ('InProgress', helper.translate._(30177)), - ('Boxsets', helper.translate._(20434)), - ('Favorite', helper.translate._(33168)), - ('FirstLetter', helper.translate._(33171)), - ('Genres', helper.translate._(135)), - ('Random', helper.translate._(30229)) - #('Recommended', helper.translate._(30230)) - ], - 'musicvideos': [ - ('all', None), - ('RecentlyAdded', helper.translate._(30256)), - ('InProgress', helper.translate._(30257)), - ('Unwatched', helper.translate._(30258)) - ], - 'homevideos': [ - ('all', None), - ('RecentlyAdded', helper.translate._(33167)), - ('InProgress', helper.translate._(33169)), - ('Favorite', helper.translate._(33168)) - ], - 'books': [ - ('all', None), - ('RecentlyAdded', helper.translate._(33167)), - ('InProgress', helper.translate._(33169)), - ('Favorite', helper.translate._(33168)) - ], - 'audiobooks': [ - ('all', None), - ('RecentlyAdded', helper.translate._(33167)), - ('InProgress', helper.translate._(33169)), - ('Favorite', helper.translate._(33168)) - ], - 'music': [ - ('all', None), - ('RecentlyAdded', helper.translate._(33167)), - ('Favorite', helper.translate._(33168)) - ] - } - - ServerOnline = False + params = dict(parse_qsl(Parameter[2][1:])) + Handle = int(Parameter[1]) + mode = params.get('mode') + self.server_id = params.get('server') - for i in range(60): - if self.Utils.window('emby_online.bool'): - ServerOnline = True - break + #Simple commands + if mode == 'deviceid': + self.event('reset_device_id', {}) + return - xbmc.sleep(500) + if mode == 'reset': + self.event('DatabaseReset', {}) + return - if not ServerOnline: - helper.loghandler.reset() + if mode == 'login': + self.event('ServerConnect', {'ServerId': None}) return - #Load server connection data - emby.main.Emby().set_state(self.Utils.window('emby.server.state.json')) + if mode == 'backup': + self.event('Backup', {'ServerId': None}) + return - for server in self.Utils.window('emby.server.states.json') or []: - emby.main.Emby(server).set_state(self.Utils.window('emby.server.%s.state.json' % server)) + if mode == 'restartservice': + self.event('restartservice', {}) + return - try: - params = dict(parse_qsl(Parameter[2][1:])) - except: - params = {} + if mode == 'patchmusic': + self.event('PatchMusic', {'Notification': True, 'ServerId': None}) + return - if 'content_type' in params: - self.Utils.window('emby.plugin.content.type', params['content_type']) - self.CONTENT_TYPE = params['content_type'] - else: - self.CONTENT_TYPE = self.Utils.window('emby.plugin.content.type') or None + if mode == 'settings': + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + return - Handle = int(Parameter[1]) - mode = params.get('mode') - server = params.get('server') + if mode == 'texturecache': + self.event('TextureCache', {}) + return - if server == 'None' or not server: - server = None + if mode == 'delete': + self.event('DeleteItem', {}) + return - self.LOG.warning("path: %s params: %s", Parameter[2], json.dumps(params, indent=4)) + self.LOG = helper.loghandler.LOG('EMBY.Events') + self.LOG.debug("path: %s params: %s" % (Parameter[2], json.dumps(params, indent=4))) -# if '/extrafanart' in Parameter[0]: -# emby_path = path -# emby_id = params.get('id') -# self.get_fanart(Handle, emby_id, emby_path, server) -# elif '/Extras' in Parameter[0] or '/VideoFiles' in Parameter[0]: -# emby_path = path -# emby_id = params.get('id') -# self.get_video_extras(emby_id, emby_path, server) - if mode == 'photoviewer': - xbmc.executebuiltin('ShowPicture(%s/emby/Items/%s/Images/Primary)' % (emby.main.Emby(server)['auth/server-address'], params['id'])) - elif mode == 'deviceid': - self.Utils.reset_device_id() - elif mode == 'reset': - database.database.reset() - elif mode == 'delete': - self.delete_item() - elif mode == 'refreshboxsets': - self.Utils.event('SyncLibrary', {'Id': "Boxsets:Refresh"}) - elif mode == 'nextepisodes': - self.get_next_episodes(Handle, params['id'], params['limit']) + #Events +# if mode == 'refreshboxsets': +# self.event('SyncLibrary', {'Id': "Boxsets: Refresh", 'Update': False, 'ServerId': self.server_id}) + if mode == 'nextepisodes': + self.EmbyQueryData('nextepisodes', {'Handle': Handle, 'libraryname': params.get('libraryname')}) + elif mode == 'photoviewer': + xbmc.executebuiltin('ShowPicture(http://127.0.0.1:57578/%s/Images/Primary)' % params['id']) elif mode == 'browse': - self.browse(Handle, params.get('type'), params.get('id'), params.get('folder'), server) - elif mode == 'synclib': - self.Utils.event('SyncLibrary', {'Id': params.get('id')}) - elif mode == 'updatelib': - self.Utils.event('SyncLibrary', {'Id': params.get('id'), 'Update': True}) - elif mode == 'repairlib': - self.Utils.event('RepairLibrary', {'Id': params.get('id')}) - elif mode == 'removelib': - self.Utils.event('RemoveLibrary', {'Id': params.get('id')}) + self.EmbyQueryData('browse', {'Handle': Handle, 'type': params.get('type'), 'id': params.get('id'), 'folder': params.get('folder'), 'name': params.get('name'), 'extra': params.get('extra')}) elif mode == 'repairlibs': - self.Utils.event('RepairLibrarySelection') + self.event('RepairLibrarySelection', {'ServerId': self.server_id}) elif mode == 'updatelibs': - self.Utils.event('SyncLibrarySelection') + self.event('SyncLibrarySelection', {'ServerId': self.server_id}) elif mode == 'removelibs': - self.Utils.event('RemoveLibrarySelection') + self.event('RemoveLibrarySelection', {'ServerId': self.server_id}) elif mode == 'addlibs': - self.Utils.event('AddLibrarySelection') - elif mode == 'connect': - self.Utils.event('EmbyConnect') + self.event('AddLibrarySelection', {'ServerId': self.server_id}) elif mode == 'addserver': - self.Utils.event('AddServer') - elif mode == 'login': - self.Utils.event('ServerConnect', {'Id': server}) + self.event('AddServer', {'ServerId': self.server_id}) elif mode == 'removeserver': - self.Utils.event('RemoveServer', {'Id': server}) - elif mode == 'settings': - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + self.event('RemoveServer', {'ServerId': self.server_id}) elif mode == 'adduser': - self.add_user(params.get('permanent') == 'true') - elif mode == 'checkupdate': - self.Utils.event('CheckUpdate') - elif mode == 'resetupdate': - self.Utils.event('ResetUpdate') - elif mode == 'updateserver': - self.Utils.event('UpdateServer') + self.event('AddUser', {'ServerId': self.server_id}) elif mode == 'thememedia': - self.get_themes() + self.event('SyncThemes', {'ServerId': self.server_id}) elif mode == 'managelibs': - self.manage_libraries(Handle) - elif mode == 'texturecache': - self.cache_artwork() - elif mode == 'backup': - self.backup() - elif mode == 'restartservice': - self.Utils.window('emby.restart.bool', True) - elif mode == 'patchmusic': - self.Utils.event('PatchMusic', {'Notification': True}) - elif mode == 'changelog': - pass - #self.changelog() + self.EmbyQueryData('manage_libraries', {'Handle': Handle}) elif mode == 'setssl': - self.Utils.event('SetServerSSL', {'Id': server}) + self.event('SetServerSSL', {'ServerId': self.server_id}) else: - self.listing(Handle) - - helper.loghandler.reset() - - def get_server(self, server=None): - try: - self.EMBY = emby.main.Emby(server).get_client() - except KeyError: # Server never loaded. - self.Utils.event('ServerConnect', {'Id': server}) - - monitor = xbmc.Monitor() - for i in range(300): - if server is None and self.Utils.window('emby_online.bool'): - emby.main.Emby().set_state(self.Utils.window('emby.server.state.json')) + self.EmbyQueryData('listing', {'Handle': Handle}) + + def event(self, method, data): + data = '"[%s]"' % json.dumps(data).replace('"', '\\"') + xbmc.executebuiltin('NotifyAll(plugin.video.emby-next-gen, %s, %s)' % (method, data)) + + def EmbyQueryData(self, method, Data): + QueryID = str(round(time.time() * 100000)) + Data['QueryId'] = QueryID + Data['ServerId'] = self.server_id + WindowID = xbmcgui.Window(10000) + Data = '"[%s]"' % json.dumps(Data).replace('"', '\\"') + xbmc.executebuiltin('NotifyAll(plugin.video.emby-next-gen, %s, %s)' % (method, Data)) + Ack = False + + for _ in range(10): + for _ in range(20): #wait for ack (2 seconds timeout) + data = WindowID.getProperty('emby_event_ack_%s' % QueryID) + + if data: + Ack = True break - if server is not None and server in self.Utils.window('emby.server.states.json') or []: - emby.main.Emby(server).set_state(self.Utils.window('emby.server.%s.state.json' % server)) + try: + if xbmc.Monitor().waitForAbort(0.1): + WindowID.clearProperty('emby_event_ack_%s' % QueryID) + break + except: break - if monitor.waitForAbort(0.1): - raise Exception('ShutDownRequested') - else: - self.LOG.error("Server %s is not online", server) - raise Exception('ServerOffline') - - self.EMBY = emby.main.Emby(server).get_client() - - #Display all emby nodes and dynamic entries when appropriate - def listing(self, Handle): - total = int(self.Utils.window('Emby.nodes.total') or 0) - sync = database.database.get_sync() - whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] - servers = database.database.get_credentials()['Servers'][1:] - - for i in range(total): - window_prop = "Emby.nodes.%s" % i - path = self.Utils.window('%s.index' % window_prop) - - if not path: - path = self.Utils.window('%s.content' % window_prop) or self.Utils.window('%s.path' % window_prop) - - label = self.Utils.window('%s.title' % window_prop) - node = self.Utils.window('%s.type' % window_prop) - artwork = self.Utils.window('%s.artwork' % window_prop) - view_id = self.Utils.window('%s.id' % window_prop) - contextData = [] - - if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed') and view_id not in whitelist: - label = "%s %s" % (label, helper.translate._(33166)) - contextData.append((helper.translate._(33123), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=synclib&id=%s)" % view_id)) - - if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music') and view_id in whitelist: - contextData.append((helper.translate._(33136), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=updatelib&id=%s)" % view_id)) - contextData.append((helper.translate._(33132), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=repairlib&id=%s)" % view_id)) - contextData.append((helper.translate._(33133), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=removelib&id=%s)" % view_id)) - - self.LOG.debug("--[ listing/%s/%s ] %s", node, label, path) - - if path: - if xbmc.getCondVisibility('Window.IsActive(Pictures)') and node in ('photos', 'homevideos'): - self.directory(Handle, label, path, artwork=artwork) - elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'music', 'audiobooks'): - self.directory(Handle, label, path, artwork=artwork, contextData=contextData) - elif xbmc.getCondVisibility('Window.IsActive(Music)') and node in 'music': - self.directory(Handle, label, path, artwork=artwork, contextData=contextData) - elif not xbmc.getCondVisibility('Window.IsActive(Videos) | Window.IsActive(Pictures) | Window.IsActive(Music)'): - self.directory(Handle, label, path, artwork=artwork) - - for server in servers: - contextData = [] - - if server.get('ManualAddress'): - contextData.append((helper.translate._(30500), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=setssl&server=%s)" % server['Id'])) - contextData.append((helper.translate._(33141), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=removeserver&server=%s)" % server['Id'])) - - if 'AccessToken' not in server: - self.directory(Handle, "%s (%s)" % (server['Name'], helper.translate._(30539)), "plugin://plugin.video.emby-next-gen/?mode=login&server=%s" % server['Id'], False, contextData=contextData) - else: - self.directory(Handle, server['Name'], "plugin://plugin.video.emby-next-gen/?mode=browse&server=%s" % server['Id'], contextData=contextData) - - self.directory(Handle, helper.translate._(33194), "plugin://plugin.video.emby-next-gen/?mode=managelibs", True) - self.directory(Handle, helper.translate._(33134), "plugin://plugin.video.emby-next-gen/?mode=addserver", False) - self.directory(Handle, helper.translate._(33054), "plugin://plugin.video.emby-next-gen/?mode=adduser", False) - self.directory(Handle, helper.translate._(5), "plugin://plugin.video.emby-next-gen/?mode=settings", False) - self.directory(Handle, helper.translate._(33059), "plugin://plugin.video.emby-next-gen/?mode=texturecache", False) - self.directory(Handle, helper.translate._(33058), "plugin://plugin.video.emby-next-gen/?mode=reset", False) - self.directory(Handle, helper.translate._(33192), "plugin://plugin.video.emby-next-gen/?mode=restartservice", False) - - if self.Utils.settings('backupPath'): - self.directory(Handle, helper.translate._(33092), "plugin://plugin.video.emby-next-gen/?mode=backup", False) - - self.directory(Handle, "Changelog", "plugin://plugin.video.emby-next-gen/?mode=changelog", False) - xbmcplugin.setContent(Handle, 'files') - xbmcplugin.endOfDirectory(Handle) - - #Add directory listitem. context should be a list of tuples [(label, action)*] - def directory(self, Handle, label, path, folder=True, artwork=None, fanart=None, contextData=None): - li = self.dir_listitem(label, path, artwork, fanart) - - if contextData: - li.addContextMenuItems(contextData) - - xbmcplugin.addDirectoryItem(Handle, path, li, folder) - return li - - def dir_listitem(self, label, path, artwork=None, fanart=None): - li = xbmcgui.ListItem(label, path=path) -# li.setThumbnailImage(artwork or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png") - li.setArt({"thumb": artwork or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png", "fanart": fanart or "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg", "landscape": artwork or fanart or "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg"}) -# li.setArt({"landscape": artwork or fanart or "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg"}) -# self.li.setArt({"thumb": artwork or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png", "icon" : 'DefaultFolder.png' }) - return li - - def manage_libraries(self, Handle): - self.directory(Handle, helper.translate._(33098), "plugin://plugin.video.emby-next-gen/?mode=refreshboxsets", False,) - self.directory(Handle, helper.translate._(33154), "plugin://plugin.video.emby-next-gen/?mode=addlibs", False) - self.directory(Handle, helper.translate._(33139), "plugin://plugin.video.emby-next-gen/?mode=updatelibs", False) - self.directory(Handle, helper.translate._(33140), "plugin://plugin.video.emby-next-gen/?mode=repairlibs", False) - self.directory(Handle, helper.translate._(33184), "plugin://plugin.video.emby-next-gen/?mode=removelibs", False) - self.directory(Handle, helper.translate._(33060), "plugin://plugin.video.emby-next-gen/?mode=thememedia", False) - self.directory(Handle, helper.translate._(33202), "plugin://plugin.video.emby-next-gen/?mode=patchmusic", False) - xbmcplugin.setContent(Handle, 'files') - xbmcplugin.endOfDirectory(Handle) - - def changelogTOBEDONE(self): - return - - #Browse content dynamically - def browse(self, Handle, media, view_id=None, folder=None, server_id=None): - self.LOG.info("--[ v:%s/%s ] %s", view_id, media, folder) - self.get_server(server_id) - folder = folder.lower() if folder else None - - if folder is None and media in ('homevideos', 'movies', 'books', 'audiobooks'): - return self.browse_subfolders(Handle, media, view_id, server_id) - - if folder and folder == 'firstletter': - return self.browse_letters(Handle, media, view_id, server_id) - - if view_id: - view = self.EMBY['api'].get_item(view_id) - xbmcplugin.setPluginCategory(Handle, view['Name']) - - content_type = "files" - - if media in ('tvshows', 'seasons', 'episodes', 'movies', 'musicvideos', 'songs', 'albums'): - content_type = media - elif media in ('homevideos', 'photos'): - content_type = "images" - elif media in ('books', 'audiobooks'): - content_type = "videos" - elif media == 'music': - content_type = "artists" - if folder == 'recentlyadded': - listing = self.EMBY['api'].get_recently_added(None, view_id, None) - elif folder == 'genres': - listing = self.EMBY['api'].get_genres(view_id) - elif media == 'livetv': - listing = self.EMBY['api'].get_channels() - elif folder == 'unwatched': - listing = self.Downloader.get_filtered_section(view_id, None, None, None, None, None, ['IsUnplayed'], None, server_id) - elif folder == 'favorite': - listing = self.Downloader.get_filtered_section(view_id, None, None, None, None, None, ['IsFavorite'], None, server_id) - elif folder == 'inprogress': - listing = self.Downloader.get_filtered_section(view_id, None, None, None, None, None, ['IsResumable'], None, server_id) - elif folder == 'boxsets': - listing = self.Downloader.get_filtered_section(view_id, self.get_media_type('boxsets'), None, True, None, None, None, None, server_id) - elif folder == 'random': - listing = self.Downloader.get_filtered_section(view_id, self.get_media_type(content_type), 25, True, "Random", None, None, None, server_id) - elif (folder or "").startswith('firstletter-'): - listing = self.Downloader.get_filtered_section(view_id, self.get_media_type(content_type), None, None, None, None, None, {'NameStartsWith': folder.split('-')[1]}, server_id) - elif (folder or "").startswith('genres-'): - listing = self.Downloader.get_filtered_section(view_id, self.get_media_type(content_type), None, None, None, None, None, {'GenreIds': folder.split('-')[1]}, server_id) - elif folder == 'favepisodes': - listing = self.Downloader.get_filtered_section(None, self.get_media_type(content_type), 25, None, None, None, ['IsFavorite'], None, server_id) - elif media == 'homevideos': - listing = self.Downloader.get_filtered_section(folder or view_id, self.get_media_type(content_type), None, False, None, None, None, None, server_id) - elif media == 'movies': - listing = self.Downloader.get_filtered_section(folder or view_id, self.get_media_type(content_type), None, True, None, None, None, None, server_id) - elif media in ('boxset', 'library'): - listing = self.Downloader.get_filtered_section(folder or view_id, None, None, True, None, None, None, None, server_id) - elif media == 'episodes': - listing = self.Downloader.get_filtered_section(folder or view_id, self.get_media_type(content_type), None, True, None, None, None, None, server_id) - elif media == 'boxsets': - listing = self.Downloader.get_filtered_section(folder or view_id, None, None, False, None, None, ['Boxsets'], None, server_id) - elif media == 'tvshows': - listing = self.Downloader.get_filtered_section(folder or view_id, self.get_media_type(content_type), None, True, None, None, None, None, server_id) - elif media == 'seasons': - listing = self.EMBY['api'].get_seasons(folder) - elif media == 'playlists': - listing = self.Downloader.get_filtered_section(folder or view_id, None, None, False, None, None, None, None, server_id, True) - elif media != 'files': - listing = self.Downloader.get_filtered_section(folder or view_id, self.get_media_type(content_type), None, False, None, None, None, None, server_id) - else: - listing = self.Downloader.get_filtered_section(folder or view_id, None, None, False, None, None, None, None, server_id) - - if listing: - listitems = core.listitem.ListItem(self.EMBY['auth/server-address'], self.Utils) - list_li = [] - listing = listing if isinstance(listing, list) else listing.get('Items', []) - - for item in listing: - if self.Utils.window('emby_should_stop.bool'): - return - - li = xbmcgui.ListItem() - li.setProperty('embyid', item['Id']) - li.setProperty('embyserver', server_id) - li = listitems.set(item, li, None, False, None) - - if item.get('IsFolder'): - params = { - 'id': view_id or item['Id'], - 'mode': "browse", - 'type': self.get_folder_type(item, media) or media, - 'folder': item['Id'], - 'server': server_id - } - - path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - contextData = [] - - if item['Type'] in ('Series', 'Season', 'Playlist'): - contextData.append(("Play", "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))) - - if item['UserData']['Played']: - contextData.append((helper.translate._(16104), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) - else: - contextData.append((helper.translate._(16103), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) - - li.addContextMenuItems(contextData) - list_li.append((path, li, True)) - elif item['Type'] == 'Genre': - params = { - 'id': view_id or item['Id'], - 'mode': "browse", - 'type': self.get_folder_type(item, media) or media, - 'folder': 'genres-%s' % item['Id'], - 'server': server_id - } - - path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - list_li.append((path, li, True)) - else: - if item['Type'] == 'Photo': - path = "plugin://plugin.video.emby-next-gen/?mode=photoviewer&id=%s" % item['Id'] - li.setProperty('path', path) - elif item['Type'] not in ('PhotoAlbum', 'Photo'): - path = "" - - if item['Type'] == "MusicVideo": - Type = "musicvideo" - elif item['Type'] == "Movie": - Type = "movie" - elif item['Type'] == "Episode": - Type = "tvshow" - elif item['Type'] == "Audio": - Type = "audio" - elif item['Type'] == "Video": - Type = "video" - elif item['Type'] == "Trailer": - Type = "trailer" - elif item['Type'] == "TvChannel": - Type = "tvchannel" - path = "http://127.0.0.1:57578/livetv/%s-stream.ts" % item['Id'] - else: - return - - if not path: - if 'MediaSources' in item: - FilenameURL = self.Utils.PathToFilenameReplaceSpecialCharecters(item['Path']) - - if len(item['MediaSources'][0]['MediaStreams']) >= 1: - path = "http://127.0.0.1:57578/%s/%s-%s-%s-stream-%s" % (Type, item['Id'], item['MediaSources'][0]['Id'], item['MediaSources'][0]['MediaStreams'][0]['BitRate'], FilenameURL) - else: - path = "http://127.0.0.1:57578/%s/%s-%s-stream-%s" % (Type, item['Id'], item['MediaSources'][0]['Id'], FilenameURL) - - self.Utils.window('emby_DynamicItem_' + path, item['Id']) - - li.setProperty('path', path) - contextData = [(helper.translate._(13412), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))] - - if item['UserData']['Played']: - contextData.append((helper.translate._(16104), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) - else: - contextData.append((helper.translate._(16103), "RunPlugin(plugin://plugin.video.emby-next-gen/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) - - li.addContextMenuItems(contextData) - - list_li.append((li.getProperty('path'), li, False)) - - xbmcplugin.addDirectoryItems(Handle, list_li, len(list_li)) - - if content_type == 'images': - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_TITLE) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_DATE) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RATING) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) - - if media == 'playlists': - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_TITLE) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_DATE) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RATING) - xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) - - xbmcplugin.setContent(Handle, content_type) - xbmcplugin.endOfDirectory(Handle) - - #Display submenus for emby views - def browse_subfolders(self, Handle, media, view_id, server_id=None): - self.get_server(server_id) - view = self.EMBY['api'].get_item(view_id) - xbmcplugin.setPluginCategory(Handle, view['Name']) - - for node in self.DYNNODES[media]: - params = { - 'id': view_id, - 'mode': "browse", - 'type': media, - 'folder': view_id if node[0] == 'all' else node[0], - 'server': server_id - } - path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - self.directory(Handle, node[1] or view['Name'], path) - - xbmcplugin.setContent(Handle, 'files') - xbmcplugin.endOfDirectory(Handle) - - #Display letters as options - def browse_letters(self, Handle, media, view_id, server_id=None): - letters = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ" - self.get_server(server_id) - view = self.EMBY['api'].get_item(view_id) - xbmcplugin.setPluginCategory(Handle, view['Name']) - - for node in letters: - params = { - 'id': view_id, - 'mode': "browse", - 'type': media, - 'folder': 'firstletter-%s' % node, - 'server': server_id - } - path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) - self.directory(Handle, node, path) - - xbmcplugin.setContent(Handle, 'files') - xbmcplugin.endOfDirectory(Handle) - - def get_folder_type(self, item, content_type=None): - media = item['Type'] - - if media == 'Series': - return "seasons" - elif media == 'Season': - return "episodes" - elif media == 'BoxSet': - return "boxset" - elif media == 'MusicArtist': - return "albums" - elif media == 'MusicAlbum': - return "songs" - elif media == 'CollectionFolder': - return item.get('CollectionType', 'library') - elif media == 'Folder' and content_type == 'music': - return "albums" - else: - return None - - def get_media_type(self, media): - if media == 'movies': - return "Movie,BoxSet" - elif media == 'homevideos': - return "Video,Folder,PhotoAlbum,Photo" - elif media == 'episodes': - return "Episode" - elif media == 'boxsets': - return "BoxSet" - elif media == 'tvshows': - return "Series" - elif media == 'music': - return "MusicArtist,MusicAlbum,Audio" - else: - return None - - #Get extra fanart for listitems. This is called by skinhelper. - #Images are stored locally, due to the Kodi caching system - def get_fanart(self, Handle, item_id, path, server_id=None): - if not item_id and 'plugin.video.emby-next-gen' in path: - item_id = path.split('/')[-2] - - if not item_id: - return - - self.LOG.info("[ extra fanart ] %s", item_id) - self.get_server(server_id) - objects = core.obj_ops.Objects(self.Utils) - list_li = [] - directory = self.Utils.translatePath("special://thumbnails/emby/%s/" % item_id) - - if not xbmcvfs.exists(directory): - xbmcvfs.mkdirs(directory) - item = self.EMBY['api'].get_item(item_id) - obj = objects.map(item, 'Artwork') - backdrops = helper.api.API(item, self.Utils, self.EMBY['auth/server-address']).get_all_artwork(obj) - tags = obj['BackdropTags'] - - for index, backdrop in enumerate(backdrops): - tag = tags[index] - fanart = os.path.join(directory, "fanart%s.jpg" % tag) - li = xbmcgui.ListItem(tag, path=fanart) - xbmcvfs.copy(backdrop, fanart) - list_li.append((fanart, li, False)) - else: - self.LOG.debug("cached backdrop found") - dirs, files = xbmcvfs.listdir(directory) - - for filename in files: - fanart = os.path.join(directory, filename) - li = xbmcgui.ListItem(filename, path=fanart) - list_li.append((fanart, li, False)) - - xbmcplugin.addDirectoryItems(Handle, list_li, len(list_li)) - xbmcplugin.endOfDirectory(Handle) - - #Returns the video files for the item as plugin listing, can be used - #to browse actual files or video extras, etc - def get_video_extras(self, item_id, path, server_id=None): - if not item_id and 'plugin.video.emby-next-gen' in path: - item_id = path.split('/')[-2] - - if not item_id: - return - - self.get_server(server_id) - - #Only for synced content - def get_next_episodes(self, Handle, item_id, limit): - with database.database.Database('emby') as embydb: - - db = database.emby_db.EmbyDatabase(embydb.cursor) - library = db.get_view_name(item_id) - - if not library: - return - - result = helper.utils.JSONRPC('VideoLibrary.GetTVShows').execute({ - 'sort': {'order': "descending", 'method': "lastplayed"}, - 'filter': { - 'and': [ - {'operator': "true", 'field': "inprogress", 'value': ""}, - {'operator': "is", 'field': "tag", 'value': "%s" % library} - ]}, - 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] - }) - - try: - items = result['result']['tvshows'] - except (KeyError, TypeError): - return - - list_li = [] - - for item in items: - if self.Utils.settings('ignoreSpecialsNextEpisodes.bool'): - params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': { - 'and': [ - {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - {'operator': "greaterthan", 'field': "season", 'value': "0"}] - }, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - else: - params = { - 'tvshowid': item['tvshowid'], - 'sort': {'method': "episode"}, - 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"}, - 'properties': [ - "title", "playcount", "season", "episode", "showtitle", - "plot", "file", "rating", "resume", "tvshowid", "art", - "streamdetails", "firstaired", "runtime", "writer", - "dateadded", "lastplayed" - ], - 'limits': {"end": 1} - } - - result = helper.utils.JSONRPC('VideoLibrary.GetEpisodes').execute(params) + if not Ack: + if xbmc.Monitor().waitForAbort(1): #retry send + break - try: - episodes = result['result']['episodes'] - except (KeyError, TypeError): - pass + xbmc.executebuiltin('NotifyAll(plugin.video.emby-next-gen, %s, %s)' % (method, Data)) else: - for episode in episodes: - li = self.create_listitem(episode) - list_li.append((episode['file'], li)) - - if len(list_li) == limit: break - xbmcplugin.addDirectoryItems(Handle, list_li, len(list_li)) - xbmcplugin.setContent(Handle, 'episodes') - xbmcplugin.endOfDirectory(Handle) - - #Listitem based on jsonrpc items - def create_listitem(self, item): - title = item['title'] - label2 = "" - li = xbmcgui.ListItem(title) - li.setProperty('IsPlayable', "true") - - metadata = { - 'Title': title, - 'duration': str(item['runtime']/60), - 'Plot': item['plot'], - 'Playcount': item['playcount'] - } - - if "showtitle" in item: - metadata['TVshowTitle'] = item['showtitle'] - label2 = item['showtitle'] - - if "episodeid" in item: - # Listitem of episode - metadata['mediatype'] = "episode" - metadata['dbid'] = item['episodeid'] - - if "episode" in item: - episode = item['episode'] - metadata['Episode'] = episode - - if "season" in item: - season = item['season'] - metadata['Season'] = season - - if season and episode: - episodeno = "s%.2de%.2d" % (season, episode) - li.setProperty('episodeno', episodeno) - label2 = "%s - %s" % (label2, episodeno) if label2 else episodeno - - if "firstaired" in item: - metadata['Premiered'] = item['firstaired'] - - if "rating" in item: - metadata['Rating'] = str(round(float(item['rating']), 1)) - - if "director" in item: - metadata['Director'] = " / ".join(item['director']) - - if "writer" in item: - metadata['Writer'] = " / ".join(item['writer']) - - if "cast" in item: - cast = [] - castandrole = [] - for person in item['cast']: - name = person['name'] - cast.append(name) - castandrole.append((name, person['role'])) - metadata['Cast'] = cast - metadata['CastAndRole'] = castandrole - - li.setLabel2(label2) - li.setInfo(type="Video", infoLabels=metadata) - li.setProperty('resumetime', str(item['resume']['position'])) - li.setProperty('totaltime', str(item['resume']['total'])) - li.setArt(item['art']) -# li.setArt({"thumb": item['art'].get('thumb',''), "icon" : 'DefaultFolder.png' }) -# li.setThumbnailImage(item['art'].get('thumb','')) -# self.li.setArt({"thumb": item['art'].get('thumb',''), "icon" : 'DefaultFolder.png' }) -# li.setIconImage('DefaultTVShows.png') - li.setProperty('dbid', str(item['episodeid'])) - li.setProperty('fanart_image', item['art'].get('tvshow.fanart', '')) - - for key, value in list(item['streamdetails'].items()): - for stream in value: - li.addStreamInfo(key, stream) - - return li - - #Add or remove users from the default server session - #permanent=True from the add-on settings - def add_user(self, permanent=False): - if not self.Utils.window('emby_online.bool'): + if not Ack: + WindowID.clearProperty('emby_event_ack_%s' % QueryID) return - self.get_server() - session = self.EMBY['api'].get_device(self.EMBY['config/app.device_id']) - hidden = None if self.Utils.settings('addUsersHidden.bool') else False - users = self.EMBY['api'].get_users(False, hidden) - - for user in users: - if user['Id'] == session[0]['UserId']: - users.remove(user) - break - + #Wait for Data while True: - session = self.EMBY['api'].get_device(self.EMBY['config/app.device_id']) - additional = current = session[0]['AdditionalUsers'] - add_session = True - - if permanent: - perm_users = self.Utils.settings('addUsers').split(',') if self.Utils.settings('addUsers') else [] - current = [] - - for user in users: - for perm_user in perm_users: - - if user['Id'] == perm_user: - current.append({'UserName': user['Name'], 'UserId': user['Id']}) + data = WindowID.getProperty('emby_event_%s' % QueryID) - result = self.Utils.dialog("select", helper.translate._(33061), [helper.translate._(33062), helper.translate._(33063)] if current else [helper.translate._(33062)]) - - if result < 0: + if data: break - if not result: # Add user - eligible = [x for x in users if x['Id'] not in [current_user['UserId'] for current_user in current]] - resp = self.Utils.dialog("select", helper.translate._(33064), [x['Name'] for x in eligible]) - - if resp < 0: - break - - user = eligible[resp] - - if permanent: - perm_users.append(user['Id']) - self.Utils.settings('addUsers', ','.join(perm_users)) - - if user['Id'] in [current_user['UserId'] for current_user in additional]: - add_session = False - - if add_session: - self.Utils.event('AddUser', {'Id': user['Id'], 'Add': True}) - - self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33067), user['Name']), icon="{emby}", time=1000, sound=False) - else: # Remove user - resp = self.Utils.dialog("select", helper.translate._(33064), [x['UserName'] for x in current]) - - if resp < 0: - break - - user = current[resp] - - if permanent: - perm_users.remove(user['UserId']) - self.Utils.settings('addUsers', ','.join(perm_users)) - - if add_session: - self.Utils.event('AddUser', {'Id': user['UserId'], 'Add': False}) - - self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33066), user['UserName']), icon="{emby}", time=1000, sound=False) - - #Add theme media locally, via strm. This is only for tv tunes. - #If another script is used, adjust this code - def get_themes(self): - library = self.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/library") - play = self.Utils.settings('useDirectPaths') == "1" - - if not xbmcvfs.exists(library + '/'): - xbmcvfs.mkdir(library) - - if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): - tvtunes = xbmcaddon.Addon(id="script.tvtunes") - tvtunes.setSetting('custom_path_enable', "true") - tvtunes.setSetting('custom_path', library) - self.LOG.info("TV Tunes custom path is enabled and set.") - elif xbmc.getCondVisibility('System.HasAddon(service.tvtunes)'): - tvtunes = xbmcaddon.Addon(id="service.tvtunes") - tvtunes.setSetting('custom_path_enable', "true") - tvtunes.setSetting('custom_path', library) - self.LOG.info("TV Tunes custom path is enabled and set.") - else: - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33152)) - return - - with database.database.Database('emby') as embydb: - all_views = database.emby_db.EmbyDatabase(embydb.cursor).get_views() - views = [x[0] for x in all_views if x[2] in ('movies', 'tvshows', 'mixed')] - - self.get_server() - items = {} - - for view in views: - for result in self.Downloader.get_items(view, params={'HasThemeVideo': True}): - for item in result['Items']: - folder = self.Utils.normalize_string(item['Name']) - items[item['Id']] = folder - - for result in self.Downloader.get_items(view, params={'HasThemeSong': True}): - for item in result['Items']: - folder = self.Utils.normalize_string(item['Name']) - items[item['Id']] = folder - - for item in items: - nfo_path = os.path.join(library, items[item]) - nfo_file = os.path.join(nfo_path, "tvtunes.nfo") - - if not xbmcvfs.exists(nfo_path): - xbmcvfs.mkdir(nfo_path) - - themes = self.EMBY['api'].get_themes(item) - paths = [] - - for theme in themes['ThemeVideosResult']['Items'] + themes['ThemeSongsResult']['Items']: - if play: - paths.append(theme['MediaSources'][0]['Path']) - else: - paths.append(self.Utils.direct_url(theme)) - - self.xmls.tvtunes_nfo(nfo_file, paths) - - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33153), icon="{emby}", time=1000, sound=False) - - #Delete keymap action - def delete_item(self): - context.Context(delete=True) - - #Emby backup - def backup(self): - path = self.Utils.settings('backupPath') - folder_name = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], xbmc.getInfoLabel('System.Date(dd-mm-yy)')) - folder_name = self.Utils.dialog("input", heading=helper.translate._(33089), default=folder_name) - - if not folder_name: - return - - backup = os.path.join(path, folder_name) - - if xbmcvfs.exists(backup + '/'): - if not self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33090)): - return backup() - - self.Utils.delete_folder(backup) - - addon_data = self.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen") - destination_data = os.path.join(backup, "addon_data", "plugin.video.emby-next-gen") - destination_databases = os.path.join(backup, "Database") - - if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases): - self.LOG.info("Unable to create all directories") - self.Utils.dialog("notification", heading="{emby}", icon="{emby}", message=helper.translate._(33165), sound=False) - return - - self.Utils.copytree(addon_data, destination_data) - db = self.Utils.translatePath("special://database/") - dirs, files = xbmcvfs.listdir(db) - - for Temp in files: - if 'MyVideos' in Temp: - xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) - self.LOG.info("copied %s", Temp) - elif 'emby' in Temp: - xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) - self.LOG.info("copied %s", Temp) - elif 'MyMusic' in Temp: - xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) - self.LOG.info("copied %s", Temp) - - self.LOG.info("backup completed") - self.Utils.dialog("ok", heading="{emby}", line1="%s %s" % (helper.translate._(33091), backup)) + if xbmc.Monitor().waitForAbort(0.1): + break - def cache_artwork(self): - core.artwork.Artwork(None, self.Utils).cache_textures() + WindowID.clearProperty('emby_event_%s' % QueryID) if __name__ == "__main__": Events(sys.argv) diff --git a/helper/api.py b/helper/api.py index 02198dae7..7be025a50 100644 --- a/helper/api.py +++ b/helper/api.py @@ -5,38 +5,24 @@ from urllib.parse import urlencode class API(): - def __init__(self, item, Utils, server=None): - ''' Get item information in special cases. - server is the server address, provide if your functions requires it. - ''' + def __init__(self, Utils): self.Utils = Utils - self.item = item - self.server = server - self.verify_ssl = True - - if server and server.startswith('https') and not self.Utils.settings('sslverify.bool'): - self.verify_ssl = False def get_playcount(self, played, playcount): - ''' Convert Emby played/playcount into - the Kodi equivalent. The playcount is tied to the watch status. - ''' return (playcount or 1) if played else None - def get_actors(self): + def get_actors(self, people): cast = [] + self.get_people_artwork(people) - if 'People' in self.item: - self.get_people_artwork(self.item['People']) - - for person in self.item['People']: - if person['Type'] == "Actor": - cast.append({ - 'name': person['Name'], - 'role': person.get('Role', "Unknown"), - 'order': len(cast) + 1, - 'thumbnail': person['imageurl'] - }) + for person in people: + if person['Type'] == "Actor": + cast.append({ + 'name': person['Name'], + 'role': person.get('Role', "Unknown"), + 'order': len(cast) + 1, + 'thumbnail': person['imageurl'] + }) return cast @@ -47,7 +33,7 @@ def media_streams(self, video, audio, subtitles): 'subtitle': subtitles or [] } - def video_streams(self, tracks, container=None): + def video_streams(self, tracks, container, item): if container: container = container.split(',')[0] @@ -57,7 +43,7 @@ def video_streams(self, tracks, container=None): 'profile': track.get('Profile', "").lower(), 'height': track.get('Height'), 'width': track.get('Width'), - '3d': self.item.get('Video3DFormat'), + '3d': item.get('Video3DFormat'), 'aspect': 1.85 }) @@ -72,14 +58,14 @@ def video_streams(self, tracks, container=None): track['codec'] = "avc1" try: - width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':') + width, height = item.get('AspectRatio', track.get('AspectRatio', "0")).split(':') track['aspect'] = round(float(width) / float(height), 6) except (ValueError, ZeroDivisionError): if track['width'] and track['height']: track['aspect'] = round(float(track['width'] / track['height']), 6) - track['duration'] = self.get_runtime() + track['duration'] = self.get_runtime(item) return tracks @@ -99,21 +85,20 @@ def audio_streams(self, tracks): return tracks - def get_runtime(self): + def get_runtime(self, item): try: - runtime = self.item['RunTimeTicks'] / 10000000.0 + runtime = item['RunTimeTicks'] / 10000000.0 except KeyError: - runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0 + runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0 return runtime - @classmethod - def adjust_resume(cls, resume_seconds, Utils): + def adjust_resume(self, resume_seconds): resume = 0 if resume_seconds: resume = round(float(resume_seconds), 6) - jumpback = int(Utils.settings('resumeJumpBack')) + jumpback = int(self.Utils.Settings.resumeJumpBack) if resume > jumpback: # To avoid negative bookmark @@ -135,8 +120,8 @@ def validate_studio(self, studio_name): } return studios.get(studio_name.lower(), studio_name) - def get_overview(self, overview=None): - overview = overview or self.item.get('Overview') + def get_overview(self, overview, item): + overview = overview or item.get('Overview') if not overview: return @@ -147,8 +132,16 @@ def get_overview(self, overview=None): overview = overview.replace("
", "[CR]") return overview - def get_mpaa(self, rating=None): - mpaa = rating or self.item.get('OfficialRating', "") + def get_DateAdded(self, DateInfo): + if DateInfo: + DateAdded = DateInfo.split('.')[0].replace('T', " ") + FileDate = "%s.%s.%s" % tuple(reversed(DateAdded.split('T')[0].split('-'))) + return DateAdded, FileDate + + return None, None + + def get_mpaa(self, rating, item): + mpaa = rating or item.get('OfficialRating', "") if mpaa in ("NR", "UR"): # Kodi seems to not like NR, but will accept Not Rated @@ -157,11 +150,14 @@ def get_mpaa(self, rating=None): if "FSK-" in mpaa: mpaa = mpaa.replace("-", " ") + if "GB-" in mpaa: + mpaa = mpaa.replace("GB-", "UK:") + return mpaa - def get_file_path(self, path=None): + def get_file_path(self, path, item): if path is None: - path = self.item.get('Path') + path = item.get('Path') path = self.Utils.StringMod(path) @@ -169,9 +165,9 @@ def get_file_path(self, path=None): if path.endswith('.strm'): path = path.replace('.strm', "") - if 'Container' in self.item: - if not path.endswith(self.Utils.StringMod(self.item['Container'])): - path = path + "." + self.Utils.StringMod(self.item['Container']) + if 'Container' in item: + if not path.endswith(self.Utils.StringMod(item['Container'])): + path = path + "." + self.Utils.StringMod(item['Container']) if not path: return "" @@ -179,10 +175,10 @@ def get_file_path(self, path=None): if path.startswith('\\\\'): path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/") - if 'Container' in self.item: - if self.item['Container'] == 'dvd': + if 'Container' in item: + if item['Container'] == 'dvd': path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path - elif self.item['Container'] == 'bluray': + elif item['Container'] == 'bluray': path = "%s/BDMV/index.bdmv" % path path = path.replace('\\\\', "\\") @@ -196,14 +192,8 @@ def get_file_path(self, path=None): return path - def get_user_artwork(self, user_id): - ''' Get emby user profile picture. - ''' - return "%s/emby/Users/%s/Images/Primary?Format=original" % (self.server, user_id) - + #Get people (actor, director, etc) artwork. def get_people_artwork(self, people): - ''' Get people (actor, director, etc) artwork. - ''' for person in people: if 'PrimaryImageTag' in person: #query = [('MaxWidth', 400), ('MaxHeight', 400), ('Index', 0)] @@ -214,12 +204,8 @@ def get_people_artwork(self, people): return people + #Get all artwork possible. If parent_info is True, it will fill missing artwork with parent artwork. def get_all_artwork(self, obj, parent_info=False): - ''' Get all artwork possible. If parent_info is True, - it will fill missing artwork with parent artwork. - - obj is from objects.Objects().map(item, 'Artwork') - ''' query = [] all_artwork = { 'Primary': "", @@ -231,11 +217,6 @@ def get_all_artwork(self, obj, parent_info=False): 'Disc': "", 'Backdrop': [] } - - if self.Utils.settings('compressArt.bool'): - query.append(('Quality', 70)) - - query.append(('EnableImageEnhancers', self.Utils.settings('enableCoverArt.bool'))) all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query) for artwork in (obj['Tags'] or []): @@ -260,9 +241,8 @@ def get_all_artwork(self, obj, parent_info=False): return all_artwork - def get_backdrops(self, item_id, tags, query=None): - ''' Get backdrops based of "BackdropImageTags" in the emby object. - ''' + #Get backdrops based of "BackdropImageTags" in the emby object. + def get_backdrops(self, item_id, tags, query): query = list(query) if query else [] backdrops = [] @@ -272,17 +252,12 @@ def get_backdrops(self, item_id, tags, query=None): for index, tag in enumerate(tags): query.append(('Tag', tag)) artwork = "http://127.0.0.1:57578/%s/Images/Backdrop/%s?%s" % (item_id, index, urlencode(query)) - - if not self.verify_ssl: - artwork += "|verifypeer=false" - backdrops.append(artwork) return backdrops - def get_artwork(self, item_id, image, tag=None, query=None): - ''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc - ''' + #Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc + def get_artwork(self, item_id, image, tag, query, Native=False): query = list(query) if query else [] if item_id is None: @@ -291,9 +266,8 @@ def get_artwork(self, item_id, image, tag=None, query=None): if tag is not None: query.append(('Tag', tag)) - artwork = "http://127.0.0.1:57578/%s/Images/%s/0?%s" % (item_id, image, urlencode(query)) - - if not self.verify_ssl: - artwork += "|verifypeer=false" - + if Native: + artwork = "%s/emby/Items/%s/Images/%s/0?%s" % (Native, item_id, image, urlencode(query)) + else: + artwork = "http://127.0.0.1:57578/%s/Images/%s/0?%s" % (item_id, image, urlencode(query)) return artwork diff --git a/helper/context.py b/helper/context.py new file mode 100644 index 000000000..630dfdc0f --- /dev/null +++ b/helper/context.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +import json + +import xbmc +import xbmcaddon + +import database.database +import helper.utils +import helper.loghandler +import dialogs.context + +class Context(): + def __init__(self, Utils, EmbyServers, library): + self.LOG = helper.loghandler.LOG('EMBY.context.Context') + self._selected_option = None + self.Utils = Utils + self.item = None + self.EmbyServers = EmbyServers + self.library = library + self.XML_PATH = (xbmcaddon.Addon('plugin.video.emby-next-gen').getAddonInfo('path'), "default", "1080i") + self.server_id = None + self.OPTIONS = { + 'Refresh': self.Utils.Translate(30410), + 'Delete': self.Utils.Translate(30409), + 'Addon': self.Utils.Translate(30408), + 'AddFav': self.Utils.Translate(30405), + 'RemoveFav': self.Utils.Translate(30406), + 'Transcode': self.Utils.Translate(30412) + } + + def load_item(self): + for server_id in self.EmbyServers: ######################## WORKAROUND!!!!!!!!!!! + self.server_id = server_id + break + + kodi_id = xbmc.getInfoLabel('ListItem.DBID') + media = xbmc.getInfoLabel('ListItem.DBTYPE') + self.item = database.database.get_item(self.Utils, kodi_id, media) + + if not self.item: + return False + + return True + + def delete_item(self, LoadItem=False): + if LoadItem: + if not self.load_item(): + return + + if self.Utils.dialog("yesno", heading="{emby}", line1=self.Utils.Translate(33015)): + self.EmbyServers[self.server_id].API.delete_item(self.item[0]) + self.library[self.server_id].removed([self.item[0]]) + self.library[self.server_id].delay_verify([self.item[0]]) + + def select_menu(self): + options = [] + + if not self.load_item(): + return + + Userdata = json.loads(self.item[4]) if self.item[4] else {} + + if self.item[3] not in 'Season': + if Userdata.get('IsFavorite'): + options.append(self.OPTIONS['RemoveFav']) + else: + options.append(self.OPTIONS['AddFav']) + + options.append(self.OPTIONS['Refresh']) + + if self.Utils.Settings.enableContextDelete: + options.append(self.OPTIONS['Delete']) + + options.append(self.OPTIONS['Addon']) + context_menu = dialogs.context.ContextMenu("script-emby-context.xml", *self.XML_PATH) + context_menu.set_options(options) + context_menu.doModal() + + if context_menu.is_selected(): + self._selected_option = context_menu.get_selected() + + if self._selected_option: + self.action_menu() + + def action_menu(self): + selected = self.Utils.StringDecode(self._selected_option) + + if selected == self.OPTIONS['Refresh']: + self.EmbyServers[self.server_id].API.refresh_item(self.item[0]) + elif selected == self.OPTIONS['AddFav']: + self.EmbyServers[self.server_id].API.favorite(self.item[0], True) + elif selected == self.OPTIONS['RemoveFav']: + self.EmbyServers[self.server_id].API.favorite(self.item[0], False) + elif selected == self.OPTIONS['Addon']: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby-next-gen)') + elif selected == self.OPTIONS['Delete']: + self.delete_item(False) diff --git a/helper/exceptions.py b/helper/exceptions.py deleted file mode 100644 index 165710051..000000000 --- a/helper/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -class LibraryException(Exception): - #Emby library sync exception - def __init__(self, status): - self.status = status diff --git a/helper/jsonrpc.py b/helper/jsonrpc.py new file mode 100644 index 000000000..2e253ff65 --- /dev/null +++ b/helper/jsonrpc.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import json + +import xbmc +class JSONRPC(): + def __init__(self, method, **kwargs): + self.method = method + self.params = False + + for arg in kwargs: + self.arg = kwargs[arg] + + def _query(self): + query = { + 'jsonrpc': "2.0", + 'id': 1, + 'method': self.method, + } + + if self.params: + query['params'] = self.params + + return json.dumps(query) + + def execute(self, params): + self.params = params + return json.loads(xbmc.executeJSONRPC(self._query())) diff --git a/helper/loghandler.py b/helper/loghandler.py index 127c1a33c..0508d94da 100644 --- a/helper/loghandler.py +++ b/helper/loghandler.py @@ -1,108 +1,47 @@ # -*- coding: utf-8 -*- -import logging import xbmc -import database.database -from . import utils - -def config(): - logger = logging.getLogger('EMBY') - logger.addHandler(LogHandler()) - logger.setLevel(logging.DEBUG) - -def reset(): - for handler in logging.getLogger('EMBY').handlers: - logging.getLogger('EMBY').removeHandler(handler) - -class LogHandler(logging.StreamHandler): - def __init__(self): - self.Utils = utils.Utils() +class LOG(): + def __init__(self, Prefix): + self.Prefix = Prefix self.KodiVersion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - logging.StreamHandler.__init__(self) - self.setFormatter(MyFormatter()) - self.sensitive = {'Token': [], 'Server': []} - - for server in database.database.get_credentials()['Servers']: - if server.get('AccessToken'): - self.sensitive['Token'].append(server['AccessToken']) - - if server.get('LocalAddress'): - self.sensitive['Server'].append(server['LocalAddress'].split('://')[1]) - if server.get('RemoteAddress'): - self.sensitive['Server'].append(server['RemoteAddress'].split('://')[1]) - - if server.get('ManualAddress'): - self.sensitive['Server'].append(server['ManualAddress'].split('://')[1]) - - self.mask_info = self.Utils.settings('maskInfo.bool') + def debug(self, msg): + msg = "DEBUG: %s: %s" % (self.Prefix, msg) if self.KodiVersion >= 19: - self.LogFlag = xbmc.LOGINFO + xbmc.log(msg, xbmc.LOGDEBUG) else: - self.LogFlag = xbmc.LOGNOTICE + xbmc.log(self.Encode(msg), xbmc.LOGDEBUG) - def emit(self, record): - if self._get_log_level(record.levelno, self.Utils): - try: - string = self.format(record) - except: - xbmc.log("Error trying to format string for log", level=xbmc.LOGWARNING) - return + def info(self, msg): + msg = "INFO: %s: %s" % (self.Prefix, msg) - if self.mask_info: - for server in self.sensitive['Server']: - if self.KodiVersion >= 19: - string = string.replace(server or "{server}", "{emby-server}") - else: - string = string.replace(server.encode('utf-8') or "{server}", "{emby-server}") + if self.KodiVersion >= 19: + xbmc.log(msg, xbmc.LOGINFO) + else: + xbmc.log(self.Encode(msg), xbmc.LOGNOTICE) - for token in self.sensitive['Token']: - if self.KodiVersion >= 19: - string = string.replace(token or "{token}", "{emby-token}") - else: - string = string.replace(token.encode('utf-8') or "{token}", "{emby-token}") + def warning(self, msg): + msg = "WARNING: %s: %s" % (self.Prefix, msg) - try: - xbmc.log(string, level=self.LogFlag) - except UnicodeEncodeError: - xbmc.log(string.encode('utf-8'), level=self.LogFlag) + if self.KodiVersion >= 19: + xbmc.log(msg, xbmc.LOGWARNING) + else: + xbmc.log(self.Encode(msg), xbmc.LOGWARNING) - @classmethod - def _get_log_level(cls, level, Utils): - levels = { - logging.ERROR: 0, - logging.WARNING: 0, - logging.INFO: 1, - logging.DEBUG: 2 - } + def error(self, msg): + msg = "ERROR: %s: %s" % (self.Prefix, msg) - log_level = 1 + if self.KodiVersion >= 19: + xbmc.log(msg, xbmc.LOGERROR) + else: + xbmc.log(self.Encode(msg), xbmc.LOGERROR) + def Encode(self, Data): try: - log_level = int(Utils.settings('logLevel')) - except ValueError: - log_level = 1 - - return log_level >= levels[level] - -class MyFormatter(logging.Formatter): - def __init__(self, fmt="%(name)s -> %(message)s"): - logging.Formatter.__init__(self, fmt) - - def format(self, record): - # Save the original format configured by the user - # when the logger formatter was instantiated - format_orig = self._fmt - - # Replace the original format with one customized by logging level - if record.levelno in (logging.DEBUG, logging.ERROR): - self._fmt = '%(name)s -> %(levelname)s:: %(message)s' - - # Call the original formatter class to do the grunt work - result = logging.Formatter.format(self, record) - - # Restore the original format configured by the user - self._fmt = format_orig + Data = unicode(Data, 'utf-8') + except: + pass - return result + return Data.encode('utf-8') diff --git a/helper/pluginmenu.py b/helper/pluginmenu.py new file mode 100644 index 000000000..e4ce323d9 --- /dev/null +++ b/helper/pluginmenu.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- +try: + from urllib import urlencode +except: + from urllib.parse import urlencode + +import xbmc +import xbmcgui +import xbmcplugin + +import helper.loghandler +import core.listitem + +class Menu(): + def __init__(self, Utils, EmbyServers, player): + self.LOG = helper.loghandler.LOG('EMBY.helper.pluginmenu.Menu') + self.Utils = Utils + self.EmbyServers = EmbyServers + self.player = player + self.ListItemData = [] + + #Add directory listitem. context should be a list of tuples [(label, action)*] + def add_ListItem(self, label, path, folder, artwork, fanart): + li = xbmcgui.ListItem(label, path=path) + li.setArt({"thumb": artwork or "special://home/addons/plugin.video.emby-next-gen/resources/icon.png", "fanart": fanart or "special://home/addons/plugin.video.emby-next-gen/resources/fanart.jpg", "landscape": artwork or fanart 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"}) + self.ListItemData.append((path, li, folder)) + + #Display all emby nodes and dynamic entries when appropriate + def listing(self, Handle): + if not self.wait_online(): + return + + Handle = int(Handle) + self.ListItemData = [] + + for server_id in self.EmbyServers: + for Node in self.EmbyServers[server_id].Nodes: + label = self.Utils.StringDecode(Node['title']) + node = self.Utils.StringDecode(Node['type']) + self.LOG.debug("--[ listing/%s/%s ] %s" % (node, label, Node['path'])) + self.add_ListItem(label, Node['path'], True, Node['icon'], None) + + self.add_ListItem("%s (%s)" % (self.Utils.Translate(33194), self.EmbyServers[server_id].Data['auth.server-name']), "plugin://plugin.video.emby-next-gen/?mode=managelibs&server=%s" % server_id, True, None, None) + self.add_ListItem("%s (%s)" % (self.Utils.Translate(33054), self.EmbyServers[server_id].Data['auth.server-name']), "plugin://plugin.video.emby-next-gen/?mode=adduser&server=%s" % server_id, False, None, None) + + self.add_ListItem(self.Utils.Translate('fav_movies'), "library://video/emby_Favoritemovies.xml", True, None, None) + self.add_ListItem(self.Utils.Translate('fav_tvshows'), "library://video/emby_Favoritetvshows.xml", True, None, None) + self.add_ListItem(self.Utils.Translate('fav_episodes'), "plugin://plugin.video.emby-next-gen/?mode=browse&type=Episode&folder=FavEpisodes", True, None, None) + self.add_ListItem(self.Utils.Translate(33134), "plugin://plugin.video.emby-next-gen/?mode=addserver", False, None, None) + self.add_ListItem(self.Utils.Translate(5), "plugin://plugin.video.emby-next-gen/?mode=settings", False, None, None) + self.add_ListItem(self.Utils.Translate(33059), "plugin://plugin.video.emby-next-gen/?mode=texturecache", False, None, None) + self.add_ListItem(self.Utils.Translate(33058), "plugin://plugin.video.emby-next-gen/?mode=reset", False, None, None) + self.add_ListItem(self.Utils.Translate(33192), "plugin://plugin.video.emby-next-gen/?mode=restartservice", False, None, None) + self.add_ListItem(self.Utils.Translate(33202), "plugin://plugin.video.emby-next-gen/?mode=patchmusic", False, None, None) + self.add_ListItem(self.Utils.Translate(33092), "plugin://plugin.video.emby-next-gen/?mode=backup", False, None, None) + xbmcplugin.addDirectoryItems(Handle, self.ListItemData, len(self.ListItemData)) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(Handle, 'files') + xbmcplugin.endOfDirectory(Handle) + + def manage_libraries(self, Handle, server_id): + Handle = int(Handle) + self.ListItemData = [] +# self.add_ListItem(self.Utils.Translate(33098), "plugin://plugin.video.emby-next-gen/?mode=refreshboxsets&server=%s" % self.server_id, False, None, None) + self.add_ListItem(self.Utils.Translate(33154), "plugin://plugin.video.emby-next-gen/?mode=addlibs&server=%s" % server_id, False, None, None) + self.add_ListItem(self.Utils.Translate(33139), "plugin://plugin.video.emby-next-gen/?mode=updatelibs&server=%s" % server_id, False, None, None) + self.add_ListItem(self.Utils.Translate(33140), "plugin://plugin.video.emby-next-gen/?mode=repairlibs&server=%s" % server_id, False, None, None) + self.add_ListItem(self.Utils.Translate(33184), "plugin://plugin.video.emby-next-gen/?mode=removelibs&server=%s" % server_id, False, None, None) + self.add_ListItem(self.Utils.Translate(33060), "plugin://plugin.video.emby-next-gen/?mode=thememedia&server=%s" % server_id, False, None, None) + xbmcplugin.addDirectoryItems(Handle, self.ListItemData, len(self.ListItemData)) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(Handle, 'files') + xbmcplugin.endOfDirectory(Handle) + + #Browse dynamically content + def browse(self, Handle, media, view_id, folder, name, extra, server_id): + if not self.wait_online(): + return + + server_id = self.verify_serverid(server_id) + Handle = int(Handle) + folder = folder.lower() if folder else None + self.LOG.info("--[ v:%s/%s ] %s" % (view_id, media, folder)) + + if folder is None: + return self.browse_subfolders(Handle, media, view_id, name, server_id) + + if folder == 'letter': + return self.browse_letters(Handle, media, view_id, name, server_id) + + #Mapping + KodiMediaID = {'musicvideos': 'musicvideos', 'tvshows': 'tvshows', 'music': 'artists', 'movies': 'movies', 'livetv': 'videos', 'channels': 'songs', 'boxsets': 'movies', 'playlists': 'movies', 'Season': 'tvshows', 'Episode': 'episodes', 'MusicVideos': 'musicvideos', 'MusicAlbum': 'albums', 'Songs': 'songs', 'Folder': 'videos', 'PhotoAlbum': 'images', 'Photo': 'images', 'homevideos': 'images', 'mixed': 'mixed', 'Genre': 'videos'} + EmbyMediaID = {'musicvideos': 'MusicVideos', 'tvshows': "Series", 'music': 'MusicArtist', 'movies': 'Movie', 'livetv': 'LiveTv', 'channels': 'Songs', 'boxsets': 'BoxSet', 'playlists': 'Movie', 'Season': 'Season', 'Episode': 'Episode', 'MusicVideos': 'MusicVideos', 'MusicAlbum': 'MusicAlbum', 'Songs': 'Songs', 'Folder': 'Folder', 'PhotoAlbum': 'PhotoAlbum', 'Photo': 'Photo', 'homevideos': 'Video,PhotoAlbum,Photo', 'mixed': 'Movie,Series,Video', 'Genre': None} + KodiType = KodiMediaID[media] + EmbyType = EmbyMediaID[media] + + if view_id: + xbmcplugin.setPluginCategory(Handle, name) + + if extra: + if extra == "0-9": #Special charecters + extra = {'NameLessThan': "A"} + else: #alphabet + extra = {'NameStartsWith': extra} + + ID = view_id + else: + ID = folder + + #General nodes + if folder == 'recentlyadded': + listing = self.EmbyServers[server_id].API.get_recently_added(None, view_id, 25) + elif folder == 'genres': + listing = self.EmbyServers[server_id].API.get_genres(view_id) + elif folder == 'unwatched': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'filters': ['IsUnplayed']}) + elif folder == 'favorite': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'filters': ['IsFavorite']}) + elif folder == 'inprogress': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'filters': ['IsResumable']}) + elif folder == 'boxsets': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'media': "BoxSet"}) + elif folder == 'random': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'media': EmbyType, 'random': True, 'recursive': True, 'limit': 25}) + elif (folder or "").startswith('genres-'): + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': view_id, 'media': None, 'extra': {'GenreIds': folder.split('-')[1]}}) + elif folder == 'favepisodes': + listing = self.EmbyServers[server_id].API.get_filtered_section({'limit': 25, 'media': EmbyType, 'filters': ['IsFavorite']}) + + #Root nodes + elif media == 'musicvideos': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "MusicArtist", 'recursive': True}) + elif media == 'tvshows': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Series", 'recursive': True}) + elif media == 'music': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "MusicArtist", 'recursive': True}) + elif media == 'movies': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Movie", 'recursive': True}) + elif media == 'livetv': + listing = self.EmbyServers[server_id].API.get_channels() + elif media == 'channels': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Folder", 'recursive': True}) + elif media == 'boxsets': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "BoxSet", 'recursive': True}) + elif media == 'playlists': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'recursive': False}) + elif media == 'homevideos': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Video,PhotoAlbum,Photo", 'recursive': False}) + elif media == 'mixed': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Movie,Series,Video", 'recursive': False}) + + #Emby Server media ID nodes + elif media == 'Season': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Season", 'recursive': True}) + elif media == 'Episode': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Episode", 'recursive': True}) + elif media == 'MusicVideos': + listing = self.EmbyServers[server_id].API.browse_MusicByArtistId(folder, view_id, "MusicVideos", extra) + elif media == 'MusicAlbum': + listing = self.EmbyServers[server_id].API.browse_MusicByArtistId(folder, view_id, "MusicAlbum", extra) + elif media == 'Songs': + listing = self.EmbyServers[server_id].API.browse_MusicByArtistId(folder, view_id, "Audio", extra) + elif media == 'Folder': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'recursive': False}) + elif media == 'PhotoAlbum': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "PhotoAlbum", 'recursive': False}) + elif media == 'Photo': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Photo", 'recursive': False}) + elif media == 'Movie': + listing = self.EmbyServers[server_id].API.get_filtered_section({'ViewId': ID, 'extra': extra, 'media': "Movie", 'recursive': True}) + + if listing: + listitems = core.listitem.ListItem(self.Utils) + list_li = [] + listing = listing if isinstance(listing, list) else listing.get('Items', []) + + for item in listing: + if xbmc.Monitor().waitForAbort(0.0001): + return + + li = listitems.set(item) + + if item.get('IsFolder') or item['Type'] in ('MusicArtist', 'MusicAlbum'): + params = { + 'id': view_id, + 'mode': "browse", + 'type': self.get_Subfolder(item['Type'], KodiType), + 'name': name, + 'folder': item['Id'], + 'server': server_id + } + + path = "plugin://plugin.video.emby-next-gen/?%s" % urlencode(params) + list_li.append((path, li, True)) + elif item['Type'] == 'Genre': + params = { + 'id': view_id or item['Id'], + 'mode': "browse", + 'type': self.get_Subfolder(item['Type'], KodiType), + 'folder': 'genres-%s' % item['Id'], + 'name': name, + 'server': server_id + } + path = "%s?%s" % ("plugin://plugin.video.emby-next-gen/", urlencode(params)) + list_li.append((path, li, True)) + else: + if item['Type'] == 'Photo': + path = "http://127.0.0.1:57578/%s/Images/Primary" % item['Id'] + elif item['Type'] == 'PhotoAlbum': + path = "plugin://plugin.video.emby-next-gen/?mode=photoviewer&id=%s" % item['Id'] + else: + path = "" + + if item['Type'] == "MusicVideo": + Type = "musicvideo" + elif item['Type'] == "Movie": + Type = "movie" + elif item['Type'] == "Episode": + Type = "tvshow" + elif item['Type'] == "Audio": + Type = "audio" + elif item['Type'] == "Video": + Type = "video" + elif item['Type'] == "Trailer": + Type = "trailer" + elif item['Type'] == "TvChannel": + Type = "tvchannel" + path = "http://127.0.0.1:57578/livetv/%s-stream.ts" % item['Id'] + else: + return + + if not path: + path = "http://127.0.0.1:57578/%s/%s-DYNAMIC-stream-%s" % (Type, item['Id'], self.Utils.PathToFilenameReplaceSpecialCharecters(item['Path'])) + + self.player.DynamicItem[self.Utils.ReplaceSpecialCharecters(li.getLabel())] = item['Id'] + + list_li.append((path, li, False)) + + xbmcplugin.addDirectoryItems(Handle, list_li, len(list_li)) + + #Set Sorting + if media in ('homevideos', 'Photo', 'PhotoAlbum'): + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_LABEL) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_DATE) + elif media == 'playlists' or folder == 'random': + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RATING) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + else: + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_TITLE) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_DATE) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RATING) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + + xbmcplugin.setContent(Handle, KodiType) + xbmcplugin.endOfDirectory(Handle) + + #Display submenus for emby views + def browse_subfolders(self, Handle, media, view_id, name, server_id): + DYNNODES = { + 'tvshows': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultTVShows.png'), + ('recentlyadded', self.Utils.Translate(30170), 'DefaultRecentlyAddedEpisodes.png'), + ('genres', self.Utils.Translate(135), 'DefaultGenre.png'), + ('random', self.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png') + ], + 'mixed': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultTVShows.png'), + ('recentlyadded', self.Utils.Translate(30170), 'DefaultRecentlyAddedEpisodes.png'), + ('inprogress', self.Utils.Translate(30171), 'DefaultInProgressShows.png'), + ('inprogressepisodes', self.Utils.Translate(30178), 'DefaultInProgressShows.png'), + ('genres', self.Utils.Translate(135), 'DefaultGenre.png'), + ('random', self.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png') + ], + 'movies': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultMovies.png'), + ('recentlyadded', self.Utils.Translate(30174), 'DefaultRecentlyAddedMovies.png'), + ('inprogress', self.Utils.Translate(30177), 'DefaultInProgressShows.png'), + ('boxsets', self.Utils.Translate(20434), 'DefaultSets.png'), + ('favorite', self.Utils.Translate(33168), 'DefaultFavourites.png'), + ('genres', self.Utils.Translate(135), 'DefaultGenre.png'), + ('random', self.Utils.Translate(30229), 'special://home/addons/plugin.video.emby-next-gen/resources/random.png') + ], + 'boxsets': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultSets.png') + ], + 'livetv': [ + ('all', None, 'DefaultMovies.png') + ], + 'musicvideos': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultMusicVideos.png'), + ('recentlyadded', self.Utils.Translate(30256), 'DefaultRecentlyAddedMusicVideos.png'), + ('inprogress', self.Utils.Translate(30257), 'DefaultInProgressShows.png'), + ('Unwatched', self.Utils.Translate(30258), 'OverlayUnwatched.png') + ], + 'homevideos': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultAddonVideo.png'), + ('recentlyadded', self.Utils.Translate(33167), '') + ], + 'channels': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultTVShows.png'), + ('recentlyadded', self.Utils.Translate(33167), ''), + ('inprogress', self.Utils.Translate(33169), ''), + ('favorite', self.Utils.Translate(33168), 'DefaultFavourites.png') + ], + 'playlists': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultPlaylist.png') + ], + 'books': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, ''), + ('recentlyadded', self.Utils.Translate(33167), ''), + ('inprogress', self.Utils.Translate(33169), ''), + ('favorite', self.Utils.Translate(33168), 'DefaultFavourites.png') + ], + 'audiobooks': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultMusicVideos.png'), + ('recentlyadded', self.Utils.Translate(33167), ''), + ('inprogress', self.Utils.Translate(33169), ''), + ('favorite', self.Utils.Translate(33168), 'DefaultFavourites.png') + ], + 'music': [ + ('letter', "A-Z", 'special://home/addons/plugin.video.emby-next-gen/resources/letter.png'), + ('all', None, 'DefaultAddonMusic.png') + ] + } + self.ListItemData = [] + xbmcplugin.setPluginCategory(Handle, name) + + for node in DYNNODES[media]: + params = { + 'id': view_id, + 'mode': "browse", + 'type': media, + 'folder': view_id if node[0] == 'all' else node[0], + 'name': name, + 'server': server_id + } + path = "plugin://plugin.video.emby-next-gen/?%s" % urlencode(params) + self.add_ListItem(node[1] or name, path, True, node[2], None) + + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(Handle, 'files') + xbmcplugin.addDirectoryItems(Handle, self.ListItemData, len(self.ListItemData)) + xbmcplugin.endOfDirectory(Handle) + + #Display letters as options + def browse_letters(self, Handle, media, view_id, name, server_id): + letters = ["0-9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] + self.ListItemData = [] + xbmcplugin.setPluginCategory(Handle, name) + + for node in letters: + params = { + 'id': view_id, + 'mode': "browse", + 'type': media, + 'extra': node, + 'folder': view_id if node[0] == 'all' else node[0], + 'name': name, + 'server': server_id + } + path = "plugin://plugin.video.emby-next-gen/?%s" % urlencode(params) + self.add_ListItem(node, path, True, None, None) + + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(Handle, 'files') + xbmcplugin.addDirectoryItems(Handle, self.ListItemData, len(self.ListItemData)) + xbmcplugin.endOfDirectory(Handle) + + #Browsing (subfolder) mapping Table + def get_Subfolder(self, media, KodiType): + if media == 'tvshows': + return "Series" + + if media == 'Series': + return "Season" + + if media == 'Season': + return "Episode" + + if media == 'MusicArtist' and KodiType == "musicvideos": + return "MusicVideos" + + if media == 'MusicArtist': + return "MusicAlbum" + + if media == 'MusicAlbum': + return "Songs" + + if media == 'Folder': + return "Folder" + + if media == 'BoxSet': + return "movies" + + if media == 'Playlist': + return "playlists" + + if media == 'PhotoAlbum': + return "Photo" + + if media == 'movies': + return "movies" + + if media == 'channels': + return "channels2" + + if media == 'homevideos': + return "homevideos" + + if media == 'Genre': + return "Genre" + + def get_next_episodes(self, Handle, libraryname): + Handle = int(Handle) + result = helper.jsonrpc.JSONRPC('VideoLibrary.GetTVShows').execute({ + 'sort': {'order': "descending", 'method': "lastplayed"}, + 'filter': { + 'and': [ + {'operator': "true", 'field': "inprogress", 'value': ""}, + {'operator': "is", 'field': "tag", 'value': "%s" % libraryname} + ]}, + 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] + }) + items = result['result']['tvshows'] + list_li = [] + + for item in items: + params = { + 'tvshowid': item['tvshowid'], + 'sort': {'method': "episode"}, + 'filter': { + 'and': [ + {'operator': "lessthan", 'field': "playcount", 'value': "1"}, + {'operator': "greaterthan", 'field': "season", 'value': "0"}] + }, + 'properties': [ + "title", "playcount", "season", "episode", "showtitle", "plot", "file", "rating", "resume", "streamdetails", "firstaired", "writer", "dateadded", "lastplayed", "originaltitle", "seasonid", "specialsortepisode", "specialsortseason", "userrating", "votes", "cast", "art", "uniqueid" + ], + 'limits': {"end": 1} + } + result = helper.jsonrpc.JSONRPC('VideoLibrary.GetEpisodes').execute(params) + episodes = result['result']['episodes'] + + for episode in episodes: + FilePath = episode["file"] + li = self.Utils.CreateListitem("episode", episode) + list_li.append((FilePath, li, False)) + + xbmcplugin.addDirectoryItems(Handle, list_li, len(list_li)) + xbmcplugin.addSortMethod(Handle, xbmcplugin.SORT_METHOD_UNSORTED) + xbmcplugin.setContent(Handle, 'episodes') + xbmcplugin.endOfDirectory(Handle) + + def wait_online(self): + for _ in range(60): #wait for ack (60 seconds timeout) + if self.EmbyServers: + return True + + if xbmc.Monitor().waitForAbort(1): + return False + + return False + + def verify_serverid(self, server_id): + if not server_id or server_id == 'None': + for server_id in self.EmbyServers: ######################## WORKAROUND!!!!!!!!!!! + break + + return server_id diff --git a/helper/settings.py b/helper/settings.py new file mode 100644 index 000000000..fcd8b7db4 --- /dev/null +++ b/helper/settings.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +from . import loghandler +from . import jsonrpc + +class Settings(): + def __init__(self, Addon): + self.LOG = loghandler.LOG('EMBY.helper.settings.Settings') + self.Addon = Addon + self.VideoBitrateOptions = [664000, 996000, 1320000, 2000000, 3200000, 4700000, 6200000, 7700000, 9200000, 10700000, 12200000, 13700000, 15200000, 16700000, 18200000, 20000000, 25000000, 30000000, 35000000, 40000000, 100000000, 1000000000] + self.AudioBitrateOptions = [64000, 96000, 128000, 192000, 256000, 320000, 384000, 448000, 512000] + self.VideoCodecOptions = ["h264", "hevc"] + self.AudioCodecOptions = ["aac", "ac3"] + + #Global variable + self.emby_shouldstop = False + self.emby_restart = False + self.emby_servers_sync = None + self.emby_servers = None + self.emby_UserImage = None + + #Settings + self.limitThreads = "" + self.TranscodeFormatVideo = "" + self.TranscodeFormatAudio = "" + self.videoBitrate = "" + self.audioBitrate = "" + self.resumeJumpBack = "" + self.displayMessage = "" + self.syncProgress = "" + self.newvideotime = "" + self.newmusictime = "" + self.startupDelay = "" + self.backupPath = "" + self.LastIncrementalSync = "" + self.MinimumSetup = "" + self.useDirectPaths = "" + self.limitIndex = "" + self.idMethod = "" + self.username = "" + self.connectUsername = "" + self.serverName = "" + self.server = "" + self.deviceName = "" + self.askSyncIndicator = "" + self.Users = "" + self.Migrate = False + self.SyncInstallRunDone = False + self.newContent = False + self.restartMsg = False + self.connectMsg = False + self.addUsersHidden = False + self.enableContextDelete = False + self.enableContext = False + self.transcodeH265 = False + self.transcodeDivx = False + self.transcodeXvid = False + self.transcodeMpeg2 = False + self.enableCinema = False + self.askCinema = False + self.offerDelete = False + self.deleteTV = False + self.deleteMovies = False + self.userRating = False + self.enableCoverArt = False + self.compressArt = False + self.getDateCreated = False + self.getGenres = False + self.getStudios = False + self.getTaglines = False + self.getOverview = False + self.getProductionLocations = False + self.getCast = False + self.groupedSets = False + self.deviceNameOpt = False + self.sslverify = False + self.kodiCompanion = False + self.syncDuringPlay = False + self.dbSyncScreensaver = False + self.enableAddon = False + self.ReloadSkin = False + self.VideoBitrate = 0 + self.AudioBitrate = 0 + self.VideoCodecID = "" + self.AudioCodecID = "" + self.Screensaver = False + self.WebserverData = {} + self.GroupedSet = "" + + def InitSettings(self): + self.load_settings('limitThreads') + self.load_settings('TranscodeFormatVideo') + self.load_settings('TranscodeFormatAudio') + self.load_settings('videoBitrate') + self.load_settings('audioBitrate') + self.load_settings('resumeJumpBack') + self.load_settings('displayMessage') + self.load_settings('syncProgress') + self.load_settings('newvideotime') + self.load_settings('newmusictime') + self.load_settings('startupDelay') + self.load_settings('backupPath') + self.load_settings('LastIncrementalSync') + self.load_settings('MinimumSetup') + self.load_settings('useDirectPaths') + self.load_settings('limitIndex') + self.load_settings('idMethod') + self.load_settings('username') + self.load_settings('connectUsername') + self.load_settings('serverName') + self.load_settings('server') + self.load_settings('deviceName') + self.load_settings('askSyncIndicator') + self.load_settings('Users') + self.load_settings_bool('Migrate') + self.load_settings_bool('SyncInstallRunDone') + self.load_settings_bool('newContent') + self.load_settings_bool('restartMsg') + self.load_settings_bool('connectMsg') + self.load_settings_bool('addUsersHidden') + self.load_settings_bool('enableContextDelete') + self.load_settings_bool('enableContext') + self.load_settings_bool('transcodeH265') + self.load_settings_bool('transcodeDivx') + self.load_settings_bool('transcodeXvid') + self.load_settings_bool('transcodeMpeg2') + self.load_settings_bool('enableCinema') + self.load_settings_bool('askCinema') + self.load_settings_bool('offerDelete') + self.load_settings_bool('deleteTV') + self.load_settings_bool('deleteMovies') + self.load_settings_bool('userRating') + self.load_settings_bool('enableCoverArt') + self.load_settings_bool('compressArt') + self.load_settings_bool('getDateCreated') + self.load_settings_bool('getGenres') + self.load_settings_bool('getStudios') + self.load_settings_bool('getTaglines') + self.load_settings_bool('getOverview') + self.load_settings_bool('getProductionLocations') + self.load_settings_bool('getCast') + self.load_settings_bool('groupedSets') + self.load_settings_bool('deviceNameOpt') + self.load_settings_bool('sslverify') + self.load_settings_bool('kodiCompanion') + self.load_settings_bool('syncDuringPlay') + self.load_settings_bool('dbSyncScreensaver') + self.load_settings_bool('enableAddon') + self.load_settings_bool('ReloadSkin') + self.VideoBitrate = self.VideoBitrateOptions[int(self.videoBitrate)] + self.AudioBitrate = self.AudioBitrateOptions[int(self.audioBitrate)] + self.VideoCodecID = self.VideoCodecOptions[int(self.TranscodeFormatVideo)] + self.AudioCodecID = self.AudioCodecOptions[int(self.TranscodeFormatAudio)] + self.Screensaver = self.get_screensaver() + self.WebserverData = self.get_web_server_data() + self.GroupedSet = self.get_grouped_set() + + def get_web_server(self): + result = jsonrpc.JSONRPC('Settings.GetSettingValue').execute({'setting': "services.webserver"}) + + try: + return result['result']['value'] + except (KeyError, TypeError): + return False + + #Enable the webserver if not enabled. This is used to cache artwork. + #Will only test once, if it fails, user will be notified only once + def get_web_server_data(self): + Data = {'Enabled' : False} + get_setting = jsonrpc.JSONRPC('Settings.GetSettingValue') + + if not self.get_web_server(): + set_setting = jsonrpc.JSONRPC('Settings.SetSetingValue') + set_setting.execute({'setting': "services.webserver", 'value': True}) + + if not self.get_web_server(): +# self.dialog("ok", heading="{emby}", line1=self.Translate(33103)) + return Data + + result = get_setting.execute({'setting': "services.webserverport"}) + Data['webServerPort'] = str(result['result']['value'] or "") + result = get_setting.execute({'setting': "services.webserverusername"}) + Data['webServerUser'] = str(result['result']['value'] or "") + result = get_setting.execute({'setting': "services.webserverpassword"}) + Data['webServerPass'] = str(result['result']['value'] or "") + result = get_setting.execute({'setting': "services.webserverssl"}) + Data['webServerSSL'] = (result['result']['value'] or False) + Data['Enabled'] = True + return Data + + #Get if boxsets should be grouped + def get_grouped_set(self): + result = jsonrpc.JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) + + try: + return result['result']['value'] + except: + return False + + #Get the current screensaver value + def get_screensaver(self): + result = jsonrpc.JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) + + try: + return result['result']['value'] + except KeyError: + return "" + + def load_settings_bool(self, setting): + value = self.Addon.getSetting(setting) + + if value == "true": + setattr(self, setting, True) + else: + setattr(self, setting, False) + + def load_settings(self, setting): + value = self.Addon.getSetting(setting) + setattr(self, setting, value) + + def set_settings(self, setting, value): + setattr(self, setting, value) + self.Addon.setSetting(setting, value) + + def set_settings_bool(self, setting, value): + setattr(self, setting, value) + + if value: + self.Addon.setSetting(setting, "true") + else: + self.Addon.setSetting(setting, "false") diff --git a/helper/setup.py b/helper/setup.py index f454b519f..45974c327 100644 --- a/helper/setup.py +++ b/helper/setup.py @@ -1,31 +1,40 @@ # -*- coding: utf-8 -*- -#import logging - import xbmc import xbmcvfs import database.database -from . import translate -from . import utils +from . import loghandler class Setup(): def __init__(self, Utils): self.Utils = Utils -# self.LOG = logging.getLogger("EMBY.setup") -# self.LOG.info("---<[ setup ]") + self.LOG = loghandler.LOG('EMBY.helper.setup') + self.LOG.info("---<[ setup ]") #Setup playback mode. If native mode selected, check network credentials def _is_mode(self): - value = self.Utils.dialog("yesno", heading=translate._('playback_mode'), line1=translate._(33035), nolabel=translate._('addon_mode'), yeslabel=translate._('native_mode')) - self.Utils.settings('useDirectPaths', value="1" if value else "0") + value = self.Utils.dialog("yesno", heading=self.Utils.Translate('playback_mode'), line1=self.Utils.Translate(33035), nolabel=self.Utils.Translate('addon_mode'), yeslabel=self.Utils.Translate('native_mode')) if value: - self.Utils.dialog("ok", heading="{emby}", line1=translate._(33145)) + self.Utils.Settings.set_settings('useDirectPaths', "1") + self.Utils.direct_path = True + self.Utils.dialog("ok", heading="{emby}", line1=self.Utils.Translate(33145)) + else: + self.Utils.Settings.set_settings('useDirectPaths', "0") + self.Utils.direct_path = False + + def EnableUserRating(self): + value = self.Utils.dialog("yesno", heading="{emby}", line1="Enable userrating sync") + + if value: + self.Utils.Settings.set_settings_bool('userRating', True) + else: + self.Utils.Settings.set_settings_bool('userRating', False) def Migrate(self): Done = True - if not self.Utils.settings('Migrate.bool'): + if not self.Utils.Settings.Migrate: Source = self.Utils.translatePath("special://profile/addon_data/plugin.video.emby/") if xbmcvfs.exists(Source): @@ -55,50 +64,33 @@ def Migrate(self): if xbmcvfs.exists(Source): self.Utils.copy_file(Source, Dest) - self.Utils.settings('Migrate.bool', True) + self.Utils.Settings.set_settings_bool('Migrate', True) Done = False else: xbmc.executeJSONRPC('{"jsonrpc": "2.0", "id":1, "method": "Addons.SetAddonEnabled", "params": {"addonid": "plugin.video.emby-next-gen", "enabled": false}}') - self.Utils.settings('Migrate.bool', False) + self.Utils.Settings.set_settings_bool('Migrate', False) Done = False else: - self.Utils.settings('Migrate.bool', True) + self.Utils.Settings.set_settings_bool('Migrate', True) return Done def setup(self): minimum = "5.0.0" - cached = self.Utils.settings('MinimumSetup') + cached = self.Utils.Settings.MinimumSetup if cached == minimum: return - self.Utils.window('emby_reloadskin.bool', False) + self.EnableUserRating() + self.LOG.info("Userrating: %s" % self.Utils.Settings.userRating) if not cached: self._is_mode() -# self.LOG.info("Add-on playback: %s", settings('useDirectPaths') == "0") - self._is_empty_shows() -# self.LOG.info("Sync empty shows: %s", str(self.Utils.settings('syncEmptyShows.bool'))) - self._is_multiep() -# self.LOG.info("Enable multi episode label: %s", self.Utils.settings('displayMultiEpLabel.bool')) - self.Utils.window('emby_reloadskin.bool', True) + self.LOG.info("Add-on playback: %s" % self.Utils.Settings.useDirectPaths == "0") else: self.Utils.dialog("notification", heading="{emby}", message="Database reset required, please be patient!", icon="{emby}", time=15000, sound=True) - database.database.reset(True) - self.Utils.window('emby_reloadskin.bool', True) + database.database.reset(self.Utils, True) #Setup completed - self.Utils.settings('MinimumSetup', minimum) - - def _is_empty_shows(self): - value = self.Utils.dialog("yesno", heading="{emby}", line1=translate._(33100)) - self.Utils.settings('syncEmptyShows.bool', value) - - def _is_music(self): - value = self.Utils.dialog("yesno", heading="{emby}", line1=translate._(33039)) - self.Utils.settings('enableMusic.bool', value=value) - - def _is_multiep(self): - value = self.Utils.dialog("yesno", heading="{emby}", line1=translate._(33213)) - self.Utils.settings('displayMultiEpLabel.bool', value=value) + self.Utils.Settings.set_settings('MinimumSetup', minimum) diff --git a/helper/translate.py b/helper/translate.py deleted file mode 100644 index e3f47f5a7..000000000 --- a/helper/translate.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -import xbmc -import xbmcaddon - -#Get add-on string. Returns in unicode -def _(string): - if type(string) != int: - string = STRINGS[string] - - result = xbmcaddon.Addon('plugin.video.emby-next-gen').getLocalizedString(string) - - if not result: - result = xbmc.getLocalizedString(string) - - return result - -STRINGS = { - 'addon_name': 29999, - 'playback_mode': 30511, - 'empty_user': 30613, - 'empty_user_pass': 30608, - 'empty_server': 30617, - 'network_credentials': 30517, - 'invalid_auth': 33009, - 'addon_mode': 33036, - 'native_mode': 33037, - 'cancel': 30606, - 'username': 30024, - 'password': 30602, - 'gathering': 33021, - 'boxsets': 30185, - 'movies': 30302, - 'tvshows': 30305, - 'fav_movies': 30180, - 'fav_tvshows': 30181, - 'fav_episodes': 30182, - 'task_success': 33203, - 'task_fail': 33204 -} diff --git a/helper/utils.py b/helper/utils.py index ef952d04f..6c38dd143 100644 --- a/helper/utils.py +++ b/helper/utils.py @@ -1,122 +1,170 @@ # -*- coding: utf-8 -*- -import binascii import json -import logging import os import re import unicodedata +import uuid +import _strptime # Workaround for threads using datetime: _striptime is locked +from datetime import datetime, timedelta +from dateutil import tz, parser try: - from urllib import quote, quote_plus + from urllib import quote_plus, quote except: - from urllib.parse import quote, quote_plus + from urllib.parse import quote_plus, quote unicode = str -import uuid -import xml.etree.ElementTree -import requests -from dateutil import tz, parser - import xbmc import xbmcaddon import xbmcgui import xbmcvfs -from . import translate +from . import xmls +from . import settings +from . import loghandler +from . import jsonrpc class Utils(): def __init__(self): - self.LOG = logging.getLogger("EMBY.helper.utils.Utils") - self.KodiVersion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - self.Dialog = xbmcgui.Dialog() + self.LOG = loghandler.LOG('EMBY.helper.utils.Utils') self.Addon = xbmcaddon.Addon("plugin.video.emby-next-gen") + self.Settings = settings.Settings(self.Addon) self.WindowID = xbmcgui.Window(10000) + self.KodiVersion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + self.STRINGS = { + 'addon_name': 29999, + 'playback_mode': 30511, + 'empty_user': 30613, + 'empty_user_pass': 30608, + 'empty_server': 30617, + 'network_credentials': 30517, + 'invalid_auth': 33009, + 'addon_mode': 33036, + 'native_mode': 33037, + 'cancel': 30606, + 'username': 30024, + 'password': 30602, + 'gathering': 33021, + 'boxsets': 30185, + 'movies': 30302, + 'tvshows': 30305, + 'fav_movies': 30180, + 'fav_tvshows': 30181, + 'fav_episodes': 30182, + 'task_success': 33203, + 'task_fail': 33204 + } + self.Settings.InitSettings() + self.Dialog = xbmcgui.Dialog() + self.DeviceName = xbmc.getInfoLabel('System.FriendlyName') + self.DeviceID = "" + self.device_id = self.get_device_id(False) + self.SyncData = self.load_sync() + self.PathVerified = False + self.DefaultVideoSettings = None + self.addon_name = xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('name').upper() + self.addon_version = xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('version') + self.device_name = self.get_device_name() + self.direct_path = self.Settings.useDirectPaths == "1" + self.device_info = {'DeviceName': self.device_name, 'Version': self.addon_version, 'DeviceId': self.device_id} + self.Screensaver = None + self.SyncTimestampLast = datetime.fromtimestamp(0) + self.DatabaseFiles = self.load_DatabaseFiles() + + def load_DatabaseFiles(self): + DatabaseFiles = { + 'emby': self.translatePath("special://database/") + "emby.db", + 'texture': "", + 'texture-version': 0, + 'music': "", + 'music-version': 0, + 'video': "", + 'video-version': 0 + } - #Enable the webserver if not enabled. This is used to cache artwork. - #Will only test once, if it fails, user will be notified only once - def set_web_server(self): - get_setting = JSONRPC('Settings.GetSettingValue') + folder = self.translatePath("special://database/") + _, files = xbmcvfs.listdir(folder) - if not self.get_web_server(): - set_setting = JSONRPC('Settings.SetSetingValue') - set_setting.execute({'setting': "services.webserver", 'value': True}) + for Filename in files: + if not Filename.endswith('-wal') and not Filename.endswith('-shm') and not Filename.endswith('db-journal'): + if Filename.startswith('Textures'): + Version = int(''.join(i for i in Filename if i.isdigit())) - if not self.get_web_server(): - self.dialog("ok", heading="{emby}", line1=translate._(33103)) - return False + if Version > DatabaseFiles['texture-version']: + DatabaseFiles['texture'] = os.path.join(folder, Filename) + DatabaseFiles['texture-version'] = Version + elif Filename.startswith('MyMusic'): + Version = int(''.join(i for i in Filename if i.isdigit())) - result = get_setting.execute({'setting': "services.webserverport"}) - self.window('webServerPort', str(result['result']['value'] or "")) - result = get_setting.execute({'setting': "services.webserverusername"}) - self.window('webServerUser', str(result['result']['value'] or "")) - result = get_setting.execute({'setting': "services.webserverpassword"}) - self.window('webServerPass', str(result['result']['value'] or "")) - result = get_setting.execute({'setting': "services.webserverssl"}) - self.window('webServerSSL.bool', (result['result']['value'] or False)) - return True + if Version > DatabaseFiles['music-version']: + DatabaseFiles['music'] = os.path.join(folder, Filename) + DatabaseFiles['music-version'] = Version + elif Filename.startswith('MyVideos'): + Version = int(''.join(i for i in Filename if i.isdigit())) - def get_addon_name(self): - return xbmcaddon.Addon(self.addon_id()).getAddonInfo('name').upper() - - def get_version(self): - return xbmcaddon.Addon(self.addon_id()).getAddonInfo('version') - - def get_platform(self): - if xbmc.getCondVisibility('system.platform.osx'): - return "OSX" - elif xbmc.getCondVisibility('system.platform.atv2'): - return "ATV2" - elif xbmc.getCondVisibility('system.platform.ios'): - return "iOS" - elif xbmc.getCondVisibility('system.platform.windows'): - return "Windows" - elif xbmc.getCondVisibility('system.platform.android'): - return "Linux/Android" - elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): - return "Linux/RPi" - elif xbmc.getCondVisibility('system.platform.linux'): - return "Linux" - else: - return "Unknown" - - def get_distro(self): - if xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)'): - return "CoreElec" - elif xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'): - return "LibreElec" - elif xbmc.getCondVisibility('System.HasAddon(service.osmc.settings)'): - return "OSMC" - else: - return "Kodi" + if Version > DatabaseFiles['video-version']: + DatabaseFiles['video'] = os.path.join(folder, Filename) + DatabaseFiles['video-version'] = Version - def get_device_name(self): - ''' Detect the device name. If deviceNameOpt, then - use the device name in the add-on settings. - Otherwise fallback to the Kodi device name. - ''' - if not self.settings('deviceNameOpt.bool'): - device_name = xbmc.getInfoLabel('System.FriendlyName') - else: - device_name = self.settings('deviceName') - device_name = device_name.replace("\"", "_") - device_name = device_name.replace("/", "_") + return DatabaseFiles - if not device_name: - device_name = "Kodi" + def load_defaultvideosettings(self): + if not self.DefaultVideoSettings: #load from file + self.DefaultVideoSettings = xmls.Xmls(self).load_defaultvideosettings() - return device_name + def load_sync(self): + SyncData = self.Settings.emby_servers_sync - def get_device_id(self, reset=False): - ''' Return the device_id if already loaded. - It will load from emby_guid file. If it's a fresh - setup, it will generate a new GUID to uniquely - identify the setup for all users. - window prop: emby_deviceId - ''' - client_id = self.window('emby_deviceId') + if not SyncData: #load from file + path = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + if xbmcvfs.exists(os.path.join(path, "sync.json")): + with open(os.path.join(path, 'sync.json'), 'rb') as infile: + SyncData = json.load(infile) + else: + SyncData = {} + + SyncData['Libraries'] = SyncData.get('Libraries', []) + SyncData['RestorePoint'] = SyncData.get('RestorePoint', {}) + SyncData['Whitelist'] = list(set(SyncData.get('Whitelist', []))) + SyncData['SortedViews'] = SyncData.get('SortedViews', []) + self.Settings.emby_servers_sync = SyncData + + return SyncData + + def save_sync(self, Data, ForceSave=False): + CurrentDate = datetime.utcnow() + + if not ForceSave: + if (CurrentDate - self.SyncTimestampLast).seconds <= 30: #save every 30 seconds on sync progress + return + + self.SyncTimestampLast = CurrentDate + Data['Date'] = CurrentDate.strftime('%Y-%m-%dT%H:%M:%SZ') + path = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + with open(os.path.join(path, 'sync.json'), 'wb') as outfile: + output = json.dumps(Data, sort_keys=True, indent=4, ensure_ascii=False) + outfile.write(output.encode('utf-8')) + + self.Settings.emby_servers_sync = Data + self.SyncData = Data - if client_id: - return client_id + def save_last_sync(self): + time_now = datetime.utcnow() - timedelta(minutes=2) + last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') + self.Settings.set_settings('LastIncrementalSync', last_sync) + self.LOG.info("--[ sync/%s ]" % last_sync) + + def get_device_id(self, reset): + if self.DeviceID: + return self.DeviceID directory = self.translatePath('special://profile/addon_data/plugin.video.emby-next-gen/') @@ -125,191 +173,80 @@ def get_device_id(self, reset=False): emby_guid = os.path.join(directory, "emby_guid") file_guid = xbmcvfs.File(emby_guid) - client_id = file_guid.read() + self.DeviceID = file_guid.read() - if not client_id or reset: + if not self.DeviceID or reset: self.LOG.info("Generating a new GUID.") - client_id = str(self.create_id()) + self.DeviceID = str(self.create_id()) file_guid = xbmcvfs.File(emby_guid, 'w') - file_guid.write(client_id) + file_guid.write(self.DeviceID) file_guid.close() - self.LOG.info("DeviceId loaded: %s", client_id) - self.window('emby_deviceId', value=client_id) - return client_id + self.LOG.info("DeviceId loaded: %s" % self.DeviceID) + return self.DeviceID + + def get_device_name(self): + ''' Detect the device name. If deviceNameOpt, then + use the device name in the add-on settings. + Otherwise fallback to the Kodi device name. + ''' + if not self.Settings.deviceNameOpt: + device_name = self.DeviceName + else: + device_name = self.Settings.deviceName + device_name = device_name.replace("\"", "_") + device_name = device_name.replace("/", "_") + + if not device_name: + device_name = "Kodi" + + return device_name def reset_device_id(self): - self.window('emby_deviceId', clear=True) + self.DeviceID = "" self.get_device_id(True) - self.dialog("ok", heading="{emby}", line1=translate._(33033)) + self.dialog("ok", heading="{emby}", line1=self.Translate(33033)) xbmc.executebuiltin('RestartApp') - def get_info(self): - return { - 'DeviceName': self.get_device_name(), - 'Version': self.get_version(), - 'DeviceId': self.get_device_id() - } - #Download external subtitles to temp folder - def download_external_subs(self, src, filename): + def download_file_from_Embyserver(self, request, filename, EmbyServer): temp = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/temp/") if not xbmcvfs.exists(temp): xbmcvfs.mkdir(temp) path = os.path.join(temp, filename) + response = EmbyServer.http.request(request, True, True) - try: -# response = requests.get(src, stream=True, verify=False) - response = requests.get(src, stream=True) - response.raise_for_status() - response.encoding = 'utf-8' - + if response: with open(path, 'wb') as f: - f.write(response.content) - - del response - except: - path = None - - return path - - def StringMod(self, Data): - if self.KodiVersion >= 19: - return Data - - return Data.encode('utf-8') - - def StringDecode(self, Data): - if self.KodiVersion <= 18: - try: - Data = Data.decode('utf-8') - except: - Data = Data.encode('utf8').decode('utf-8') - - return Data - - def translatePath(self, Data): - if self.KodiVersion >= 19: - return xbmcvfs.translatePath(Data) - - return xbmc.translatePath(Data) - - def addon_id(self): - return "plugin.video.emby-next-gen" - - def kodi_version(self): - return self.KodiVersion - - def PathToFilenameReplaceSpecialCharecters(self, Path): - Temp = Path - Pos = Temp.rfind("/") - - if Pos == -1: #Windows - Pos = Temp.rfind("\\") + f.write(response) - Temp = Temp[Pos + 1:] + return path - if self.KodiVersion <= 18: - if isinstance(Temp, str): - Temp = unicode(Temp, 'utf-8') - Temp = Temp.encode('utf-8') - Filename = quote(Temp, safe=u':/'.encode('utf-8')) - else: - Filename = quote(Temp.encode('utf-8'), safe=u':/'.encode('utf-8')) - else: - Filename = quote(Temp) - - while Filename.find("%") != -1: - Pos = Filename.find("%") - Filename = Filename.replace(Filename[Pos:Pos + 3], "_") - - return Filename - - #Get or set window properties - def window(self, key, value=None, clear=False, WindowIDLocal=10000): - if WindowIDLocal == 10000: - WindowIDLocal = self.WindowID - else: - WindowIDLocal = xbmcgui.Window(WindowIDLocal) - - if clear: - self.LOG.debug("--[ window clear: %s ]", key) - WindowIDLocal.clearProperty(key.replace('.json', "").replace('.bool', "")) - elif value is not None: - if key.endswith('.json'): - key = key.replace('.json', "") - value = json.dumps(value) - elif key.endswith('.bool'): - key = key.replace('.bool', "") - value = "true" if value else "false" - - WindowIDLocal.setProperty(key, value) - else: - result = WindowIDLocal.getProperty(key.replace('.json', "").replace('.bool', "")) - - if result: - if key.endswith('.json'): - result = json.loads(result) - elif key.endswith('.bool'): - result = result in ("true", "1") - - return result - - #Get or add add-on settings. - #getSetting returns unicode object - def settings(self, setting, value=None): - self.Addon = xbmcaddon.Addon("plugin.video.emby-next-gen") - - if value is not None: - if setting.endswith('.bool'): - setting = setting.replace('.bool', "") - value = "true" if value else "false" - - self.Addon.setSetting(setting, value) - else: - result = self.Addon.getSetting(setting.replace('.bool', "")) - - if result and setting.endswith('.bool'): - result = result in ("true", "1") - - return result + return None def create_id(self): return uuid.uuid4() #Find value in dictionary - def find(self, dictData, item, beta=True): + def find(self, dictData, item, beta): if item in dictData: return dictData[item], item - for key, value in sorted(iter(list(dictData.items())), key=lambda k_v: (k_v[1], k_v[0])): + for key, _ in sorted(iter(list(dictData.items())), key=lambda k_v: (k_v[1], k_v[0])): if re.match(key, item, re.I): return dictData[key], key if beta: return self.find(dictData, item.replace('beta-', ""), False) - #Data is a dictionary - def event(self, method, data=None, sender=None, hexlify=False): - data = data or {"ServerId" : None} - sender = sender or "plugin.video.emby-next-gen" - - if hexlify: - data = '\\"[\\"{0}\\"]\\"'.format(binascii.hexlify(json.dumps(data))) - else: - data = '"[%s]"' % json.dumps(data).replace('"', '\\"') - - xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data)) - self.LOG.debug("---[ event: %s/%s ] %s", sender, method, data) - def dialog(self, dialog_type, *args, **kwargs): if "icon" in kwargs: kwargs['icon'] = kwargs['icon'].replace("{emby}", "special://home/addons/plugin.video.emby-next-gen/resources/icon.png") if "heading" in kwargs: - kwargs['heading'] = kwargs['heading'].replace("{emby}", translate._('addon_name')) + kwargs['heading'] = kwargs['heading'].replace("{emby}", self.Translate('addon_name')) if self.KodiVersion >= 19: if "line1" in kwargs: @@ -326,80 +263,45 @@ def dialog(self, dialog_type, *args, **kwargs): 'multi': self.Dialog.multiselect, 'textviewer': self.Dialog.textviewer } - return types[dialog_type](*args, **kwargs) - #Get webinterface settings - def get_webinterface(self): - Data = { - 'username': "kodi", - 'password': "", - 'port': "8080", - 'ssl': False, - 'enabled': False - } - - result = json.loads(xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.GetSettings", "params": {"filter": {"category": "control", "section": "services"}}}')) - - for ControlItem in result['result']['settings']: - if ControlItem['id'] == 'services.webserver': - Data['enabled'] = ControlItem['value'] - elif ControlItem['id'] == 'services.webserverusername': - Data['username'] = ControlItem['value'] - elif ControlItem['id'] == 'services.webserverport': - Data['port'] = ControlItem['value'] - elif ControlItem['id'] == 'services.webserverpassword': - Data['password'] = ControlItem['value'] - elif ControlItem['id'] == 'services.webserverssl': - Data['ssl'] = ControlItem['value'] - - return Data - - #Get the current screensaver value - def get_screensaver(self): - result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) - - try: - return result['result']['value'] - except KeyError: - return "" - #Toggle the screensaver def set_screensaver(self, value): params = { 'setting': "screensaver.mode", 'value': value } - result = JSONRPC('Settings.setSettingValue').execute(params) - self.LOG.info("---[ screensaver/%s ] %s", value, result) + result = jsonrpc.JSONRPC('Settings.setSettingValue').execute(params) + self.Screensaver = value + self.LOG.info("---[ screensaver/%s ] %s" % (value, result)) #Verify if path is accessible def validate(self, path): - if self.window('emby_pathverified.bool'): + if self.PathVerified: return True path = self.StringDecode(path) - string = "%s %s. %s" % (translate._(33047), path, translate._(33048)) + string = "%s %s. %s" % (self.Translate(33047), path, self.Translate(33048)) if not os.path.supports_unicode_filenames: path = path.encode('utf-8') if not xbmcvfs.exists(path): - self.LOG.info("Could not find %s", path) + self.LOG.info("Could not find %s" % path) if self.dialog("yesno", heading="{emby}", line1=string): return False - self.window('emby_pathverified.bool', True) + self.PathVerified = True return True #Grab the values in the item for a list of keys {key},{key1}.... #If the key has no brackets, the key will be passed as is def values(self, item, keys): - return (item[key.replace('{', "").replace('}', "")] if type(key) == str and key.startswith('{') else key for key in keys) + return (item[key.replace('{', "").replace('}', "")] if isinstance(key, str) and key.startswith('{') else key for key in keys) #Prettify xml docs - def indent(self, elem, level=0): + def indent(self, elem, level): try: i = "\n" + level * " " @@ -428,7 +330,7 @@ def write_xml(self, content, Filepath): self.LOG.error(error) #Delete objects from kodi cache - def delete_folder(self, path=None): + def delete_folder(self, path): self.LOG.debug("--[ delete folder ]") delete_path = path is not None path = path or self.translatePath('special://temp/emby') @@ -441,7 +343,7 @@ def delete_folder(self, path=None): if delete_path: xbmcvfs.delete(path) - self.LOG.warning("DELETE %s", path) + self.LOG.warning("DELETE %s" % path) #Delete files and dirs recursively def delete_recursive(self, path, dirs): @@ -455,7 +357,7 @@ def delete_recursive(self, path, dirs): xbmcvfs.rmdir(os.path.join(path, directory)) #Unzip file. zipfile module seems to fail on android with badziperror - def unzip(self, path, dest, folder=None): + def unzip(self, path, dest, folder): path = quote_plus(path) root = "zip://" + path + '/' @@ -472,7 +374,7 @@ def unzip(self, path, dest, folder=None): for Filename in files: self.unzip_file(os.path.join(root, Filename), os.path.join(dest, Filename)) - self.LOG.warning("Unzipped %s", path) + self.LOG.warning("Unzipped %s" % path) def unzip_recursive(self, path, dirs, dest): for directory in dirs: @@ -490,10 +392,10 @@ def unzip_recursive(self, path, dirs, dest): #Unzip specific file. Path should start with zip:// def unzip_file(self, path, dest): xbmcvfs.copy(path, dest) - self.LOG.debug("unzip: %s to %s", path, dest) + self.LOG.debug("unzip: %s to %s" % (path, dest)) def get_zip_directory(self, path, folder): - dirs, files = xbmcvfs.listdir(path) + dirs, _ = xbmcvfs.listdir(path) if folder in dirs: return os.path.join(path, folder) @@ -516,7 +418,7 @@ def copytree(self, path, dest): for Filename in files: self.copy_file(os.path.join(path, Filename), os.path.join(dest, Filename)) - self.LOG.info("Copied %s", path) + self.LOG.info("Copied %s" % path) def copy_recursive(self, path, dirs, dest): for directory in dirs: @@ -537,7 +439,7 @@ def copy_file(self, path, dest): return xbmcvfs.copy(path, dest) - self.LOG.debug("copy: %s to %s", path, dest) + self.LOG.debug("copy: %s to %s" % (path, dest)) #Delete pyo files to force Kodi to recreate them def delete_pyo(self, path): @@ -584,30 +486,20 @@ def direct_url(self, item): return path #Split up list in pieces of size. Will generate a list of lists - def split_list(self, itemlist, size): + def split_list(self, itemlist): + size = int(self.Settings.limitIndex) return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] - def get_resolution(self): - return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight')) - - def get_VideoBitrate(self): - VideoBitrate = [664000, 996000, 1320000, 2000000, 3200000, 4700000, 6200000, 7700000, 9200000, 10700000, 12200000, 13700000, 15200000, 16700000, 18200000, 20000000, 25000000, 30000000, 35000000, 40000000, 100000000, 1000000000] - return VideoBitrate[int(self.settings('videoBitrate'))] - - def get_AudioBitrate(self): - AudioBitrate = [64000, 96000, 128000, 192000, 256000, 320000, 384000, 448000, 512000] - return AudioBitrate[int(self.settings('audioBitrate'))] - #Convert the local datetime to local def convert_to_local(self, date): try: if not date: return "" - if type(date) is int: + if isinstance(date, int): date = str(date) - if type(date) in (unicode, str): + if isinstance(date, (str, unicode)): date = parser.parse(date.encode('utf-8')) date = date.replace(tzinfo=tz.tzutc()) @@ -615,89 +507,147 @@ def convert_to_local(self, date): return date.strftime('%Y-%m-%dT%H:%M:%S') except Exception as error: self.LOG.error(error) - self.LOG.info("date: %s", str(date)) - return str(date) + self.LOG.info("date: %s" % str(date)) + return "" - #Get if boxsets should be grouped - def get_grouped_set(self): - result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) + def set_queryIO(self, key): + self.WindowID.setProperty(key, "1") - try: - return result['result']['value'] - except: - return False + def StringDecode(self, Data): + if self.KodiVersion <= 18: + try: + Data = Data.decode('utf-8') + except: + Data = Data.encode('utf8').decode('utf-8') - def get_web_server(self): - result = JSONRPC('Settings.GetSettingValue').execute({'setting': "services.webserver"}) + return Data - try: - return result['result']['value'] - except (KeyError, TypeError): - return False + def Translate(self, String): + if isinstance(String, str): + String = self.STRINGS[String] - def enable_busy_dialog(self): - xbmc.executebuiltin('ActivateWindow(busydialognocancel)') + result = self.Addon.getLocalizedString(String) - def disable_busy_dialog(self): - xbmc.executebuiltin('Dialog.Close(busydialognocancel)') + if not result: + result = xbmc.getLocalizedString(String) - # Settings table for audio and subtitle tracks - def default_settings_default(self): - path = self.translatePath('special://profile/') - Filepath = os.path.join(path, 'guisettings.xml') + return result - try: - xmlData = xml.etree.ElementTree.parse(Filepath).getroot() - except Exception: - return + def PathToFilenameReplaceSpecialCharecters(self, Path): + Temp = Path + Pos = Temp.rfind("/") - default = xmlData.find('defaultvideosettings') - - return { - 'Deinterlace': default.find('interlacemethod').text, - 'ViewMode': default.find('viewmode').text, - 'ZoomAmount': default.find('zoomamount').text, - 'PixelRatio': default.find('pixelratio').text, - 'VerticalShift': default.find('verticalshift').text, - 'SubtitleDelay': default.find('subtitledelay').text, - 'ShowSubtitles': default.find('showsubtitles').text == 'true', - 'Brightness': default.find('brightness').text, - 'Contrast': default.find('contrast').text, - 'Gamma': default.find('gamma').text, - 'VolumeAmplification': default.find('volumeamplification').text, - 'AudioDelay': default.find('audiodelay').text, - 'Sharpness': default.find('sharpness').text, - 'NoiseReduction': default.find('noisereduction').text, - 'NonLinStretch': int(default.find('nonlinstretch').text == 'true'), - 'PostProcess': int(default.find('postprocess').text == 'true'), - 'ScalingMethod': default.find('scalingmethod').text, - 'StereoMode': default.find('stereomode').text, - 'CenterMixLevel': default.find('centermixlevel').text - } + if Pos == -1: #Windows + Pos = Temp.rfind("\\") + + Temp = Temp[Pos + 1:] + + if self.KodiVersion <= 18: + if isinstance(Temp, str): + Temp = unicode(Temp, 'utf-8') + Temp = Temp.encode('utf-8') + Filename = quote(Temp, safe=u':/'.encode('utf-8')) + else: + Filename = quote(Temp.encode('utf-8'), safe=u':/'.encode('utf-8')) + else: + Filename = quote(Temp) -class JSONRPC(): - version = 1 - jsonrpc = "2.0" + while Filename.find("%") != -1: + Pos = Filename.find("%") + Filename = Filename.replace(Filename[Pos:Pos + 3], "_") - def __init__(self, method, **kwargs): - self.method = method - self.params = None + return Filename - for arg in kwargs: - self.arg = kwargs[arg] + def ReplaceSpecialCharecters(self, Data): + if self.KodiVersion <= 18: + try: + Data = unicode(Data, 'utf-8') + except: + pass - def _query(self): - query = { - 'jsonrpc': self.jsonrpc, - 'id': self.version, - 'method': self.method, - } + Data = Data.encode('utf-8') + Data = quote(Data, safe=u':/'.encode('utf-8')) + else: + Data = quote(Data) + + Data = Data.replace("%", "") + return Data + + def StringMod(self, Data): + if self.KodiVersion >= 19: + return Data + + return Data.encode('utf-8') + + def translatePath(self, Data): + if self.KodiVersion >= 19: + return xbmcvfs.translatePath(Data) + + return xbmc.translatePath(Data) + + def CreateListitem(self, MediaType, Data): + li = xbmcgui.ListItem(Data['title']) + Data['mediatype'] = MediaType + Properties = {'IsPlayable': "true"} + + if "resume" in Data: + Properties['resumetime'] = str(Data['resume']['position']) + Properties['totaltime'] = str(Data['resume']['total']) + del Data['resume'] + + if "art" in Data: + li.setArt(Data['art']) + del Data['art'] + + if 'cast' in Data: + li.setCast(Data['cast']) + del Data['cast'] + + if 'uniqueid' in Data: + li.setUniqueIDs(Data['uniqueid']) + del Data['uniqueid'] + + if "streamdetails" in Data: + for key, value in list(Data['streamdetails'].items()): + for stream in value: + li.addStreamInfo(key, stream) + + del Data['streamdetails'] + + if "showtitle" in Data: + Data['TVshowTitle'] = Data['showtitle'] + del Data["showtitle"] + + if "firstaired" in Data: + Data['premiered'] = Data['firstaired'] + del Data["firstaired"] + + if "specialsortepisode" in Data: + Data['sortseason'] = Data['specialsortepisode'] + del Data["specialsortepisode"] + + if "specialsortseason" in Data: + Data['sortepisode'] = Data['specialsortseason'] + del Data["specialsortseason"] + + if "file" in Data: + del Data["file"] + + if "label" in Data: + li.setLabel(Data['label']) + del Data["label"] + + if "seasonid" in Data: + del Data["seasonid"] + + if "episodeid" in Data: + del Data["episodeid"] - if self.params is not None: - query['params'] = self.params + if "movieid" in Data: + del Data["movieid"] - return json.dumps(query) + if "musicvideoid" in Data: + del Data["musicvideoid"] - def execute(self, params=None): - self.params = params - return json.loads(xbmc.executeJSONRPC(self._query())) + li.setInfo('video', Data) + return li diff --git a/helper/wrapper.py b/helper/wrapper.py deleted file mode 100644 index 6b7782c33..000000000 --- a/helper/wrapper.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -import xbmcgui -import xbmc - -from . import translate -from . import exceptions -from . import utils - -Utils = utils.Utils() -LOG = logging.getLogger("EMBY.helper.wrapper") - -#Will start and close the progress dialog. -def progress(message=None): - def decorator(func): - def wrapper(self, item=None, *args, **kwargs): - dialog = xbmcgui.DialogProgressBG() - - if item and isinstance(item, dict): - dialog.create(translate._('addon_name'), "%s %s" % (translate._('gathering'), item['Name'])) - LOG.info("Processing %s: %s", item['Name'], item['Id']) - else: - dialog.create(translate._('addon_name'), message) - LOG.info("Processing %s", message) - - if item: - args = (item,) + args - - try: - result = func(self, dialog=dialog, *args, **kwargs) - dialog.close() - except Exception: - dialog.close() - raise - - return result - - return wrapper - return decorator - -#Wrapper to catch exceptions and return using catch -def stop(func): - def wrapper(*args, **kwargs): - if xbmc.Monitor().waitForAbort(0.00001) or Utils.window('emby_should_stop.bool') or not Utils.window('emby_online.bool'): - raise exceptions.LibraryException('StopCalled') - - if Utils.window('emby.sync.pause.bool'): - LOG.info("Stopping db writing!") - raise exceptions.LibraryException('StopWriteCalled') - - return func(*args, **kwargs) - return wrapper diff --git a/helper/xmls.py b/helper/xmls.py index 784273749..15a561345 100644 --- a/helper/xmls.py +++ b/helper/xmls.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -import logging import os import xml.etree.ElementTree +import xbmcvfs import xbmc -from . import translate + +from . import loghandler class Xmls(): def __init__(self, Utils): - self.LOG = logging.getLogger("EMBY.helper.xmls.Xmls") + self.LOG = loghandler.LOG('EMBY.helper.xmls.Xmls') self.Utils = Utils #Create master lock compatible sources. @@ -18,7 +19,7 @@ def sources(self): try: xmlData = xml.etree.ElementTree.parse(Filepath).getroot() - except Exception: + except: xmlData = xml.etree.ElementTree.Element('sources') video = xml.etree.ElementTree.SubElement(xmlData, 'video') files = xml.etree.ElementTree.SubElement(xmlData, 'files') @@ -60,16 +61,16 @@ def sources(self): xml.etree.ElementTree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "http://kodi.emby.media" xml.etree.ElementTree.SubElement(source, 'allowsharing').text = "true" except Exception as error: - self.LOG.exception(error) + self.LOG.error(error) - self.Utils.indent(xmlData) + self.Utils.indent(xmlData, 0) self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), Filepath) #Create tvtunes.nfo def tvtunes_nfo(self, path, urls): - try: + if xbmcvfs.exists(path): xmlData = xml.etree.ElementTree.parse(path).getroot() - except Exception: + else: xmlData = xml.etree.ElementTree.Element('tvtunes') for elem in xmlData.iter('tvtunes'): @@ -79,24 +80,50 @@ def tvtunes_nfo(self, path, urls): for url in urls: xml.etree.ElementTree.SubElement(xmlData, 'file').text = url - self.Utils.indent(xmlData) + self.Utils.indent(xmlData, 0) self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), path) + #Settings table for audio and subtitle tracks. + def load_defaultvideosettings(self): + try: #Skip fileread issues + path = xbmc.translatePath('special://profile/') + FilePath = os.path.join(path, 'guisettings.xml') + xmlData = xml.etree.ElementTree.parse(FilePath).getroot() + default = xmlData.find('defaultvideosettings') + return { + 'Deinterlace': default.find('interlacemethod').text, + 'ViewMode': default.find('viewmode').text, + 'ZoomAmount': default.find('zoomamount').text, + 'PixelRatio': default.find('pixelratio').text, + 'VerticalShift': default.find('verticalshift').text, + 'SubtitleDelay': default.find('subtitledelay').text, + 'ShowSubtitles': default.find('showsubtitles').text == 'true', + 'Brightness': default.find('brightness').text, + 'Contrast': default.find('contrast').text, + 'Gamma': default.find('gamma').text, + 'VolumeAmplification': default.find('volumeamplification').text, + 'AudioDelay': default.find('audiodelay').text, + 'Sharpness': default.find('sharpness').text, + 'NoiseReduction': default.find('noisereduction').text, + 'NonLinStretch': int(default.find('nonlinstretch').text == 'true'), + 'PostProcess': int(default.find('postprocess').text == 'true'), + 'ScalingMethod': default.find('scalingmethod').text, + 'StereoMode': default.find('stereomode').text, + 'CenterMixLevel': default.find('centermixlevel').text + } + except: + return {} + #Track the existence of true #It is incompatible with plugin paths. def advanced_settings(self): - if self.Utils.settings('useDirectPaths') != "0": - return - + video = None path = self.Utils.translatePath('special://profile/') Filepath = os.path.join(path, 'advancedsettings.xml') - try: + if xbmcvfs.exists(Filepath): xmlData = xml.etree.ElementTree.parse(Filepath).getroot() - except Exception: - return - - video = xmlData.find('videolibrary') + video = xmlData.find('videolibrary') if video is not None: cleanonupdate = video.find('cleanonupdate') @@ -104,8 +131,61 @@ def advanced_settings(self): if cleanonupdate is not None and cleanonupdate.text == "true": self.LOG.warning("cleanonupdate disabled") video.remove(cleanonupdate) - self.Utils.indent(xmlData) - self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), path) - self.Utils.dialog("ok", heading="{emby}", line1=translate._(33097)) + self.Utils.indent(xmlData, 0) + self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), Filepath) + self.Utils.dialog("ok", heading="{emby}", line1=self.Utils.Translate(33097)) xbmc.executebuiltin('RestartApp') - return True + + def advanced_settings_add_timeouts(self): + WriteData = False + path = self.Utils.translatePath('special://profile/') + Filepath = os.path.join(path, 'advancedsettings.xml') + + if xbmcvfs.exists(Filepath): + xmlData = xml.etree.ElementTree.parse(Filepath).getroot() + Network = xmlData.find('network') + + if Network is not None: + curlclienttimeout = Network.find('curlclienttimeout') + + if curlclienttimeout is not None: + if curlclienttimeout.text != "9999999": + self.LOG.warning("advancedsettings.xml set curlclienttimeout") + Network.remove(curlclienttimeout) + xml.etree.ElementTree.SubElement(Network, 'curlclienttimeout').text = "9999999" + WriteData = True + else: + xml.etree.ElementTree.SubElement(Network, 'curlclienttimeout').text = "9999999" + WriteData = True + + curllowspeedtime = Network.find('curllowspeedtime') + + if curllowspeedtime is not None: + if curllowspeedtime.text != "9999999": + self.LOG.warning("advancedsettings.xml set curllowspeedtime") + Network.remove(curllowspeedtime) + xml.etree.ElementTree.SubElement(Network, 'curllowspeedtime').text = "9999999" + WriteData = True + else: + xml.etree.ElementTree.SubElement(Network, 'curllowspeedtime').text = "9999999" + WriteData = True + else: + self.LOG.warning("advancedsettings.xml set network") + Network = xml.etree.ElementTree.SubElement(xmlData, 'network') + xml.etree.ElementTree.SubElement(Network, 'curllowspeedtime').text = "9999999" + xml.etree.ElementTree.SubElement(Network, 'curlclienttimeout').text = "9999999" + WriteData = True + + else: + self.LOG.warning("advancedsettings.xml set data") + xmlData = xml.etree.ElementTree.Element('advancedsettings') + Network = xml.etree.ElementTree.SubElement(xmlData, 'network') + xml.etree.ElementTree.SubElement(Network, 'curllowspeedtime').text = "9999999" + xml.etree.ElementTree.SubElement(Network, 'curlclienttimeout').text = "9999999" + WriteData = True + + if WriteData: + self.Utils.indent(xmlData, 0) + self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), Filepath) + self.Utils.dialog("ok", heading="{emby}", line1="Network timeouts modified in advancedsettings.xml, Kodi will reboot now") + xbmc.executebuiltin('RestartApp') diff --git a/hooks/monitor.py b/hooks/monitor.py index 8ff9c50ae..d6bd24ddf 100644 --- a/hooks/monitor.py +++ b/hooks/monitor.py @@ -1,26 +1,23 @@ # -*- coding: utf-8 -*- import json -import logging import threading -import uuid - -try: #python 2 - import imp -except: #python 3 - import importlib as imp +import os import xbmc -import xbmcgui -import helper.api -import helper.utils -import helper.translate +import xbmcvfs +import xbmcaddon + +import helper.loghandler +import helper.xmls +import helper.jsonrpc +import helper.context +import helper.pluginmenu import database.database import database.library -import emby.main import emby.connect -import emby.views import hooks.webservice import core.listitem +import core.artwork from . import player try: @@ -29,161 +26,154 @@ import Queue class Monitor(xbmc.Monitor): - def __init__(self): - self.LOG = logging.getLogger("EMBY.hooks.monitor.Monitor") + def __init__(self, Service): + self.LOG = helper.loghandler.LOG('EMBY.hooks.monitor.Monitor') self.sleep = False - self.library = None - self.servers = [] - self.Utils = helper.utils.Utils() - self.device_id = self.Utils.get_device_id() + self.EmbyServers = {} + self.library = {} self.ServerIP = None self.ServerToken = None self.Server = None - self.DeviceID = None - self.connect = emby.connect.Connect(self.Utils) - self.auth_check = True - self.warn = False - self.ProgressThread = None self.WebServiceThread = None - self.log_level = self.Utils.settings('logLevel') or "1" self.WebserviceOnPlayThread = None self.WebserviceEventOut = Queue.Queue() self.WebserviceEventIn = Queue.Queue() - self.player = player.PlayerEvents(monitor=self) - self.server = [] - self.Service = None - self.ItemSkipUpdate = [] - self.ItemSkipUpdateAfterStop = [] - self.ItemSkipUpdateReset = False - self.PlaySessionIdLast = "" - self.PlaySessionId = "" - self.MediasourceID = "" - self.Trailer = False - self.PlayerReloadIndex = "-1" - self.PlayerLastItem = "" - self.PlayerLastItemID = "-1" - - self.PlayWebsocketPreviousCommand = "" #Propably an issue from Emby Server -> Same command send twice - xbmc.Monitor.__init__(self) - - def Monitor_waitForAbort(self, Data): - self.waitForAbort(Data) - - def LibraryStop(self): - if self.library is not None: - self.library.stop_client() - self.library = None - - def Register(self): - self.connect.register() - - def LibraryLoad(self): - if self.library is None: - self.library = database.library.Library(self.Utils) - - #Retrieve the Emby server. - def _get_server(self, method, data): - try: - if not data.get('ServerId'): - raise Exception("ServerId undefined.") - - if method != 'LoadServer' and data['ServerId'] not in self.servers: - try: - self.connect.register(data['ServerId']) - self.server_instance(data['ServerId']) - except Exception as error: - self.LOG.error(error) - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33142)) - return - - server = emby.main.Emby(data['ServerId']).get_client() - except Exception: - server = emby.main.Emby().get_client() - - return server + self.Service = Service + self.player = player.PlayerEvents() + self.Context = helper.context.Context(self.Service.Utils, self.EmbyServers, self.library) + self.Menu = helper.pluginmenu.Menu(self.Service.Utils, self.EmbyServers, self.player) + self.Xmls = helper.xmls.Xmls(self.Service.Utils) + self.connect = emby.connect.Connect(self.Service.Utils) #multipe servers here!!!!!!!!!!!!!!!!!!!!!!!!!!! + + def EmbyServer_ReconnectAll(self): + for server_id in self.EmbyServers: + self.Service.ServerReconnect(server_id, False) + + def EmbyServer_DisconnectAll(self): + for server_id in self.EmbyServers: + self.EmbyServers[server_id].stop() + self.EmbyServers[server_id].Online = False + self.StopServer({'ServerId': server_id}) + + def EmbyServer_Connect(self): + server_id, EmbyServer = self.connect.register({}) + + if server_id == 'cancel': + return False + + if not server_id: + self.LOG.error("EmbyServer Connect error") + return False + + self.EmbyServers[server_id] = EmbyServer + self.EmbyServers[server_id].start() + self.ServerOnline({'ServerId': server_id}) + return server_id + + #Add theme media locally, via strm. This is only for tv tunes. + #If another script is used, adjust this code + def SyncThemes(self, data): + library = self.Service.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/library") + + if not xbmcvfs.exists(library + '/'): + xbmcvfs.mkdir(library) + + if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): + tvtunes = xbmcaddon.Addon(id="script.tvtunes") + tvtunes.setSetting('custom_path_enable', "true") + tvtunes.setSetting('custom_path', library) + self.LOG.info("TV Tunes custom path is enabled and set.") + elif xbmc.getCondVisibility('System.HasAddon(service.tvtunes)'): + tvtunes = xbmcaddon.Addon(id="service.tvtunes") + tvtunes.setSetting('custom_path_enable', "true") + tvtunes.setSetting('custom_path', library) + self.LOG.info("TV Tunes custom path is enabled and set.") + else: + self.Service.Utils.dialog("ok", heading="{emby}", line1=self.Service.Utils.Translate(33152)) + return + + with database.database.Database(self.Service.Utils, 'emby', False) as embydb: + all_views = database.emby_db.EmbyDatabase(embydb.cursor).get_views() + views = [x[0] for x in all_views if x[2] in ('movies', 'tvshows', 'mixed')] + + items = {} + + for view in views: + for result in self.EmbyServers[data['ServerId']].API.get_itemsSync(view, None, False, {'HasThemeVideo': True}): + for item in result['Items']: + folder = self.Service.Utils.normalize_string(item['Name']) + items[item['Id']] = folder + + for result in self.EmbyServers[data['ServerId']].API.get_itemsSync(view, None, False, {'HasThemeSong': True}): + for item in result['Items']: + folder = self.Service.Utils.normalize_string(item['Name']) + items[item['Id']] = folder + + for item in items: + nfo_path = os.path.join(library, items[item]) + nfo_file = os.path.join(nfo_path, "tvtunes.nfo") + + if not xbmcvfs.exists(nfo_path): + xbmcvfs.mkdir(nfo_path) + + themes = self.EmbyServers[data['ServerId']].API.get_themes(item) + paths = [] + + for theme in themes['ThemeVideosResult']['Items'] + themes['ThemeSongsResult']['Items']: + if self.Service.Utils.direct_path: + paths.append(theme['MediaSources'][0]['Path']) + else: + paths.append(self.Service.Utils.direct_url(theme)) + + self.Xmls.tvtunes_nfo(nfo_file, paths) + + self.Service.Utils.dialog("notification", heading="{emby}", message=self.Service.Utils.Translate(33153), icon="{emby}", time=1000, sound=False) + + def DatabaseReset(self): + database.database.reset(self.Service.Utils, False) + + def TextureCache(self): + core.artwork.Artwork(None, self.Service.Utils).cache_textures() + + def LibraryStopAll(self): + for server_id in self.library: + self.library[server_id].stop_thread = True + + self.library = {} + + def LibraryStop(self, server_id): + if server_id in self.library: + self.library[server_id].stop_thread = True + del self.library[server_id] + + def LibraryLoad(self, server_id): + if not server_id in self.library: + self.ServerOnline({'ServerId': server_id}) + self.library[server_id] = database.library.Library(self.player, self.EmbyServers[server_id]) def onScanStarted(self, library): - self.LOG.info("-->[ kodi scan/%s ]", library) + self.LOG.info("-->[ kodi scan/%s ]" % library) def onScanFinished(self, library): - self.LOG.info("--<[ kodi scan/%s ]", library) + self.LOG.info("--<[ kodi scan/%s ]" % library) def onSettingsChanged(self): - new_thread = MonitorWorker(self, None, "settingschanged", None, None, self.device_id) + new_thread = MonitorWorker(self, None, "settingschanged", None) new_thread.start() def onNotification(self, sender, method, data): - if sender.lower() not in ('plugin.video.emby-next-gen', 'xbmc', 'upnextprovider.signal'): + if sender.lower() not in ('plugin.video.emby-next-gen', 'plugin.video.emby', 'xbmc', 'upnextprovider.signal'): return if self.sleep: self.LOG.info("System.OnSleep detected, ignore monitor request.") return - server = self._get_server(method, data) - new_thread = MonitorWorker(self, sender, method, data, server) + new_thread = MonitorWorker(self, sender, method, data) new_thread.start() - def server_instance(self, server_id=None): - server = emby.main.Emby(server_id).get_client() - self.post_capabilities(server) - - if server_id is not None: - self.servers.append(server_id) - elif self.Utils.settings('addUsers'): - users = self.Utils.settings('addUsers').split(',') - hidden = None if self.Utils.settings('addUsersHidden.bool') else False - all_users = server['api'].get_users(hidden=hidden) - - for additional in users: - for user in all_users: - if user['Id'] == additional: - server['api'].session_add_user(server['config/app.session'], user['Id']) - - self.additional_users(server) - - self.Utils.event('ServerOnline', {'ServerId': server_id}) - - def post_capabilities(self, server): - self.LOG.info("--[ post capabilities/%s ]", server['auth/server-id']) - server['api'].post_capabilities({ - 'PlayableMediaTypes': "Audio,Video", - 'SupportsMediaControl': 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," - "Play,Playstate,PlayNext,PlayMediaSource" - ), - 'IconUrl': "https://raw.githubusercontent.com/MediaBrowser/plugin.video.emby/master/kodi_icon.png" - }) - - session = server['api'].get_device(self.device_id) - server['config']['app.session'] = session[0]['Id'] - - def additional_users(self, server): - for i in range(10): - self.Utils.window('EmbyAdditionalUserImage.%s' % i, clear=True) - - try: - session = server['api'].get_device(self.device_id) - except Exception as error: - self.LOG.error(error) - return - - for index, user in enumerate(session[0]['AdditionalUsers']): - info = server['api'].get_user(user['UserId']) - image = helper.api.API(info, self.Utils, server['config/auth.server']).get_user_artwork(user['UserId']) - self.Utils.window('EmbyAdditionalUserImage.%s' % index, image) - self.Utils.window('EmbyAdditionalUserPosition.%s' % user['UserId'], str(index)) - #Emby playstate updates. - def Playstate(self, server, data): + def Playstate(self, data): command = data['Command'] actions = { 'Stop': self.player.stop, @@ -198,13 +188,13 @@ def Playstate(self, server, data): if self.player.isPlaying(): seektime = data['SeekPositionTicks'] / 10000000.0 self.player.seekTime(seektime) - self.LOG.info("[ seek/%s ]", seektime) + self.LOG.info("[ seek/%s ]" % seektime) elif command in actions: actions[command]() - self.LOG.info("[ command/%s ]", command) + self.LOG.info("[ command/%s ]" % command) #General commands from Emby to control the Kodi interface. - def GeneralCommand(self, server, data): + def GeneralCommand(self, data): command = data['Name'] args = data['Arguments'] @@ -224,13 +214,13 @@ def GeneralCommand(self, server, data): self.player.report_playback() elif command == 'DisplayMessage': - self.Utils.dialog("notification", heading=args['Header'], message=args['Text'], icon="{emby}", time=int(self.Utils.settings('displayMessage')) * 1000) + self.Service.Utils.dialog("notification", heading=args['Header'], message=args['Text'], icon="{emby}", time=int(self.Service.Utils.Settings.displayMessage) * 1000) elif command == 'SendString': - helper.utils.JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) + helper.jsonrpc.JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) elif command == 'GoHome': - helper.utils.JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) + helper.jsonrpc.JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) elif command == 'Guide': - helper.utils.JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) + helper.jsonrpc.JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): actions = { 'MoveUp': "Input.Up", @@ -238,8 +228,7 @@ def GeneralCommand(self, server, data): 'MoveRight': "Input.Right", 'MoveLeft': "Input.Left" } - - helper.utils.JSONRPC(actions[command]).execute() + helper.jsonrpc.JSONRPC(actions[command]).execute(False) else: builtin = { 'ToggleFullscreen': 'Action(FullScreen)', @@ -262,123 +251,33 @@ def GeneralCommand(self, server, data): if command in builtin: xbmc.executebuiltin(builtin[command]) - def LoadServer(self, server, data): - self.server_instance(data['ServerId']) - - if not data['ServerId']: - self.Utils.window('emby.server.state.json', server.get_state()) - else: - self.Utils.window('emby.server.%s.state.json' % data['ServerId'], server.get_state()) - current = self.Utils.window('emby.server.states.json') or [] - current.append(data['ServerId']) - self.Utils.window('emby.server.states.json', current) - - def StopServer(self, server, data): - if not data['ServerId']: - self.Utils.window('emby.server.state', clear=True) - else: - self.Utils.window('emby.server.%s.state' % data['ServerId'], clear=True) - current = self.Utils.window('emby.server.states.json') - current.pop(current.index(data['ServerId'])) - self.Utils.window('emby.server.states.json', current) + def StopServer(self, data): + self.EmbyServers[data['ServerId']].Online = False def Player_OnAVChange(self, *args, **kwargs): self.ReportProgressRequested(*args, **kwargs) - def ReportProgressRequested(self, server, data): - if not self.Trailer: + def ReportProgressRequested(self, data): + if not self.player.Trailer: self.player.report_playback() - def Player_OnStop(self, server, data): - if self.ProgressThread: - self.ProgressThread.Stop() - self.ProgressThread = None - - server['api'].close_transcode(self.device_id) - self.Utils.window('emby.sync.pause.bool', clear=True) - - def Player_OnPlay(self, server, data): - self.Utils.window('emby.sync.pause.bool', True) - current_file = self.player.get_playing_file() - - if not "id" in data['item']: - if 'title' in data['item']: - DynamicItemId = self.Utils.window('emby_DynamicItem_' + current_file) - - if not DynamicItemId: - return - else: - return - - if self.ProgressThread: - self.ProgressThread.Stop() - self.ProgressThread = None - - if not self.Trailer: - CurrentItem = None - - if current_file: - if not "id" in data['item']: - CurrentItem = dict([ - ('Type', data['item']['type']), - ('Id', DynamicItemId), - ('Path', current_file), - ('MediaSourceId', self.MediasourceID), - ('ServerId', None), - ('Server', server), - ('Paused', False), - ('PlaySessionId', self.PlaySessionId), - ('RunTime', -1), - ('CurrentPosition', -1), - ('DeviceId', self.Utils.window('emby_deviceId')) - ]) - else: - kodi_id = data['item']['id'] - media_type = data['item']['type'] - item = database.database.get_item(kodi_id, media_type) - - if item: - CurrentItem = dict([ - ('Type', media_type), - ('Id', item[0]), - ('Path', current_file), - ('MediaSourceId', self.MediasourceID), - ('ServerId', None), - ('Server', server), - ('Paused', False), - ('PlaySessionId', self.PlaySessionId), - ('RunTime', -1), - ('CurrentPosition', -1), - ('DeviceId', self.Utils.window('emby_deviceId')) - ]) - - if CurrentItem: #else native Kodi - #native mode - if not "127.0.0.1" in current_file: - CurrentItem['PlaySessionId'] = str(uuid.uuid4()).replace("-", "") - else: - if self.PlaySessionIdLast == CurrentItem['PlaySessionId']: - for i in range(60): - xbmc.sleep(500) + def Player_OnStop(self, data): + for server_id in self.EmbyServers: ######################## WORKAROUND!!!!!!!!!!! + break - if self.PlaySessionIdLast != self.PlaySessionId: - break + self.player.OnStop(self.EmbyServers[server_id]) - CurrentItem['PlaySessionId'] = self.PlaySessionId - CurrentItem['MediaSourceId'] = self.MediasourceID + def Player_OnPlay(self, data): + for server_id in self.EmbyServers: ######################## WORKAROUND!!!!!!!!!!! + break - self.PlaySessionIdLast = CurrentItem['PlaySessionId'] - self.player.set_item(CurrentItem) + self.player.OnPlay(data, self.EmbyServers[server_id], self.library[server_id]) - if not self.ProgressThread: - self.ProgressThread = player.ProgressUpdates(self.player) - self.ProgressThread.start() - - def AddPlaylistItem(self, Position, EmbyID, server, Offset=0): - Data = database.database.get_kodiID(str(EmbyID)) + def AddPlaylistItem(self, Position, EmbyID, server_id, Offset): + Data = database.database.get_kodiID(self.Service.Utils, str(EmbyID)) if Data: #Requested video is synced to KodiDB. No additional info required - if Data[0][1] == "audio": + if Data[0][1] in ("song", "album", "artist"): playlistID = 0 playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) else: @@ -386,12 +285,12 @@ def AddPlaylistItem(self, Position, EmbyID, server, Offset=0): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) Pos = self.GetPlaylistPos(Position, playlist, Offset) - helper.utils.JSONRPC('Playlist.Insert').execute({'playlistid': playlistID, 'position': Pos, 'item': {'%sid' % Data[0][1]: int(Data[0][0])}}) + + helper.jsonrpc.JSONRPC('Playlist.Insert').execute({'playlistid': playlistID, 'position': Pos, 'item': {'%sid' % Data[0][1]: int(Data[0][0])}}) else: - listitems = core.listitem.ListItem(server['auth/server-address'], self.Utils) - item = server['api'].get_item(EmbyID) - li = xbmcgui.ListItem() - li = listitems.set(item, li, None, False, None) + listitems = core.listitem.ListItem(self.Service.Utils) + item = self.EmbyServers[server_id].API.get_item(EmbyID) + li = listitems.set(item) path = "" if item['Type'] == "MusicVideo": @@ -414,7 +313,7 @@ def AddPlaylistItem(self, Position, EmbyID, server, Offset=0): if not path: if 'MediaSources' in item: - FilenameURL = self.Utils.PathToFilenameReplaceSpecialCharecters(item['Path']) + FilenameURL = self.Service.Utils.PathToFilenameReplaceSpecialCharecters(item['Path']) if len(item['MediaSources'][0]['MediaStreams']) >= 1: path = "http://127.0.0.1:57578/%s/%s-%s-%s-stream-%s" % (Type, item['Id'], item['MediaSources'][0]['Id'], item['MediaSources'][0]['MediaStreams'][0]['BitRate'], FilenameURL) @@ -429,7 +328,7 @@ def AddPlaylistItem(self, Position, EmbyID, server, Offset=0): playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) Pos = self.GetPlaylistPos(Position, playlist, Offset) - self.Utils.window('emby_DynamicItem_' + path, item['Id']) + self.player.DynamicItem[self.Service.Utils.ReplaceSpecialCharecters(li.getLabel())] = item['Id'] playlist.add(path, li, index=Pos) return playlist @@ -455,11 +354,7 @@ def GetPlaylistPos(self, Position, playlist, Offset): return Pos #Websocket command from Emby server - def Play(self, server, data): - if self.PlayWebsocketPreviousCommand == data: - return - - self.PlayWebsocketPreviousCommand = data + def Play(self, data): FirstItem = True Offset = 0 @@ -467,12 +362,12 @@ def Play(self, server, data): Offset += 1 if data["PlayCommand"] == "PlayNow": - playlist = self.AddPlaylistItem("current", ID, server, Offset) + playlist = self.AddPlaylistItem("current", ID, data['ServerId'], Offset) elif data["PlayCommand"] == "PlayNext": - playlist = self.AddPlaylistItem("current", ID, server, Offset) + playlist = self.AddPlaylistItem("current", ID, data['ServerId'], Offset) elif data["PlayCommand"] == "PlayLast": - playlist = self.AddPlaylistItem("last", ID, server) + playlist = self.AddPlaylistItem("last", ID, data['ServerId'], 0) #Play Item if data["PlayCommand"] == "PlayNow": @@ -484,10 +379,10 @@ def Play(self, server, data): if Pos == -1: Pos = 0 - xbmc.Player().play(item=playlist, startpos=Pos + Offset) + self.player.play(item=playlist, startpos=Pos + Offset) if "StartPositionTicks" in data: - xbmc.Player().seekTime(int(data["StartPositionTicks"]) / 100000) + self.player.seekTime(int(data["StartPositionTicks"]) / 100000) Offset = 0 FirstItem = False @@ -498,336 +393,452 @@ def Play(self, server, data): if Pos == -1: Pos = 0 - xbmc.Player().play(item=playlist, startpos=Pos + Offset) + self.player.play(item=playlist, startpos=Pos + Offset) if "StartPositionTicks" in data: - xbmc.Player().seekTime(int(data["StartPositionTicks"]) / 100000) + self.player.seekTime(int(data["StartPositionTicks"]) / 100000) Offset = 0 FirstItem = False - def Playlist_OnClear(self, server, data): + def Playlist_OnClear(self, data): self.player.played = {} - def ServerConnect(self, server, data): - self.connect.register(data['Id']) + def ServerConnect(self, data): + self.EmbyServer_Connect() xbmc.executebuiltin("Container.Refresh") - def EmbyConnect(self, server, data): - self.connect.setup_login_connect() - - def RemoveServer(self, server, data): - self.connect.remove_server(data['Id']) + def RemoveServer(self, data): + self.EmbyServers[data['ServerId']].close() + self.connect.remove_server(data['ServerId']) xbmc.executebuiltin("Container.Refresh") - def UserDataChanged(self, server, data): - if not self.library and data.get('ServerId') or not self.library.started: + def UserDataChanged(self, data): + if data.get('UserId') != self.EmbyServers[data['ServerId']].auth.get_server_info()['UserId']: return - if data.get('UserId') != emby.main.Emby()['auth/user-id']: - return - - self.LOG.info("[ UserDataChanged ] %s", data) + self.LOG.info("[ UserDataChanged ] %s" % data) UpdateData = [] for ItemData in data['UserDataList']: - if not ItemData['ItemId'] in self.ItemSkipUpdate: #Check EmbyID - item = database.database.get_Presentationkey(ItemData['ItemId']) + if not ItemData['ItemId'] in self.player.ItemSkipUpdate: #Check EmbyID + item = database.database.get_Presentationkey(self.Service.Utils, ItemData['ItemId']) if item: PresentationKey = item.split("-") - if not PresentationKey[0] in self.ItemSkipUpdate: #Check PresentationKey + if not PresentationKey[0] in self.player.ItemSkipUpdate: #Check PresentationKey UpdateData.append(ItemData) else: - self.LOG.info("[ skip update/%s ]", ItemData['ItemId']) + self.LOG.info("[ skip update/%s ]" % ItemData['ItemId']) else: UpdateData.append(ItemData) else: - self.LOG.info("[ skip update/%s ]", ItemData['ItemId']) + self.LOG.info("[ skip update/%s ]" % ItemData['ItemId']) if UpdateData: - self.library.userdata(UpdateData) + self.library[data['ServerId']].userdata(UpdateData) - if self.ItemSkipUpdateReset: - self.ItemSkipUpdateReset = False - self.ItemSkipUpdate = [] + if self.player.ItemSkipUpdateReset: + self.player.ItemSkipUpdateReset = False + self.player.ItemSkipUpdate = [] self.LOG.info("[ skip reset ]") - def LibraryChanged(self, server, data): - if data.get('ServerId') or not self.library.started: - return - - self.LOG.info("[ LibraryChanged ] %s", data) - self.library.updated(data['ItemsUpdated'] + data['ItemsAdded']) - self.library.removed(data['ItemsRemoved']) - self.library.delay_verify(data.get('ItemsVerify', [])) - - def WebSocketRestarting(self, server, data): - if self.library: - try: - self.library.get_fast_sync() - except Exception as error: - self.LOG.error(error) - - def SyncLibrarySelection(self, server, data): - self.library.select_libraries("SyncLibrarySelection") - - def RepairLibrarySelection(self, server, data): - self.library.select_libraries("RepairLibrarySelection") - - def AddLibrarySelection(self, server, data): - self.library.select_libraries("AddLibrarySelection") - - def RemoveLibrarySelection(self, server, data): - self.library.select_libraries("RemoveLibrarySelection") + def LibraryChanged(self, data): + self.LOG.info("[ LibraryChanged ] %s" % data) + self.library[data['ServerId']].updated(data['ItemsUpdated'] + data['ItemsAdded']) + self.library[data['ServerId']].removed(data['ItemsRemoved']) + self.library[data['ServerId']].delay_verify(data.get('ItemsVerify', [])) - def ServerUnreachable(self, server, data): - if self.warn or data.get('ServerId'): - self.warn = data.get('ServerId') is not None - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33146) if data.get('ServerId') is None else helper.translate._(33149), icon=xbmcgui.NOTIFICATION_ERROR) + def WebSocketRestarting(self, data): + self.library[data['ServerId']].get_fast_sync() - if data.get('ServerId') is None: - self.Service.Server(20) + def SyncLibrarySelection(self, data): + self.library[data['ServerId']].select_libraries("SyncLibrarySelection") - def ServerShuttingDown(self, server, data): - if self.warn or data.get('ServerId'): - self.warn = data.get('ServerId') is not None - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33146) if data.get('ServerId') is None else helper.translate._(33149), icon=xbmcgui.NOTIFICATION_ERROR) + def RepairLibrarySelection(self, data): + self.library[data['ServerId']].select_libraries("RepairLibrarySelection") - if data.get('ServerId') is None: - self.Service.Server(20) + def AddLibrarySelection(self, data): + self.library[data['ServerId']].select_libraries("AddLibrarySelection") - def ServiceHandle(self, Handle): - self.Service = Handle + def RemoveLibrarySelection(self, data): + self.library[data['ServerId']].select_libraries("RemoveLibrarySelection") - def UserConfigurationUpdated(self, server, data): - if data.get('ServerId') is None: - emby.views.Views(self.Utils).get_views() - - def UserPolicyUpdated(self, server, data): - if data.get('ServerId') is None: - emby.views.Views(self.Utils).get_views() - - def SyncLibrary(self, server, data): - if not data.get('Id'): + def ServerUnreachable(self, data): + if self.Service.ServerReconnectingInProgress(data['ServerId']): return - self.library.add_library(data['Id'], data.get('Update', False)) + self.Service.Utils.dialog("notification", heading="{emby}", message=self.Service.Utils.Translate(33146)) + self.EmbyServers[data['ServerId']].Online = False + self.Service.ServerReconnect(data['ServerId']) - def RepairLibrary(self, server, data): - if not data.get('Id'): + def ServerShuttingDown(self, data): + if self.Service.ServerReconnectingInProgress(data['ServerId']): return - libraries = data['Id'].split(',') - - for lib in libraries: - self.library.remove_library(lib) - - self.library.add_library(data['Id']) + self.Service.Utils.dialog("notification", heading="{emby}", message="Enable server shutdown") + self.EmbyServers[data['ServerId']].Online = False + self.Service.ServerReconnect(data['ServerId']) - def RemoveLibrary(self, server, data): - libraries = data['Id'].split(',') + def UserConfigurationUpdated(self, data): + self.Service.Views.get_views() - for lib in libraries: - self.library.remove_library(lib) + def UserPolicyUpdated(self, data): + self.Service.Views.get_views() - def GUI_OnScreensaverDeactivated(self, server, data): + def GUI_OnScreensaverDeactivated(self, data): self.LOG.info("--<[ screensaver ]") xbmc.sleep(5000) - if self.library is not None: - self.library.get_fast_sync() + if data['ServerId'] in self.library: + self.library[data['ServerId']].get_fast_sync() - def PatchMusic(self, server, data): - self.library.patch_music(data.get('Notification', True)) + def PatchMusic(self, data): + self.library[data['ServerId']].patch_music(data.get('Notification', True)) - def Unauthorized(self, server, data): - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33147) if data['ServerId'] is None else helper.translate._(33148), icon=xbmcgui.NOTIFICATION_ERROR) + def Unauthorized(self, data): + self.EmbyServers[data['ServerId']].Online = False + self.Service.Utils.dialog("notification", heading="{emby}", message=self.Service.Utils.Translate(33147)) - if data.get('ServerId') is None and self.auth_check: - self.auth_check = False - self.Service.Server(120) - - def System_OnQuit(self, server, data): + def System_OnQuit(self): self.Server = None - server['api'].close_transcode(self.device_id) - self.Utils.window('emby_should_stop.bool', True) - self.Service.running = False - def Other_ServerRestarting(self, server, data): + for server_id in self.EmbyServers: + if self.player.Transcoding: + self.EmbyServers[server_id].API.close_transcode() + + self.Service.ShouldStop = True + + def ServerRestarting(self, data): self.Server = None - if data.get('ServerId'): + if self.Service.ServerReconnectingInProgress(data['ServerId']): return - if self.Utils.settings('restartMsg.bool'): - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33006), icon="{emby}") + if self.Service.Utils.Settings.restartMsg: + self.Service.Utils.dialog("notification", heading="{emby}", message=self.Service.Utils.Translate(33006), icon="{emby}") - self.Service.Server(20) + self.EmbyServers[data['ServerId']].Online = False + self.Service.ServerReconnect(data['ServerId']) def WebserviceUpdateInfo(self, server): if self.ServerIP: - try: - if server: #Restart Webservice - self.Server = server - - if self.WebserviceOnPlayThread: - self.WebserviceOnPlayThread.Stop() - self.WebserviceOnPlayThread.join() - self.WebserviceOnPlayThread = None - - self.WebserviceOnPlayThread = player.WebserviceOnPlay(self, server) - self.WebserviceOnPlayThread.start() - - if self.WebServiceThread: - self.WebServiceThread.stop() - self.WebServiceThread.join() - self.WebServiceThread = None - - self.WebServiceThread = hooks.webservice.WebService(self.WebserviceEventOut, self.WebserviceEventIn, self.ServerIP, self.ServerToken) - self.WebServiceThread.start() - except: - self.LOG.info("[ WebserviceUpdateInfo -> No Connection ]") + if server: #Restart Webservice + self.Server = server + + if self.WebserviceOnPlayThread: + self.WebserviceOnPlayThread.Stop() + self.WebserviceOnPlayThread = None + + self.WebserviceOnPlayThread = player.WebserviceOnPlay(self.player, server, self.WebserviceEventIn, self.WebserviceEventOut) + self.WebserviceOnPlayThread.start() + + if self.WebServiceThread: + self.WebServiceThread.stop() + self.WebServiceThread.join() + self.WebServiceThread = None + + self.WebServiceThread = hooks.webservice.WebService(self.WebserviceEventOut, self.WebserviceEventIn, self.ServerIP, self.ServerToken, self.Service.Utils.Settings.enableCoverArt, self.Service.Utils.Settings.compressArt) + self.WebServiceThread.start() else: self.LOG.info("[ WebserviceUpdateInfo -> No Info ]") def QuitThreads(self): if self.WebserviceOnPlayThread: self.WebserviceOnPlayThread.Stop() - self.WebserviceOnPlayThread.join() self.WebserviceOnPlayThread = None - if self.ProgressThread: - self.ProgressThread.Stop() - self.ProgressThread.join() - self.ProgressThread = None + if self.player.ProgressThread: + self.player.ProgressThread.Stop() + self.player.ProgressThread = None if self.WebServiceThread: self.WebServiceThread.stop() - self.WebServiceThread.join() self.WebServiceThread = None - def AddServer(self, server, data): - self.connect.setup_manual_server() - xbmc.executebuiltin("Container.Refresh") - - def UpdateServer(self, server, data): - self.Utils.dialog("ok", heading="{emby}", line1=helper.translate._(33151)) - self.connect.setup_manual_server() - - def SetServerSSL(self, server, data): - self.connect.set_ssl(data['Id']) + def AddServer(self, data): +# self.connect.setup_manual_server() +# xbmc.executebuiltin("Container.Refresh") + self.EmbyServer_Connect() - def settingschanged(self, server, data): - imp.reload(helper.utils) - self.Utils = helper.utils.Utils() + def SetServerSSL(self, data): + self.connect.set_ssl(data['ServerId']) - if self.Utils.window('emby_should_stop.bool'): + def settingschanged(self): + if self.Service.ShouldStop or self.sleep: return - self.DeviceID = self.Utils.get_device_id() - self.WebserviceUpdateInfo(self.Server) + self.Service.Utils.Settings.InitSettings() - def Application_OnVolumeChanged(self, server, data): + def Application_OnVolumeChanged(self, data): self.player.SETVolume(data['volume'], data['muted']) - def System_OnWake(self, server=None, data=None): + def System_OnWake(self): if not self.sleep: self.LOG.warning("System.OnSleep was never called, skip System.OnWake") return - xbmc.sleep(5000) self.LOG.info("--<[ sleep ]") - self.Utils.window('emby_sleep.bool', False) self.sleep = False - self.Utils.window('emby_should_stop', clear=True) - self.Service.Server() + self.EmbyServer_ReconnectAll() + self.player.SyncPause = False - def System_OnSleep(self, server, data): + def System_OnSleep(self): self.LOG.info("-->[ sleep ]") - self.Utils.window('emby_sleep.bool', True) - self.Utils.window('emby_should_stop.bool', True) - self.Service.Server(close=True) - emby.main.Emby().close_all() - self.server = [] + self.player.SyncPause = True + self.QuitThreads() + self.EmbyServer_DisconnectAll() + self.LibraryStopAll() self.sleep = True - def ServerOnline(self, server, data): + def ServerOnline(self, data): self.LOG.info("[ Server Online ]") - if data.get('ServerId') is None: - self.Utils.window('emby_online.bool', True) - self.ServerIP = server['auth/server-address'] - self.ServerToken = server['auth/token'] - self.DeviceID = self.Utils.get_device_id() - self.WebserviceUpdateInfo(server) - - if self.Utils.window('emby_reloadskin.bool'): - xbmc.executebuiltin('ReloadSkin()') - - xbmc.sleep(1000) #Give Kodi a second for skin reload - self.auth_check = True - self.warn = True + if self.EmbyServers[data['ServerId']].Online: + return - if self.Utils.settings('connectMsg.bool'): - users = emby.main.Emby()['api'].get_device(self.Utils.window('emby_deviceId'))[0]['AdditionalUsers'] - users = [user['UserName'] for user in users] - users.insert(0, self.Utils.settings('username')) - self.Utils.dialog("notification", heading="{emby}", message="%s %s" % (helper.translate._(33000), ", ".join(users)), icon="{emby}", time=1500, sound=False) + self.EmbyServers[data['ServerId']].Online = True + self.ServerIP = self.EmbyServers[data['ServerId']].auth.get_serveraddress() + self.ServerToken = self.EmbyServers[data['ServerId']].Data['auth.token'] + self.WebserviceUpdateInfo(self.EmbyServers[data['ServerId']]) - def AddSkipItem(self, ID): - self.ItemSkipUpdate.append(ID) - self.ItemSkipUpdateAfterStop.append(ID) + if self.Service.Utils.Settings.ReloadSkin: + xbmc.executebuiltin('ReloadSkin()') + self.Service.Utils.Settings.set_settings_bool('ReloadSkin', False) - def SetSkipItemAfterStop(self): - self.ItemSkipUpdate = self.ItemSkipUpdateAfterStop - self.ItemSkipUpdateReset = True + if self.Service.Utils.Settings.connectMsg: + users = self.EmbyServers[data['ServerId']].API.get_device()[0] # ['AdditionalUsers'] + users = users['UserName'] +# users = [user['UserName'] for user in users['AdditionalUsers']] +# users.insert(0, self.Service.Utils.Settings.username) +# self.Service.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.Service.Utils.Translate(33000), ", ".join(users)), icon="{emby}", time=1500, sound=False) + self.Service.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.Service.Utils.Translate(33000), self.Service.Utils.StringMod(users)), icon="{emby}", time=1500, sound=False) #Mark as watched/unwatched updates - def VideoLibrary_OnUpdate(self, server, data): + def VideoLibrary_OnUpdate(self, data): + for server_id in self.EmbyServers: ######################## WORKAROUND!!!!!!!!!!! ADD Serverid info in emby.db and query from there + break + if 'item' in data: if 'playcount' in data: kodi_id = data['item']['id'] media = data['item']['type'] - item = database.database.get_item_complete(kodi_id, media) + item = database.database.get_item_complete(self.Service.Utils, kodi_id, media) #read here serverid if item: - if media in ("tvshow"): #Search for all items in TVShow and update them - PresentationKey = item[10].split("-") - items = database.database.get_ItemsByPresentationkey(PresentationKey[0]) + if not item[0] in self.player.ItemSkipUpdate: #Check EmbyID + if media == "tvshow": #Search for all items in TVShow and update them + PresentationKey = item[10].split("-") + items = database.database.get_ItemsByPresentationkey(self.Service.Utils, PresentationKey[0]) + + for item2 in items: + self.player.ItemSkipUpdate.append(item2[0]) + self.EmbyServers[server_id].API.item_played(item2[0], bool(data['playcount'])) + + return + + self.EmbyServers[server_id].API.item_played(item[0], bool(data['playcount'])) + + else: + self.player.ItemSkipUpdate.append(item[0]) + + self.player.ItemSkipUpdateReset = True + + #Emby backup + def Backup(self, data): + if not self.Service.Utils.Settings.backupPath: + self.Service.Utils.dialog("notification", heading="{emby}", icon="{emby}", message="No backup path set", sound=False) + return + + path = self.Service.Utils.Settings.backupPath + folder_name = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], xbmc.getInfoLabel('System.Date(dd-mm-yy)')) + folder_name = self.Service.Utils.dialog("input", heading=self.Service.Utils.Translate(33089), defaultt=folder_name) + + if not folder_name: + return + + backup = os.path.join(path, folder_name) + + if xbmcvfs.exists(backup + '/'): + if not self.Service.Utils.dialog("yesno", heading="{emby}", line1=self.Service.Utils.Translate(33090)): + return backup() + + self.Service.Utils.delete_folder(backup) + + addon_data = self.Service.Utils.translatePath("special://profile/addon_data/plugin.video.emby-next-gen") + destination_data = os.path.join(backup, "addon_data", "plugin.video.emby-next-gen") + destination_databases = os.path.join(backup, "Database") + + if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases): + self.LOG.info("Unable to create all directories") + self.Service.Utils.dialog("notification", heading="{emby}", icon="{emby}", message=self.Service.Utils.Translate(33165), sound=False) + return + + self.Service.Utils.copytree(addon_data, destination_data) + db = self.Service.Utils.translatePath("special://database/") + _, files = xbmcvfs.listdir(db) + + for Temp in files: + if 'MyVideos' in Temp: + xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) + self.LOG.info("copied %s" % Temp) + elif 'emby' in Temp: + xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) + self.LOG.info("copied %s" % Temp) + elif 'MyMusic' in Temp: + xbmcvfs.copy(os.path.join(db, Temp), os.path.join(destination_databases, Temp)) + self.LOG.info("copied %s" % Temp) + + self.LOG.info("backup completed") + self.Service.Utils.dialog("ok", heading="{emby}", line1="%s %s" % (self.Service.Utils.Translate(33091), backup)) + + #Add or remove users from the default server session + #permanent=True from the add-on settings + def AddUser(self, data): + server_id = data['ServerId'] + permanent = True + session = self.EmbyServers[server_id].API.get_device() + hidden = None if self.Service.Utils.Settings.addUsersHidden else False + users = self.EmbyServers[server_id].API.get_users(False, hidden) + + if not users: + return + + for user in users: + if user['Id'] == session[0]['UserId']: + users.remove(user) + break + + while True: + session = self.EmbyServers[server_id].API.get_device() + current = session[0]['AdditionalUsers'] + + if permanent: + perm_users = self.Service.Utils.Settings.Users.split(',') if self.Service.Utils.Settings.Users else [] + current = [] + + for user in users: + for perm_user in perm_users: + + if user['Id'] == perm_user: + current.append({'UserName': user['Name'], 'UserId': user['Id']}) + + result = self.Service.Utils.dialog("select", self.Service.Utils.Translate(33061), [self.Service.Utils.Translate(33062), self.Service.Utils.Translate(33063)] if current else [self.Service.Utils.Translate(33062)]) + + if result < 0: + break + + if not result: # Add user + eligible = [x for x in users if x['Id'] not in [current_user['UserId'] for current_user in current]] + resp = self.Service.Utils.dialog("select", self.Service.Utils.Translate(33064), [x['Name'] for x in eligible]) + + if resp < 0: + break + + user = eligible[resp] + + if permanent: + perm_users.append(user['Id']) + self.Service.Utils.Settings.set_settings('Users', ','.join(perm_users)) - for item2 in items: - self.ItemSkipUpdate.append(item2[0]) - server['api'].item_played(item2[0], bool(data['playcount'])) + self.Service.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.Service.Utils.Translate(33067), user['Name']), icon="{emby}", time=1000, sound=False) + else: # Remove user + resp = self.Service.Utils.dialog("select", self.Service.Utils.Translate(33064), [x['UserName'] for x in current]) - return + if resp < 0: + break - self.ItemSkipUpdate.append(item[0]) - server['api'].item_played(item[0], bool(data['playcount'])) + user = current[resp] - self.ItemSkipUpdateReset = True + if permanent: + perm_users.remove(user['UserId']) + self.Service.Utils.Settings.set_settings('Users', ','.join(perm_users)) + + self.Service.Utils.dialog("notification", heading="{emby}", message="%s %s" % (self.Service.Utils.Translate(33066), user['UserName']), icon="{emby}", time=1000, sound=False) #Thread the monitor so that we can do whatever we need without blocking onNotification. class MonitorWorker(threading.Thread): - def __init__(self, monitor=None, sender=None, method=None, data=None, server=None, device_id=None): + def __init__(self, monitor, sender, method, data): self.sender = sender self.method = method self.data = data - self.server = server self.monitor = monitor - self.device_id = device_id - self.Utils = helper.utils.Utils() - self.LOG = logging.getLogger("EMBY.hooks.monitor.MonitorWorker") threading.Thread.__init__(self) def run(self): - if self.method in ('System.OnWake', 'System.OnSleep', 'Unauthorized', 'System.OnQuit', 'Other.ServerRestarting', 'settingschanged'): - self.data = {} - elif self.sender == 'plugin.video.emby-next-gen': + if self.method == 'System.OnWake': + self.monitor.System_OnWake() + return + + if self.method == 'Other.DeleteItem': + self.monitor.Context.delete_item(True) + return + + if self.method == 'Other.browse': + self.data = json.loads(self.data)[0] + self.monitor.Service.Utils.set_queryIO('emby_event_ack_%s' % self.data['QueryId']) + self.monitor.Menu.browse(self.data['Handle'], self.data['type'], self.data['id'], self.data['folder'], self.data['name'], self.data['extra'], self.data['ServerId']) + self.monitor.Service.Utils.set_queryIO('emby_event_%s' % self.data['QueryId']) + return + + if self.method == 'Other.manage_libraries': + self.data = json.loads(self.data)[0] + self.monitor.Service.Utils.set_queryIO('emby_event_ack_%s' % self.data['QueryId']) + self.monitor.Menu.manage_libraries(self.data['Handle'], self.data['ServerId']) + self.monitor.Service.Utils.set_queryIO('emby_event_%s' % self.data['QueryId']) + return + + if self.method == 'Other.nextepisodes': + self.data = json.loads(self.data)[0] + self.monitor.Service.Utils.set_queryIO('emby_event_ack_%s' % self.data['QueryId']) + self.monitor.Menu.get_next_episodes(self.data['Handle'], self.data['libraryname']) + self.monitor.Service.Utils.set_queryIO('emby_event_%s' % self.data['QueryId']) + return + + if self.method == 'Other.listing': + self.data = json.loads(self.data)[0] + self.monitor.Service.Utils.set_queryIO('emby_event_ack_%s' % self.data['QueryId']) + self.monitor.Menu.listing(self.data['Handle']) + self.monitor.Service.Utils.set_queryIO('emby_event_%s' % self.data['QueryId']) + return + + if self.method == 'Other.restartservice': + self.monitor.Service.Utils.Settings.emby_restart = True + return + + if self.method == 'Other.reset_device_id': + self.monitor.Service.Utils.reset_device_id() + return + + if self.method == 'Other.context': + self.monitor.Context.select_menu() + return + + if self.method == 'System.OnSleep': + self.monitor.System_OnSleep() + return + + if self.method == 'System.OnQuit': + self.monitor.System_OnQuit() + return + + if self.method == 'settingschanged': + self.monitor.settingschanged() + return + + if self.method == 'Other.DatabaseReset': + self.monitor.DatabaseReset() + return + + if self.method == 'Other.TextureCache': + self.monitor.TextureCache() + return + + if self.sender == 'plugin.video.emby-next-gen': self.method = self.method.split('.')[1] - if self.method not in ('ReportProgressRequested', 'LoadServer', 'Play', 'Playstate', 'GeneralCommand', 'StopServer', 'RegisterServer', 'ServerOnline', 'ServerConnect', 'EmbyConnect', 'AddServer', 'RemoveServer', 'UpdateServer', 'SetServerSSL', 'UserDataChanged', 'LibraryChanged', 'WebSocketRestarting', 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', 'GUI.OnScreensaverDeactivated', 'PatchMusic', 'UserConfigurationUpdated', 'UserPolicyUpdated', 'ServerUnreachable', 'ServerShuttingDown'): + if self.method not in ('ReportProgressRequested', 'Play', 'Playstate', 'GeneralCommand', 'StopServer', 'RegisterServer', 'ServerOnline', 'ServerConnect', 'AddUser', 'AddServer', 'RemoveServer', 'SetServerSSL', 'UserDataChanged', 'LibraryChanged', 'WebSocketRestarting', 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection', 'GUI.OnScreensaverDeactivated', 'PatchMusic', 'UserConfigurationUpdated', 'UserPolicyUpdated', 'ServerUnreachable', 'ServerShuttingDown', 'ServerRestarting', 'Unauthorized', 'SyncThemes', 'Backup'): return self.data = json.loads(self.data)[0] @@ -847,4 +858,4 @@ def run(self): self.data['MonitorMethod'] = self.method func = getattr(self.monitor, self.method.replace('.', '_')) - func(self.server, self.data) + func(self.data) diff --git a/hooks/player.py b/hooks/player.py index 23024ed69..f375ebbf2 100644 --- a/hooks/player.py +++ b/hooks/player.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- import json import threading -import logging import uuid import xbmc -import xbmcgui -import helper.utils -import helper.translate + import database.database import database.emby_db +import core.queries_videos +import helper.jsonrpc +import helper.loghandler class ProgressUpdates(threading.Thread): def __init__(self, Player): @@ -21,79 +21,99 @@ def Stop(self): self.Exit = True def run(self): - while not self.Exit: - self.Player.report_playback() - - if xbmc.Monitor().waitForAbort(4): - break -#Basic Player class to track progress of Emby content. -class PlayerEvents(xbmc.Player): - def __init__(self, monitor): - self.Monitor = monitor - self.Utils = self.Monitor.Utils - self.Monitor.CurrentlyPlaying = {} - self.LOG = logging.getLogger("EMBY.hooks.player.Player") - xbmc.Player.__init__(self) - - #Call when playback start to setup play entry in player tracker. - def set_item(self, PlayItem): - self.stop_playback(True) - PlayItem['Volume'], PlayItem['Muted'] = self.get_volume() - self.Monitor.CurrentlyPlaying = PlayItem - self.LOG.info("-->[ play/%s ] %s", PlayItem['Id'], PlayItem) - data = { - 'ItemId': PlayItem['Id'], - 'MediaSourceId': PlayItem['MediaSourceId'], - 'PlaySessionId': PlayItem['PlaySessionId'] - } - - #Init session - PlayItem['Server']['api'].session_playing(data) - self.report_playback() - - def SETVolume(self, Volume, Mute): - if not self.Monitor.CurrentlyPlaying: - return + while True: + if xbmc.Monitor().waitForAbort(5): + return - self.Monitor.CurrentlyPlaying['Volume'] = Volume - self.Monitor.CurrentlyPlaying['Muted'] = Mute - self.report_playback() - - #Report playback progress to emby server. - def report_playback(self): - if self.Monitor.Trailer: - return + if not self.Exit: + self.Player.report_playback(True) + else: + return - if not self.Monitor.CurrentlyPlaying: +class PlayerEvents(xbmc.Player): + def __init__(self): + self.CurrentlyPlaying = {} + self.LOG = helper.loghandler.LOG('EMBY.hooks.player.Player') + self.Trailer = False + self.PlayerReloadIndex = "-1" + self.PlayerLastItem = "" + self.PlayerLastItemID = "-1" + self.ItemSkipUpdate = [] + self.ItemSkipUpdateAfterStop = [] + self.ItemSkipUpdateReset = False + self.SyncPause = False + self.ProgressThread = None + self.PlaySessionId = "" + self.MediasourceID = "" + self.Transcoding = False + self.CurrentItem = {} + self.SkipUpdate = False + self.PlaySessionIdLast = "" + self.DynamicItem = {} + + #Threaded by Monitor + def OnStop(self, EmbyServer): + if self.ProgressThread: + self.ProgressThread.Stop() + self.ProgressThread = None + + if self.Transcoding: + EmbyServer.API.close_transcode() + + self.SyncPause = False + + #Threaded by Monitor + def OnPlay(self, data, EmbyServer, library): + self.LOG.info("[ OnPlay ] %s " % data) + + if data['item']['type'] in ('picture', 'unknown'): return - try: - current_time = int(self.getTime()) - TotalTime = int(self.getTotalTime()) - except: - return #not playing any file + if self.ProgressThread: + self.ProgressThread.Stop() + self.ProgressThread = None - self.Monitor.CurrentlyPlaying['CurrentPosition'] = current_time * 10000000 + self.SyncPause = True - if self.Monitor.CurrentlyPlaying['RunTime'] == -1: - self.Monitor.CurrentlyPlaying['RunTime'] = TotalTime * 10000000 + if not self.Trailer: + if not "id" in data['item']: + DynamicID = EmbyServer.Utils.ReplaceSpecialCharecters(data['item']['title']) - data = { - 'ItemId': self.Monitor.CurrentlyPlaying['Id'], - 'MediaSourceId': self.Monitor.CurrentlyPlaying['MediaSourceId'], - 'PositionTicks': self.Monitor.CurrentlyPlaying['CurrentPosition'], - 'RunTimeTicks': self.Monitor.CurrentlyPlaying['RunTime'], - 'CanSeek': True, - 'QueueableMediaTypes': "Video,Audio", - 'VolumeLevel': self.Monitor.CurrentlyPlaying['Volume'], - 'IsPaused': self.Monitor.CurrentlyPlaying['Paused'], - 'IsMuted': self.Monitor.CurrentlyPlaying['Muted'], - 'PlaySessionId': self.Monitor.CurrentlyPlaying['PlaySessionId'] - } - self.Monitor.CurrentlyPlaying['Server']['api'].session_progress(data) + if DynamicID in self.DynamicItem: + self.CurrentItem['Id'] = self.DynamicItem[DynamicID] + else: + self.CurrentItem['Tracking'] = False + return + else: + kodi_id = data['item']['id'] + media_type = data['item']['type'] + item = database.database.get_item_complete(EmbyServer.Utils, kodi_id, media_type) - def onAVStarted(self): - self.LOG.info("[ onAVStarted ]") + if item: + self.CurrentItem['Id'] = item[0] + else: + self.CurrentItem['Tracking'] = False + return #Kodi internal Source + + if EmbyServer.Utils.direct_path: #native mode + PresentationKey = item[10].split("-") + self.ItemSkipUpdate.append(PresentationKey[0]) + self.ItemSkipUpdate.append(self.CurrentItem['Id']) + self.ItemSkipUpdateAfterStop.append(PresentationKey[0]) + self.ItemSkipUpdateAfterStop.append(self.CurrentItem['Id']) + self.PlaySessionId = str(uuid.uuid4()).replace("-", "") + + self.CurrentItem['Tracking'] = True + self.CurrentItem['Type'] = data['item']['type'] + self.CurrentItem['Volume'], self.CurrentItem['Muted'] = self.get_volume() + self.CurrentItem['MediaSourceId'] = self.MediasourceID + self.CurrentItem['EmbyServer'] = EmbyServer + self.CurrentItem['RunTime'] = 0 + self.CurrentItem['CurrentPosition'] = 0 + self.CurrentItem['Paused'] = False + self.CurrentItem['library'] = library + self.CurrentItem['MediaSourceId'] = self.MediasourceID + self.CurrentItem['Volume'], self.CurrentItem['Muted'] = self.get_volume() def onAVChange(self): self.LOG.info("[ onAVChange ]") @@ -110,21 +130,21 @@ def onPlayBackStarted(self): def onPlayBackPaused(self): self.LOG.info("[ onPlayBackPaused ]") - if not self.Monitor.CurrentlyPlaying: + if not self.CurrentlyPlaying: return - self.Monitor.CurrentlyPlaying['Paused'] = True + self.CurrentlyPlaying['Paused'] = True self.report_playback() self.LOG.debug("-->[ paused ]") def onPlayBackResumed(self): self.LOG.info("[ onPlayBackResumed ]") - if not self.Monitor.CurrentlyPlaying: + if not self.CurrentlyPlaying: return - self.Monitor.CurrentlyPlaying['Paused'] = False - self.report_playback() + self.CurrentlyPlaying['Paused'] = False + self.report_playback(False) self.LOG.debug("--<[ paused ]") def onPlayBackStopped(self): @@ -133,34 +153,141 @@ def onPlayBackStopped(self): if self.ReloadStream():#Media reload (3D Movie) return - self.Monitor.PlayerLastItemID = "-1" - self.Monitor.PlayerLastItem = "" - self.Monitor.Trailer = False - self.Utils.window('emby.sync.pause.bool', True) - self.stop_playback() + self.PlayerLastItemID = "-1" + self.PlayerLastItem = "" + self.Trailer = False + self.SyncPause = True + self.stop_playback(False) self.LOG.info("--<[ playback ]") def onPlayBackSeek(self, time, seekOffset): self.LOG.info("[ onPlayBackSeek ]") - self.report_playback() + + if not self.CurrentlyPlaying: + return + + SeekPosition = int(time * 10000) + + if self.CurrentlyPlaying['RunTime']: + if SeekPosition > self.CurrentlyPlaying['RunTime']: + SeekPosition = self.CurrentlyPlaying['RunTime'] + + self.CurrentlyPlaying['CurrentPosition'] = SeekPosition + self.report_playback(False) + self.SkipUpdate = True #Pause progress updates for one cycle -> new seek position def onPlayBackEnded(self): self.LOG.info("[ onPlayBackEnded ]") - if self.Monitor.Trailer: + if self.Trailer or self.ReloadStream(): return - if self.ReloadStream():#Media reload (3D Movie) + self.PlayerLastItemID = "-1" + self.PlayerLastItem = "" + self.SyncPause = True + self.stop_playback(False) + self.LOG.info("--<<[ playback ]") + + #Threaded to ThreadAVStarted + def onAVStarted(self): + self.LOG.info("[ onAVStarted ]") + new_thread = PlayerWorker(self, "ThreadAVStarted") + new_thread.start() + + def ThreadAVStarted(self): + self.LOG.info("[ ThreadAVStarted ]") + self.stop_playback(True) + + while not self.CurrentItem: #wait for OnPlay + if xbmc.Monitor().waitForAbort(1): + return + + if not self.CurrentItem['Tracking']: + self.CurrentItem = {} return - self.Monitor.PlayerLastItemID = "-1" - self.Monitor.PlayerLastItem = "" - self.Utils.window('emby.sync.pause.bool', True) - self.stop_playback() - self.LOG.info("--<<[ playback ]") + if not self.set_CurrentPosition(): #Stopped directly after started playing + self.LOG.info("[ fast stop detected ]") + return + + self.CurrentItem['PlaySessionId'] = self.PlaySessionId + self.CurrentlyPlaying = self.CurrentItem + self.CurrentItem = {} + self.LOG.info("-->[ play/%s ] %s" % (self.CurrentlyPlaying['Id'], self.CurrentlyPlaying)) + data = { + 'ItemId': self.CurrentlyPlaying['Id'], + 'MediaSourceId': self.CurrentlyPlaying['MediaSourceId'], + 'PlaySessionId': self.CurrentlyPlaying['PlaySessionId'] + } + + #Init session + self.CurrentlyPlaying['EmbyServer'].API.session_playing(data) + self.SkipUpdate = False + self.report_playback(False) + + if not self.ProgressThread: + self.ProgressThread = ProgressUpdates(self) + self.ProgressThread.start() + + def SETVolume(self, Volume, Mute): + if not self.CurrentlyPlaying: + return + + self.CurrentlyPlaying['Volume'] = Volume + self.CurrentlyPlaying['Muted'] = Mute + self.report_playback(False) + + def set_CurrentPosition(self): + try: + CurrentPosition = int(self.getTime() * 10000000) + + if CurrentPosition < 0: + CurrentPosition = 0 + + self.CurrentlyPlaying['CurrentPosition'] = CurrentPosition + return True + except: + return False + + def set_Runtime(self): + try: + self.CurrentlyPlaying['RunTime'] = int(self.getTotalTime() * 10000000) + return bool(self.CurrentlyPlaying['RunTime']) + except: + return False + + #Report playback progress to emby server. + def report_playback(self, UpdatePosition=True): + if not self.CurrentlyPlaying or self.Trailer or self.SkipUpdate: + self.SkipUpdate = False + return + + if not self.CurrentlyPlaying['RunTime']: + if not self.set_Runtime(): + self.LOG.info("[ skip progress update, no runtime info ]") + return + + if UpdatePosition: + if not self.set_CurrentPosition(): + self.LOG.info("[ skip progress update, no position info ]") + return + + data = { + 'ItemId': self.CurrentlyPlaying['Id'], + 'MediaSourceId': self.CurrentlyPlaying['MediaSourceId'], + 'PositionTicks': self.CurrentlyPlaying['CurrentPosition'], + 'RunTimeTicks': self.CurrentlyPlaying['RunTime'], + 'CanSeek': True, + 'QueueableMediaTypes': "Video,Audio", + 'VolumeLevel': self.CurrentlyPlaying['Volume'], + 'IsPaused': self.CurrentlyPlaying['Paused'], + 'IsMuted': self.CurrentlyPlaying['Muted'], + 'PlaySessionId': self.CurrentlyPlaying['PlaySessionId'] + } + self.CurrentlyPlaying['EmbyServer'].API.session_progress(data) def get_volume(self): - result = helper.utils.JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) + result = helper.jsonrpc.JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) result = result.get('result', {}) volume = result.get('volume') muted = result.get('muted') @@ -168,86 +295,100 @@ def get_volume(self): def onPlayBackError(self): self.LOG.warning("Playback error occured") - self.stop_playback() - - def get_playing_file(self): - if self.isPlaying(): - return self.getPlayingFile() - - return None + self.stop_playback(False) def ReloadStream(self): #Media has changed -> reload - if self.Monitor.PlayerReloadIndex != "-1": + if self.PlayerReloadIndex != "-1": playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - xbmc.Player().play(item=playlist, startpos=int(self.Monitor.PlayerReloadIndex)) - self.Monitor.PlayerReloadIndex = "-1" + self.play(item=playlist, startpos=int(self.PlayerReloadIndex)) + self.PlayerReloadIndex = "-1" return True return False - def stop_playback(self, Init=False): - if self.Monitor.CurrentlyPlaying: - self.LOG.debug("[ played info ] %s", self.Monitor.CurrentlyPlaying) - + def stop_playback(self, Init): + if self.CurrentlyPlaying: + self.LOG.debug("[ played info ] %s" % self.CurrentlyPlaying) data = { - 'ItemId': self.Monitor.CurrentlyPlaying['Id'], - 'MediaSourceId': self.Monitor.CurrentlyPlaying['MediaSourceId'], - 'PositionTicks': self.Monitor.CurrentlyPlaying['CurrentPosition'], - 'PlaySessionId': self.Monitor.CurrentlyPlaying['PlaySessionId'] + 'ItemId': self.CurrentlyPlaying['Id'], + 'MediaSourceId': self.CurrentlyPlaying['MediaSourceId'], + 'PositionTicks': self.CurrentlyPlaying['CurrentPosition'], + 'PlaySessionId': self.CurrentlyPlaying['PlaySessionId'] } + self.CurrentlyPlaying['EmbyServer'].API.session_stop(data) - self.Monitor.CurrentlyPlaying['Server']['api'].close_transcode(self.Monitor.CurrentlyPlaying['DeviceId']) - self.Monitor.CurrentlyPlaying['Server']['api'].session_stop(data) - self.Monitor.CurrentlyPlaying = {} + if self.Transcoding: + self.CurrentlyPlaying['EmbyServer'].API.close_transcode() if not Init: - self.Monitor.SetSkipItemAfterStop() - self.Utils.window('emby.sync.pause', clear=True) + self.ItemSkipUpdate = self.ItemSkipUpdateAfterStop + self.ItemSkipUpdateReset = True + self.SyncPause = False + + #Offer delete + if self.CurrentlyPlaying: + if self.CurrentlyPlaying['EmbyServer'].Utils.Settings.offerDelete: + Runtime = int(self.CurrentlyPlaying['RunTime']) + + if Runtime > 10: + if int(self.CurrentlyPlaying['CurrentPosition']) > Runtime * 0.95: #95% Progress + DeleteMsg = False + + if self.CurrentlyPlaying['Type'] == 'episode' and self.CurrentlyPlaying['EmbyServer'].Utils.Settings.deleteTV: + DeleteMsg = True + elif self.CurrentlyPlaying['Type'] == 'movie' and self.CurrentlyPlaying['EmbyServer'].Utils.Settings.deleteMovies: + DeleteMsg = True + + if DeleteMsg: + self.LOG.info("Offer delete option") + + if self.CurrentlyPlaying['EmbyServer'].Utils.dialog("yesno", heading=self.CurrentlyPlaying['EmbyServer'].Utils.Translate(30091), line1=self.CurrentlyPlaying['EmbyServer'].Utils.Translate(33015)): + self.CurrentlyPlaying['EmbyServer'].API.delete_item(self.CurrentlyPlaying['Id']) + self.CurrentlyPlaying['library'].removed([self.CurrentlyPlaying['Id']]) + self.CurrentlyPlaying['library'].delay_verify([self.CurrentlyPlaying['Id']]) + + self.CurrentlyPlaying = {} + +class PlayerWorker(threading.Thread): + def __init__(self, Player, method): + self.method = method + self.Player = Player + threading.Thread.__init__(self) + + def run(self): + if self.method == 'ThreadAVStarted': + self.Player.ThreadAVStarted() + return #Call from WebSocket to manipulate playing URL class WebserviceOnPlay(threading.Thread): - def __init__(self, Monitor, server): - self.Monitor = Monitor - self.Utils = self.Monitor.Utils - self.Player = self.Monitor.player - self.server = server + def __init__(self, Player, EmbyServer, WebserviceEventIn, WebserviceEventOut): + self.LOG = helper.loghandler.LOG('EMBY.hooks.player.WebserviceOnPlay') + self.EmbyServer = EmbyServer + self.WebserviceEventIn = WebserviceEventIn + self.WebserviceEventOut = WebserviceEventOut + self.Player = Player self.Intros = None self.IntrosIndex = 0 self.Exit = False self.Trailers = False - self.device_id = self.Monitor.device_id self.EmbyIDLast = -1 self.EmbyID = -1 - self.URLQuery = "" self.Type = "" self.KodiID = -1 + self.KodiFileID = -1 self.Force = False self.Filename = "" - self.Token = self.server['auth/token'] - self.ServerIP = self.server['auth/server-address'] - self.ServerAPI = self.server['api'] self.MediaSources = [] - self.WebserviceEventOut = self.Monitor.WebserviceEventOut - self.WebserviceEventIn = self.Monitor.WebserviceEventIn self.TranscodeReasons = "" - self.VideoBitrate = self.Utils.get_VideoBitrate() - self.AudioBitrate = self.Utils.get_AudioBitrate() + self.IncommingData = "" self.TargetVideoBitrate = 0 self.TargetAudioBitrate = 0 - Codec = ["h264", "hevc"] - ID = self.Utils.settings('TranscodeFormatVideo') - self.VideoCodec = "&VideoCodec=" + Codec[int(ID)] - Codec = ["aac", "ac3"] - ID = self.Utils.settings('TranscodeFormatAudio') - self.AudioCodec = "&AudioCodec=" + Codec[int(ID)] - self.TranscodeH265 = self.Utils.settings('transcode_h265.bool') - self.TranscodeDivx = self.Utils.settings('transcodeDivx.bool') - self.TranscodeXvid = self.Utils.settings('transcodeXvid.bool') - self.TranscodeMpeg2 = self.Utils.settings('transcodeMpeg2.bool') - self.EnableCinema = self.Utils.settings('enableCinema.bool') - self.AskCinema = self.Utils.settings('askCinema.bool') - self.LOG = logging.getLogger("EMBY.hooks.player.WebserviceOnPlay") + self.emby_dbT = None + self.BitrateFromURL = None + self.MediasourceID = None + self.EmbyServer.Utils.load_defaultvideosettings() threading.Thread.__init__(self) def Stop(self): @@ -256,32 +397,40 @@ def Stop(self): def run(self): while not self.Exit: - IncommingData = self.WebserviceEventOut.get() - - if IncommingData == "quit": + self.EmbyID = None + self.MediasourceID = None + self.Type = None + self.BitrateFromURL = None + self.Filename = None + self.IncommingData = self.WebserviceEventOut.get() + self.LOG.debug("[ query IncommingData ] %s" % self.IncommingData) + + if self.IncommingData == "quit": break - self.LOG.debug("[ query IncommingData ] %s", IncommingData) - self.EmbyID, MediasourceID, self.Type, BitrateFromURL, self.Filename = self.GetParametersFromURLQuery(IncommingData) + self.GetParametersFromURLQuery() - if 'audio' in IncommingData: - self.WebserviceEventIn.put(self.ServerIP + "/emby/audio/" + self.EmbyID + "/stream?static=true&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.device_id + "&api_key=" + self.Token + "&" + self.Filename) + if 'audio' in self.IncommingData: + self.WebserviceEventIn.put(self.EmbyServer.auth.get_serveraddress() + "/emby/audio/" + self.EmbyID + "/stream?static=true&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&api_key=" + self.EmbyServer.Data['auth.token'] + "&" + self.Filename) continue - if 'livetv' in IncommingData: - self.WebserviceEventIn.put(self.ServerIP + "/emby/videos/" + self.EmbyID + "/stream.ts?PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.device_id + "&api_key=" + self.Token + "&" + self.Filename) + if 'livetv' in self.IncommingData: + self.WebserviceEventIn.put(self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/stream.ts?PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&api_key=" + self.EmbyServer.Data['auth.token'] + "&" + self.Filename) continue - if 'main.m3u8' in IncommingData: #Dynamic Transcode query - IncommingData = IncommingData.replace("/movie/", "/") - IncommingData = IncommingData.replace("/musicvideo/", "/") - IncommingData = IncommingData.replace("/tvshow/", "/") - IncommingData = IncommingData.replace("/video/", "/") - IncommingData = IncommingData.replace("/trailer/", "/") - self.WebserviceEventIn.put(self.ServerIP + "/emby/videos/" + self.EmbyIDLast + IncommingData) + if 'main.m3u8' in self.IncommingData: #Dynamic Transcode query + URL = self.IncommingData.replace("/movie/", "/") + URL = URL.replace("/musicvideo/", "/") + URL = URL.replace("/tvshow/", "/") + URL = URL.replace("/video/", "/") + URL = URL.replace("/trailer/", "/") + self.WebserviceEventIn.put(self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyIDLast + URL) continue - self.URLQuery = "http://127.0.0.1:57578" + IncommingData + if self.Player.Transcoding: + self.EmbyServer.API.close_transcode() + + self.Player.Transcoding = False if self.Type == "movies": self.Type = "movie" @@ -290,36 +439,35 @@ def run(self): elif self.Type == "musicvideos": self.Type = "musicvideo" - self.Utils.window('emby.sync.pause.bool', True) + self.Player.SyncPause = True #Reload Playlistitem after playlist injection - if self.Monitor.PlayerReloadIndex != "-1": + if self.Player.PlayerReloadIndex != "-1": URL = "RELOAD" self.WebserviceEventIn.put(URL) continue - #Todo: SKIP TRAILERS IF MULTIPART! #Trailers - if self.EnableCinema and self.Monitor.PlayerLastItemID != self.EmbyID: + if self.EmbyServer.Utils.Settings.enableCinema and self.Player.PlayerLastItemID != self.EmbyID: PlayTrailer = True - if self.AskCinema: - if not self.Monitor.Trailer: + if self.EmbyServer.Utils.Settings.askCinema: + if not self.Player.Trailer: self.Trailers = False - if not self.Trailers and not self.Monitor.Trailer: + if not self.Trailers and not self.Player.Trailer: self.Trailers = True - PlayTrailer = self.Utils.dialog("yesno", heading="{emby}", line1=helper.translate._(33016)) + PlayTrailer = self.EmbyServer.Utils.dialog("yesno", heading="{emby}", line1=self.EmbyServer.Utils.Translate(33016)) if PlayTrailer: - if self.Monitor.PlayerLastItem != IncommingData or not self.Monitor.Trailer: + if self.Player.PlayerLastItem != self.IncommingData or not self.Player.Trailer: xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Player.SetRepeat", "params": {"playerid": 1, "repeat": "one" }, "id": 1 }') - self.Monitor.PlayerLastItem = IncommingData + self.Player.PlayerLastItem = self.IncommingData self.IntrosIndex = 0 self.Trailers = False - self.Intros = self.ServerAPI.get_intros(self.EmbyID) - #self.IntrosLocal = self.ServerAPI.get_local_trailers(self.EmbyID) - self.Monitor.Trailer = True + self.Intros = self.EmbyServer.API.get_intros(self.EmbyID) + #self.IntrosLocal = self.EmbyServer.API.get_local_trailers(self.EmbyID) + self.Player.Trailer = True try: #Play next trailer self.WebserviceEventIn.put(self.Intros['Items'][self.IntrosIndex]['Path']) @@ -328,70 +476,72 @@ def run(self): except: #No more trailers xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Player.SetRepeat", "params": {"playerid": 1, "repeat": "off" }, "id": 1 }') self.Force = True - self.Monitor.PlayerLastItem = "" + self.Player.PlayerLastItem = "" self.Intros = None self.IntrosIndex = 0 self.Trailers = False - self.Monitor.Trailer = False + self.Player.Trailer = False else: xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Player.SetRepeat", "params": {"playerid": 1, "repeat": "off" }, "id": 1 }') #Select mediasources, Audiostreams, Subtitles - if self.Monitor.PlayerLastItemID != self.EmbyID or self.Force: + if self.Player.PlayerLastItemID != self.EmbyID or self.Force: self.Force = False - self.Monitor.PlayerLastItemID = str(self.EmbyID) + self.Player.PlayerLastItemID = str(self.EmbyID) - with database.database.Database('emby') as embydb: - emby_dbT = database.emby_db.EmbyDatabase(embydb.cursor) - EmbyDBItem = emby_dbT.get_kodiid(self.EmbyID) + with database.database.Database(self.EmbyServer.Utils, 'emby', False) as embydb: + self.emby_dbT = database.emby_db.EmbyDatabase(embydb.cursor) + EmbyDBItem = self.emby_dbT.get_kodiid(self.EmbyID) - if EmbyDBItem: #Item not synced to Kodi DB - PresentationKey = EmbyDBItem[1].split("-") - self.Monitor.AddSkipItem(PresentationKey[0]) - self.KodiID = str(EmbyDBItem[0]) - else: - self.Monitor.PlayerReloadIndex = "-1" - self.Monitor.PlayerLastItem = "" - self.Intros = None - self.IntrosIndex = 0 - self.Trailers = False - self.Monitor.Trailer = False - self.SubTitlesAdd(MediasourceID, emby_dbT) - Transcoding = self.IsTranscoding(BitrateFromURL, None) + if EmbyDBItem: #Item synced to Kodi DB + if EmbyDBItem[1]: + PresentationKey = EmbyDBItem[1].split("-") + self.Player.ItemSkipUpdate.append(PresentationKey[0]) + self.Player.ItemSkipUpdateAfterStop.append(PresentationKey[0]) - if Transcoding: - URL = self.GETTranscodeURL(MediasourceID, self.Filename) - else: - URL = self.ServerIP + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId(MediasourceID) + "&DeviceId=" + self.device_id + "&api_key=" + self.Token + "&" + self.Filename + self.KodiID = str(EmbyDBItem[0]) + self.KodiFileID = str(EmbyDBItem[2]) + self.MediaSources = self.emby_dbT.get_mediasource(self.EmbyID) - self.WebserviceEventIn.put(URL) - continue + if len(self.MediaSources) == 1: + self.Player.PlayerLastItemID = "-1" + self.WebserviceEventIn.put(self.LoadData(0)) + continue - self.MediaSources = emby_dbT.get_mediasource(self.EmbyID) + #Multiversion + Selection = [] - if len(self.MediaSources) == 1: - self.Monitor.PlayerLastItemID = "-1" - self.WebserviceEventIn.put(self.LoadData(MediasourceID, emby_dbT, 0)) - continue + for Data in self.MediaSources: + Selection.append(Data[8] + " - " + self.SizeToText(float(Data[7]))) - #Multiversion - Selection = [] + MediaIndex = self.EmbyServer.Utils.dialog("select", heading="Select Media Source:", list=Selection) - for Data in self.MediaSources: - Selection.append(Data[8] + " - " + self.SizeToText(float(Data[7]))) + if MediaIndex <= 0: + MediaIndex = 0 + self.Player.PlayerLastItemID = "-1" - MediaIndex = self.Utils.dialog("select", heading="Select Media Source:", list=Selection) + self.MediasourceID = self.MediaSources[MediaIndex][3] + self.WebserviceEventIn.put(self.LoadData(MediaIndex)) + else: + self.Player.PlayerReloadIndex = "-1" + self.Player.PlayerLastItem = "" + self.Intros = None + self.IntrosIndex = 0 + self.Trailers = False + self.Player.Trailer = False + self.SubTitlesAdd() + self.Player.Transcoding = self.IsTranscoding(self.BitrateFromURL, None) - if MediaIndex <= 0: - MediaIndex = 0 - self.Monitor.PlayerLastItemID = "-1" + if self.Player.Transcoding: + URL = self.GETTranscodeURL(self.Filename, False, False) + else: + URL = self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + self.MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&api_key=" + self.EmbyServer.Data['auth.token'] + "&" + self.Filename - MediasourceID = self.MediaSources[MediaIndex][3] - self.WebserviceEventIn.put(self.LoadData(MediasourceID, emby_dbT, MediaIndex)) + self.WebserviceEventIn.put(URL) #Load SRT subtitles - def SubTitlesAdd(self, MediasourceID, emby_dbT): - Subtitles = emby_dbT.get_Subtitles(self.EmbyID, 0) + def SubTitlesAdd(self): + Subtitles = self.emby_dbT.get_Subtitles(self.EmbyID, 0) if len(Subtitles) >= 1: CounterSubTitle = 0 @@ -400,28 +550,49 @@ def SubTitlesAdd(self, MediasourceID, emby_dbT): CounterSubTitle += 1 if Data[3] == "srt": - SubTitleURL = self.ServerIP + "/emby/videos/" + self.EmbyID + "/" + MediasourceID + "/Subtitles/ " + str(Data[18]) + "/stream.srt?api_key=" + self.Token - Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(str(CounterSubTitle) + "." + Data[4] + ".srt") - Path = self.Utils.download_external_subs(SubTitleURL, Filename) - xbmc.Player().setSubtitles(Path) - xbmc.Player().showSubtitles(False) + SubTitleURL = self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/" + self.MediasourceID + "/Subtitles/" + str(Data[18]) + "/stream.srt" + request = {'type': "GET", 'url': SubTitleURL, 'params': {}} + + #Get Subtitle Settings + with database.database.Database(self.EmbyServer.Utils, 'video', False) as videodb: + videodb.cursor.execute(core.queries_videos.get_settings, (self.KodiFileID,)) + FileSettings = videodb.cursor.fetchone() + + if FileSettings: + EnableSubtitle = bool(FileSettings[9]) + else: + if self.EmbyServer.Utils.DefaultVideoSettings: + EnableSubtitle = self.EmbyServer.Utils.DefaultVideoSettings['ShowSubtitles'] + else: + EnableSubtitle = False + + if Data[4]: + SubtileLanguage = Data[4] + else: + SubtileLanguage = "unknown" + + Filename = self.EmbyServer.Utils.PathToFilenameReplaceSpecialCharecters(str(CounterSubTitle) + "." + SubtileLanguage + ".srt") + Path = self.EmbyServer.Utils.download_file_from_Embyserver(request, Filename, self.EmbyServer) + + if Path: + self.Player.setSubtitles(Path) + self.Player.showSubtitles(EnableSubtitle) - def LoadData(self, MediasourceID, emby_dbT, MediaIndex): - VideoStreams = emby_dbT.get_videostreams(self.EmbyID, MediaIndex) - AudioStreams = emby_dbT.get_AudioStreams(self.EmbyID, MediaIndex) + def LoadData(self, MediaIndex): + VideoStreams = self.emby_dbT.get_videostreams(self.EmbyID, MediaIndex) + AudioStreams = self.emby_dbT.get_AudioStreams(self.EmbyID, MediaIndex) if not VideoStreams: - self.LOG.warning("[ VideoStreams not found ] %s", self.EmbyID) - return self.ServerIP + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId(MediasourceID) + "&DeviceId=" + self.device_id + "&api_key=" + self.Token + "&" + self.Filename + self.LOG.warning("[ VideoStreams not found ] %s" % self.EmbyID) + return self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + self.MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&api_key=" + self.EmbyServer.Data['auth.token'] + "&" + self.Filename - Bitrate = VideoStreams[0][9] - Transcoding = self.IsTranscoding(Bitrate, VideoStreams[0][3]) #add codec from videostreams, Bitrate (from file) + self.Player.Transcoding = self.IsTranscoding(VideoStreams[0][9], VideoStreams[0][3]) #add codec from videostreams, Bitrate (from file) - if Transcoding: + if self.Player.Transcoding: SubtitleIndex = -1 AudioIndex = -1 Subtitles = [] - Subtitles = emby_dbT.get_Subtitles(self.EmbyID, MediaIndex) + Subtitles = self.emby_dbT.get_Subtitles(self.EmbyID, MediaIndex) if len(AudioStreams) >= 2: Selection = [] @@ -429,7 +600,7 @@ def LoadData(self, MediasourceID, emby_dbT, MediaIndex): for Data in AudioStreams: Selection.append(Data[7]) - AudioIndex = self.Utils.dialog("select", heading="Select Audio Stream:", list=Selection) + AudioIndex = self.EmbyServer.Utils.dialog("select", heading="Select Audio Stream:", list=Selection) if len(Subtitles) >= 1: Selection = [] @@ -437,10 +608,10 @@ def LoadData(self, MediasourceID, emby_dbT, MediaIndex): for Data in Subtitles: Selection.append(Data[7]) - SubtitleIndex = self.Utils.dialog("select", heading="Select Subtitle:", list=Selection) + SubtitleIndex = self.EmbyServer.Utils.dialog("select", heading="Select Subtitle:", list=Selection) if AudioIndex <= 0 and SubtitleIndex < 0 and MediaIndex <= 0: #No change -> resume - return self.GETTranscodeURL(MediasourceID, self.Filename) + return self.GETTranscodeURL(self.Filename, False, False) if AudioIndex <= 0: AudioIndex = 0 @@ -450,23 +621,27 @@ def LoadData(self, MediasourceID, emby_dbT, MediaIndex): else: Subtitle = Subtitles[SubtitleIndex] - return self.UpdateItem(MediasourceID, Transcoding, self.MediaSources[MediaIndex], VideoStreams[0], AudioStreams[AudioIndex], emby_dbT, Subtitle) + return self.UpdateItem(self.MediaSources[MediaIndex], AudioStreams[AudioIndex], Subtitle) if MediaIndex == 0: - self.SubTitlesAdd(MediasourceID, emby_dbT) - return self.ServerIP + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId(MediasourceID) + "&DeviceId=" + self.device_id + "&api_key=" + self.Token + "&" + self.Filename + self.SubTitlesAdd() + return self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/stream?static=true&MediaSourceId=" + self.MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&api_key=" + self.EmbyServer.Data['auth.token'] + "&" + self.Filename - return self.UpdateItem(MediasourceID, Transcoding, self.MediaSources[MediaIndex], VideoStreams[0], AudioStreams[0], emby_dbT) + return self.UpdateItem(self.MediaSources[MediaIndex], AudioStreams[0], False) - def GETTranscodeURL(self, MediasourceID, Filename, Audio="", Subtitle=""): + def GETTranscodeURL(self, Filename, Audio, Subtitle): TranscodingVideo = "" TranscodingAudio = "" if Subtitle: Subtitle = "&SubtitleStreamIndex=" + Subtitle + else: + Subtitle = "" if Audio: Audio = "&AudioStreamIndex=" + Audio + else: + Audio = "" if self.TargetVideoBitrate: TranscodingVideo = "&VideoBitrate=" + str(self.TargetVideoBitrate) @@ -475,11 +650,11 @@ def GETTranscodeURL(self, MediasourceID, Filename, Audio="", Subtitle=""): TranscodingAudio = "&AudioBitrate=" + str(self.TargetAudioBitrate) if Filename: - Filename = "&" + Filename + Filename = "&stream-" + Filename - return self.ServerIP + "/emby/videos/" + self.EmbyID + "/master.m3u8?api_key=" + self.Token + "&MediaSourceId=" + MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId(MediasourceID) + "&DeviceId=" + self.device_id + self.VideoCodec + self.AudioCodec + TranscodingVideo + TranscodingAudio + Audio + Subtitle + "&TranscodeReasons=" + self.TranscodeReasons + Filename + return self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID + "/master.m3u8?api_key=" + self.EmbyServer.Data['auth.token'] + "&MediaSourceId=" + self.MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&VideoCodec=" + self.EmbyServer.Utils.Settings.VideoCodecID + "&AudioCodec=" + self.EmbyServer.Utils.Settings.AudioCodecID + TranscodingVideo + TranscodingAudio + Audio + Subtitle + "&TranscodeReasons=" + self.TranscodeReasons + Filename - def SizeToText(self, size, precision=2): + def SizeToText(self, size): suffixes = ['B', 'KB', 'MB', 'GB', 'TB'] suffixIndex = 0 @@ -487,138 +662,113 @@ def SizeToText(self, size, precision=2): suffixIndex += 1 size = size / 1024.0 - return "%.*f%s" % (precision, size, suffixes[suffixIndex]) + return "%.*f%s" % (2, size, suffixes[suffixIndex]) - def GETPlaySessionId(self, MediasourceID=""): - self.Monitor.PlaySessionId = str(uuid.uuid4()).replace("-", "") - self.Monitor.MediasourceID = MediasourceID - return self.Monitor.PlaySessionId + def GETPlaySessionId(self): + self.Player.PlaySessionId = str(uuid.uuid4()).replace("-", "") + self.Player.MediasourceID = self.MediasourceID + return self.Player.PlaySessionId def IsTranscoding(self, Bitrate, Codec): - if self.TranscodeH265: + if self.EmbyServer.Utils.Settings.transcodeH265: if Codec in ("h265", "hevc"): self.IsTranscodingByCodec(Bitrate) return True - elif self.TranscodeDivx: + elif self.EmbyServer.Utils.Settings.transcodeDivx: if Codec == "msmpeg4v3": self.IsTranscodingByCodec(Bitrate) return True - elif self.TranscodeXvid: + elif self.EmbyServer.Utils.Settings.transcodeXvid: if Codec == "mpeg4": self.IsTranscodingByCodec(Bitrate) return True - elif self.TranscodeMpeg2: + elif self.EmbyServer.Utils.Settings.transcodeMpeg2: if Codec == "mpeg2video": self.IsTranscodingByCodec(Bitrate) return True - self.TargetVideoBitrate = self.VideoBitrate - self.TargetAudioBitrate = self.AudioBitrate + self.TargetVideoBitrate = self.EmbyServer.Utils.Settings.VideoBitrate + self.TargetAudioBitrate = self.EmbyServer.Utils.Settings.AudioBitrate self.TranscodeReasons = "ContainerBitrateExceedsLimit" return Bitrate >= self.TargetVideoBitrate def IsTranscodingByCodec(self, Bitrate): - if Bitrate >= self.VideoBitrate: + if Bitrate >= self.EmbyServer.Utils.Settings.VideoBitrate: self.TranscodeReasons = "ContainerBitrateExceedsLimit" - self.TargetVideoBitrate = self.VideoBitrate - self.TargetAudioBitrate = self.AudioBitrate + self.TargetVideoBitrate = self.EmbyServer.Utils.Settings.VideoBitrate + self.TargetAudioBitrate = self.EmbyServer.Utils.Settings.AudioBitrate else: self.TranscodeReasons = "VideoCodecNotSupported" self.TargetVideoBitrate = 0 self.TargetAudioBitrate = 0 - def GetParametersFromURLQuery(self, StreamURL): - Type = StreamURL[1:] - Type = Type[:Type.find("/")] - Temp = StreamURL[StreamURL.rfind("/") + 1:] + def GetParametersFromURLQuery(self): + Type = self.IncommingData[1:] + self.Type = Type[:Type.find("/")] + Temp = self.IncommingData[self.IncommingData.rfind("/") + 1:] Data = Temp.split("-") if len(Data[0]) < 10: - Filename = StreamURL[StreamURL.find("stream-") + 7:] + self.Filename = self.IncommingData[self.IncommingData.find("stream-") + 7:] self.EmbyIDLast = Data[0] try: - BitrateFromURL = int(Data[2]) + self.BitrateFromURL = int(Data[2]) except: - BitrateFromURL = 0 + self.BitrateFromURL = 0 - self.Utils.window('emby.sync.pause.bool', True) - self.Monitor.AddSkipItem(Data[0]) - return Data[0], Data[1], Type, BitrateFromURL, Filename + self.Player.SyncPause = True + self.Player.ItemSkipUpdate.append(Data[0]) + self.Player.ItemSkipUpdateAfterStop.append(Data[0]) + self.EmbyID = Data[0] + self.MediasourceID = Data[1] - return None, None, None, None, None + if self.MediasourceID == "DYNAMIC": + Result = self.EmbyServer.API.get_item(self.EmbyID) + self.MediasourceID = Result['MediaSources'][0]['Id'] - def UpdateItem(self, MediasourceID, Transcoding, MediaSource, VideoStream, AudioStream, emby_dbT, Subtitle=None): + def UpdateItem(self, MediaSource, AudioStream, Subtitle): if self.Type == "movie": - result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetMovieDetails", "params":{"movieid":' + self.KodiID + ', "properties":["title", "playcount", "plot", "genre", "year", "rating", "director", "trailer", "tagline", "plotoutline", "originaltitle", "writer", "studio", "mpaa", "country", "imdbnumber", "set", "showlink", "top250", "votes", "sorttitle", "dateadded", "tag", "userrating", "cast", "premiered", "setid", "art", "lastplayed", "uniqueid"]}}') + result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetMovieDetails", "params":{"movieid":' + self.KodiID + ', "properties":["title", "playcount", "plot", "genre", "year", "rating", "resume", "streamdetails", "director", "trailer", "tagline", "plotoutline", "originaltitle", "writer", "studio", "mpaa", "country", "imdbnumber", "set", "showlink", "top250", "votes", "sorttitle", "dateadded", "tag", "userrating", "cast", "premiered", "setid", "art", "lastplayed", "uniqueid"]}}') Data = json.loads(result) Details = Data['result']['moviedetails'] elif self.Type == "episode": - result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetEpisodeDetails", "params":{"episodeid":' + self.KodiID + ', "properties":["title", "firstaired", "originaltitle", "productioncode", "rating", "season", "seasonid", "showtitle", "specialsortepisode", "specialsortseason", "tvshowid", "userrating", "votes", "episode", "plot", "writer", "cast", "art", "lastplayed", "uniqueid"]}}') + result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetEpisodeDetails", "params":{"episodeid":' + self.KodiID + ', "properties":["title", "playcount", "season", "episode", "showtitle", "plot", "rating", "resume", "streamdetails", "firstaired", "writer", "dateadded", "lastplayed", "originaltitle", "seasonid", "specialsortepisode", "specialsortseason", "userrating", "votes", "cast", "art", "uniqueid"]}}') Data = json.loads(result) Details = Data['result']['episodedetails'] - Details['tvshowtitle'] = Details['showtitle'] elif self.Type == "musicvideo": - result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetMusicVideoDetails", "params":{"musicvideoid":' + self.KodiID + ', "properties":["title", "playcount", "plot", "genre", "year", "rating", "director", "studio", "dateadded", "tag", "userrating", "premiered", "album", "artist", "track", "art", "lastplayed"]}}') + result = xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"VideoLibrary.GetMusicVideoDetails", "params":{"musicvideoid":' + self.KodiID + ', "properties":["title", "playcount", "plot", "genre", "year", "rating", "resume", "streamdetails", "director", "studio", "dateadded", "tag", "userrating", "premiered", "album", "artist", "track", "art", "lastplayed"]}}') Data = json.loads(result) Details = Data['result']['musicvideodetails'] - Details['mediatype'] = self.Type - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - Index = playlist.getposition() - Filename = self.Utils.PathToFilenameReplaceSpecialCharecters(MediaSource[4]) + Filename = self.EmbyServer.Utils.PathToFilenameReplaceSpecialCharecters(MediaSource[4]) - if Subtitle: - SubtitleStream = str(int(Subtitle[2]) + 2) - else: - SubtitleStream = "" + if self.Player.Transcoding: + if Subtitle: + SubtitleStream = str(int(Subtitle[2]) + 2) + else: + SubtitleStream = "" - if Transcoding: - URL = self.GETTranscodeURL(MediasourceID, Filename, str(int(AudioStream[2]) + 1), SubtitleStream) + URL = self.GETTranscodeURL(Filename, str(int(AudioStream[2]) + 1), SubtitleStream) else: #stream - URL = self.ServerIP + "/emby/videos/" + self.EmbyID +"/stream?static=true&api_key=" + self.Token + "&MediaSourceId=" + MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.device_id + "&" + Filename - - if "3d" in MediaSource[8].lower(): - item = xbmcgui.ListItem(Details['title'], path=URL) - else: - item = xbmcgui.ListItem() - item.setPath(self.URLQuery) - - item.setArt(Details['art']) - del Details['art'] + URL = self.EmbyServer.auth.get_serveraddress() + "/emby/videos/" + self.EmbyID +"/stream?static=true&api_key=" + self.EmbyServer.Data['auth.token'] + "&MediaSourceId=" + self.MediasourceID + "&PlaySessionId=" + self.GETPlaySessionId() + "&DeviceId=" + self.EmbyServer.Data['app.device_id'] + "&" + Filename - if 'cast' in Details: - item.setCast(Details['cast']) - del Details['cast'] - - if 'uniqueid' in Details: - item.setUniqueIDs(Details['uniqueid']) - del Details['uniqueid'] - - item.setInfo('video', Details) - item.setProperty('IsPlayable', 'true') - - if not VideoStream[10]: #Duration - Duration = 0 - else: - Duration = int(VideoStream[10]) / 10000000 - - item.addStreamInfo('video', {'codec' : VideoStream[3], 'width' : VideoStream[14], 'height' : VideoStream[15], 'aspect' : VideoStream[20], 'duration' : Duration}) - item.addStreamInfo('audio', {'codec' : AudioStream[3], 'language' : AudioStream[4], 'channels' : AudioStream[12]}) - - if Subtitle: - item.addStreamInfo('subtitle', {'language' : Subtitle[4]}) + li = self.EmbyServer.Utils.CreateListitem(self.Type, Details) if "3d" in MediaSource[8].lower(): - playlist.add(URL, item, Index) + li.setPath(URL) + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + Index = playlist.getposition() + playlist.add(URL, li, Index) xbmc.executeJSONRPC('{"jsonrpc":"2.0", "method":"Playlist.Remove", "params":{"playlistid":1, "position":' + str(Index + 1) + '}}') - self.Monitor.PlayerReloadIndex = str(Index) - self.Monitor.PlayerLastItemID = str(self.EmbyID) + self.Player.PlayerReloadIndex = str(Index) + self.Player.PlayerLastItemID = str(self.EmbyID) URL = "RELOAD" else: - xbmc.Player().updateInfoTag(item) - self.SubTitlesAdd(MediasourceID, emby_dbT) - self.Monitor.PlayerReloadIndex = "-1" - self.Monitor.PlayerLastItemID = "-1" + li.setPath("http://127.0.0.1:57578" + self.IncommingData) + self.Player.updateInfoTag(li) + self.SubTitlesAdd() + self.Player.PlayerReloadIndex = "-1" + self.Player.PlayerLastItemID = "-1" return URL diff --git a/hooks/webservice.py b/hooks/webservice.py index ed78d44a0..df82e1c1a 100644 --- a/hooks/webservice.py +++ b/hooks/webservice.py @@ -1,61 +1,66 @@ # -*- coding: utf-8 -*- -try: - import socketserver as SocketServer - from http.server import SimpleHTTPRequestHandler +import threading + +try: #Python 3.X (Kodi 19) + from http.server import HTTPServer, BaseHTTPRequestHandler import http.client as httplib -except ImportError: - import SocketServer - from SimpleHTTPServer import SimpleHTTPRequestHandler +except ImportError: #Python 2.X (Kodi 18) + import BaseHTTPServer + HTTPServer = BaseHTTPServer.HTTPServer + BaseHTTPRequestHandler = BaseHTTPServer.BaseHTTPRequestHandler import httplib -import threading - import xbmcvfs import xbmc #Run a webservice to capture playback and incomming events. class WebService(threading.Thread): - def __init__(self, WebserviceEventOut, WebserviceEventIn, ServerIP, ServerToken): - self.SocketServer = SocketServer.TCPServer.allow_reuse_address = True + def __init__(self, WebserviceEventOut, WebserviceEventIn, ServerIP, ServerToken, EnableCoverArt, CompressArt): self.server = TCPServer(('127.0.0.1', 57578), RequestHandler) self.server.timeout = 9999 self.WebserviceEventOut = WebserviceEventOut self.WebserviceEventIn = WebserviceEventIn self.ServerIP = ServerIP self.ServerToken = ServerToken + self.EnableCoverArt = EnableCoverArt + self.CompressArt = CompressArt self.LOG = "EMBY.hooks.webservice.WebService" threading.Thread.__init__(self) def stop(self): - conn = httplib.HTTPConnection("127.0.0.1:57578", timeout=0.5) + conn = httplib.HTTPConnection("127.0.0.1:57578", timeout=1) conn.request("QUIT", "/") + conn.getresponse() + #resend as precaution try: - conn.getresponse() conn.request("QUIT", "/") conn.getresponse() except: pass + self.server.server_close() def run(self): xbmc.log(self.LOG + "--->[ webservice/57578 ]", xbmc.LOGWARNING) - self.server.serve_forever(self.WebserviceEventOut, self.WebserviceEventIn, self.ServerIP, self.ServerToken) + self.server.serve_forever(self.WebserviceEventOut, self.WebserviceEventIn, self.ServerIP, self.ServerToken, self.EnableCoverArt, self.CompressArt) xbmc.log(self.LOG + "---<[ webservice/57578 ]", xbmc.LOGWARNING) #Http server that reacts to self.stop flag. -class TCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class TCPServer(HTTPServer): timeout = 9999 - def serve_forever(self, WebserviceEventOut, WebserviceEventIn, ServerIP, ServerToken): + def serve_forever(self, WebserviceEventOut, WebserviceEventIn, ServerIP, ServerToken, EnableCoverArt, CompressArt): self.RequestHandlerClass.Stop = False - self.WebserviceEventIn = WebserviceEventIn - self.WebserviceEventOut = WebserviceEventOut - self.ServerIP = ServerIP - self.ServerToken = ServerToken + self.RequestHandlerClass.WebserviceEventIn = WebserviceEventIn + self.RequestHandlerClass.WebserviceEventOut = WebserviceEventOut + self.RequestHandlerClass.ServerIP = ServerIP + self.RequestHandlerClass.ServerToken = ServerToken + self.RequestHandlerClass.EnableCoverArt = EnableCoverArt + self.RequestHandlerClass.CompressArt = CompressArt blankfile = xbmcvfs.File("special://home/addons/plugin.video.emby-next-gen/resources/blank.m4v") - self.blankfileSize = blankfile.size() - self.blankfileData = blankfile.readBytes() + self.RequestHandlerClass.blankfileSize = blankfile.size() + self.RequestHandlerClass.blankfileData = blankfile.readBytes() blankfile.close() try: @@ -65,18 +70,17 @@ def serve_forever(self, WebserviceEventOut, WebserviceEventIn, ServerIP, ServerT return #Http request handler. Do not use LOG here, it will hang requests in Kodi > show information dialog. -class RequestHandler(SimpleHTTPRequestHandler): - timeout = 9999 +class RequestHandler(BaseHTTPRequestHandler): + timeout = 0.5 Stop = False - - def __init__(self, request, client_address, server): - self.ServerIP = server.ServerIP - self.ServerToken = server.ServerToken - self.WebserviceEventIn = server.WebserviceEventIn - self.WebserviceEventOut = server.WebserviceEventOut - self.blankfileSize = server.blankfileSize - self.blankfileData = server.blankfileData - SimpleHTTPRequestHandler.__init__(self, request, client_address, server) + WebserviceEventIn = None + WebserviceEventOut = None + ServerIP = "" + ServerToken = "" + EnableCoverArt = False + CompressArt = False + blankfileSize = 0 + blankfileData = b'' #Mute the webservice requests def log_message(self, format, *args): @@ -90,17 +94,34 @@ def do_QUIT(self): def do_HEAD(self): if 'stream' in self.path: self.send_response(200) + self.send_header('Content-type', 'video/mp4') self.end_headers() else: self.do_GET() def do_GET(self): if 'extrafanart' in self.path or 'extrathumbs' in self.path or 'Extras/' in self.path or self.path.endswith('.nfo'): - raise Exception("UnknownRequest") + self.send_response(404) + self.end_headers() + return if 'Images' in self.path: self.send_response(301) - self.send_header('Location', self.ServerIP + "/emby/Items" + self.path + "&api_key=" + self.ServerToken) + + if self.EnableCoverArt: + ExtendedParameter = "&EnableImageEnhancers=True" + else: + ExtendedParameter = "&EnableImageEnhancers=False" + + if self.CompressArt: + ExtendedParameter += "&Quality=70" + + if "?" in self.path: + Query = self.ServerIP + "/emby/Items" + self.path + "&api_key=" + self.ServerToken + ExtendedParameter + else: + Query = self.ServerIP + "/emby/Items" + self.path + "?api_key=" + self.ServerToken + ExtendedParameter + + self.send_header('Location', Query) self.end_headers() return @@ -120,5 +141,7 @@ def do_GET(self): self.send_header('Location', Response) self.end_headers() return - - raise Exception("UnknownRequest") + + self.send_response(404) + self.end_headers() + return diff --git a/resources/banner.png b/resources/banner.png new file mode 100644 index 000000000..39da53d8f Binary files /dev/null and b/resources/banner.png differ diff --git a/resources/clearlogo.png b/resources/clearlogo.png new file mode 100644 index 000000000..39da53d8f Binary files /dev/null and b/resources/clearlogo.png differ diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 442c29d68..623ca7f47 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -632,7 +632,7 @@ msgstr "" msgctxt "#33102" msgid "Resume the previous sync?" -msgstr "De vorige synchronisatoe hervatten?" +msgstr "De vorige synchronisatie hervatten?" msgctxt "#33103" msgid "" diff --git a/resources/letter.png b/resources/letter.png new file mode 100644 index 000000000..e534b54ac Binary files /dev/null and b/resources/letter.png differ diff --git a/resources/random.png b/resources/random.png new file mode 100644 index 000000000..f8c55c2ff Binary files /dev/null and b/resources/random.png differ diff --git a/resources/screenshot1.jpg b/resources/screenshot1.jpg new file mode 100755 index 000000000..9a7129ebe Binary files /dev/null and b/resources/screenshot1.jpg differ diff --git a/resources/screenshot2.jpg b/resources/screenshot2.jpg new file mode 100755 index 000000000..db8906596 Binary files /dev/null and b/resources/screenshot2.jpg differ diff --git a/resources/screenshot3.jpg b/resources/screenshot3.jpg new file mode 100755 index 000000000..e8bbf636b Binary files /dev/null and b/resources/screenshot3.jpg differ diff --git a/resources/screenshot4.jpg b/resources/screenshot4.jpg new file mode 100755 index 000000000..efd3f1a16 Binary files /dev/null and b/resources/screenshot4.jpg differ diff --git a/resources/settings.xml b/resources/settings.xml index a99a9a807..f02204104 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -8,7 +8,6 @@ - @@ -23,23 +22,19 @@ - - - + + + - - - - - - + + - + @@ -52,17 +47,20 @@ + + + + + - - @@ -70,33 +68,32 @@ + - - + + + + + + + - + - - - - - - - diff --git a/service.py b/service.py index 7e975e6c4..b3e15ece0 100644 --- a/service.py +++ b/service.py @@ -1,176 +1,186 @@ # -*- coding: utf-8 -*- -import logging -import _strptime # Workaround for threads using datetime: _striptime is locked -import sys -import threading -import datetime +import shutil +import os +import xml.etree.ElementTree + import xbmc +import xbmcvfs import hooks.monitor import database.database -import emby.main -import emby.views -import helper.loghandler import helper.setup -import helper.translate import helper.utils +import helper.xmls +import helper.loghandler class Service(): def __init__(self): - helper.loghandler.reset() - helper.loghandler.config() - self.server_thread = [] - self.last_progress = datetime.datetime.today() - self.last_progress_report = datetime.datetime.today() - self.running = True - self.LOG = logging.getLogger("EMBY.entrypoint.Service") + self.LOG = helper.loghandler.LOG('EMBY.entrypoint.Service') + self.ShouldStop = False + self.Startup() + + def Startup(self): + self.ShouldStop = False self.Utils = helper.utils.Utils() - self.Utils.window('emby_should_stop', clear=True) - self.Utils.window('emby_sleep.bool', False) - self.Utils.window('emby_sync_skip_resume.bool', False) - self.profile = self.Utils.translatePath('special://profile/') - self.Utils.window('emby_kodiProfile', value=self.profile) - self.Utils.settings('platformDetected', self.Utils.get_platform()) - self.Utils.settings('distroDetected', self.Utils.get_distro()) - self.Utils.window('emby_reloadskin.bool', True) + self.LOG.warning("--->>>[ %s ]" % self.Utils.addon_name) + self.KodiDefaultNodes() self.Setup = helper.setup.Setup(self.Utils) - memory = xbmc.getInfoLabel('System.Memory(total)').replace('MB', "") - self.Delay = int(self.Utils.settings('startupDelay')) - self.LOG.warning("--->>>[ %s ]", self.Utils.get_addon_name()) - self.LOG.warning("Version: %s", self.Utils.get_version()) - self.LOG.warning("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) - self.LOG.warning("Platform: %s", self.Utils.settings('platformDetected')) - self.LOG.warning("OS: %s/%sMB", self.Utils.settings('distroDetected'), memory) - self.LOG.warning("Python Version: %s", sys.version) - self.Monitor = None - Views = emby.views.Views(self.Utils) - Views.verify_kodi_defaults() - database.database.test_databases() - - try: - Views.get_nodes() - except Exception as error: - self.LOG.error(error) - - self.Utils.window('emby.connected.bool', True) - self.Utils.settings('groupedSets.bool', self.Utils.get_grouped_set()) - self.Utils.window('emby_playerreloadindex', '-1') - - def Start(self): + self.Delay = int(self.Utils.Settings.startupDelay) + self.Monitor = hooks.monitor.Monitor(self) + database.database.EmbyDatabaseBuild(self.Utils) + Xmls = helper.xmls.Xmls(self.Utils) + Xmls.advanced_settings() + Xmls.advanced_settings_add_timeouts() + self.ServerReconnecting = {} + if not self.Setup.Migrate(): #Check Migrate xbmc.executebuiltin('RestartApp') - return False + return - self.Monitor = hooks.monitor.Monitor() - self.Monitor.ServiceHandle(self) - self.Server(self.Delay) - return True + def KodiDefaultNodes(self): + node_path = self.Utils.translatePath("special://profile/library/video") - def WatchDog(self): - while self.running: - if self.Utils.window('emby_online.bool'): - if self.profile != self.Utils.window('emby_kodiProfile'): - self.LOG.info("[ profile switch ] %s", self.profile) - break + if not xbmcvfs.exists(node_path): + try: + shutil.copytree(src=self.Utils.translatePath("special://xbmc/system/library/video"), dst=self.Utils.translatePath("special://profile/library/video")) + except Exception as error: + xbmcvfs.mkdir(node_path) + + for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): + filename = os.path.join(node_path, node, "index.xml") + + if xbmcvfs.exists(filename): + try: + xmlData = xml.etree.ElementTree.parse(filename).getroot() + except Exception as error: + self.LOG.error(error) + continue - if self.Utils.window('emby.restart.bool'): - self.Utils.window('emby.restart.bool', False) - self.Utils.dialog("notification", heading="{emby}", message=helper.translate._(33193), icon="{emby}", time=1000, sound=False) - raise Exception('RestartService') + xmlData.set('order', str(17 + index)) + self.Utils.indent(xmlData, 0) + self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filename) - try: - if xbmc.Monitor().waitForAbort(1): - break + playlist_path = self.Utils.translatePath("special://profile/playlists/video") - if self.Utils.window('emby_sleep.bool'): - self.Monitor.System_OnWake() + if not xbmcvfs.exists(playlist_path): + xbmcvfs.mkdirs(playlist_path) + + node_path = self.Utils.translatePath("special://profile/library/music") + + if not xbmcvfs.exists(node_path): + try: + shutil.copytree(src=self.Utils.translatePath("special://xbmc/system/library/music"), dst=self.Utils.translatePath("special://profile/library/music")) + except Exception as error: + xbmcvfs.mkdir(node_path) + + for index, node in enumerate(['music']): + filename = os.path.join(node_path, node, "index.xml") + + if xbmcvfs.exists(filename): + try: + xmlData = xml.etree.ElementTree.parse(filename).getroot() + except Exception as error: + self.LOG.error(error) continue - if self.Utils.window('emby_should_stop.bool'): - break - except: + xmlData.set('order', str(17 + index)) + self.Utils.indent(xmlData, 0) + self.Utils.write_xml(xml.etree.ElementTree.tostring(xmlData, 'UTF-8'), filename) + + playlist_path = self.Utils.translatePath("special://profile/playlists/music") + + if not xbmcvfs.exists(playlist_path): + xbmcvfs.mkdirs(playlist_path) + + def ServerConnect(self): + if self.Delay: + if self.Monitor.waitForAbort(self.Delay): + self.shutdown() + return False + + while True: + server_id = self.Monitor.EmbyServer_Connect() + + if server_id: + break + + if self.Monitor.waitForAbort(10): + return False + + self.Setup.setup() + self.Monitor.LibraryLoad(server_id) + return True + + def ServerReconnectingInProgress(self, server_id): + if server_id in self.ServerReconnecting: + return self.ServerReconnecting[server_id] + + return False + + def ServerReconnect(self, server_id, Terminate=True): + self.ServerReconnecting[server_id] = True + self.Monitor.player.SyncPause = False + + if Terminate: + if server_id in self.Monitor.EmbyServers: + self.Monitor.EmbyServers[server_id].stop() + self.Monitor.LibraryStop(server_id) + + while True: + if self.Monitor.waitForAbort(10): + return + + server_id = self.Monitor.EmbyServer_Connect() + + if server_id: + self.ServerReconnecting[server_id] = False break - try: - self.shutdown() - except: - raise Exception("ExitService") + self.Monitor.LibraryLoad(server_id) + self.ServerReconnecting[server_id] = False - def Server(self, delay=None, close=False): - if not self.server_thread: - thread = StartDefaultServer(self, delay, close) - self.server_thread.append(thread) + def WatchDog(self): + while True: + if self.Monitor.waitForAbort(1): + self.shutdown() + return False - def shutdown(self): - self.LOG.warning("---<[ EXITING ]") - self.Utils.window('emby_should_stop.bool', True) - properties = [ - "emby_online", "emby.connected", "emby_deviceId", - "emby_pathverified", "emby_sync", "emby.restart", "emby.sync.pause", - "emby.server.state", "emby.server.states" - ] + if self.Utils.Settings.emby_shouldstop: + self.ShouldStop = True - for server in self.Utils.window('emby.server.states.json') or []: - properties.append("emby.server.%s.state" % server) + if self.Utils.Settings.emby_restart: + self.Utils.Settings.emby_restart = False + self.Utils.dialog("notification", heading="{emby}", message=self.Utils.Translate(33193), icon="{emby}", time=1000, sound=False) + self.shutdown() + self.LOG.warning("[ RESTART ]") + return True - for prop in properties: - self.Utils.window(prop, clear=True) + if self.Monitor.sleep: + xbmc.sleep(5000) + self.Monitor.System_OnWake() + if self.ShouldStop: + self.shutdown() + return False + + def shutdown(self): + self.LOG.warning("---<[ EXITING ]") + self.Monitor.player.SyncPause = True + self.ShouldStop = True self.Monitor.QuitThreads() - emby.main.Emby.close_all() - self.Monitor.LibraryStop() - self.LOG.warning("---<<<[ %s ]", self.Utils.get_addon_name()) - helper.loghandler.reset() - raise Exception("ExitService") - -class StartDefaultServer(threading.Thread): - def __init__(self, service, retry=None, close=False): - self.service = service - self.retry = retry - self.close = close - threading.Thread.__init__(self) - self.start() - - #This is a thread to not block the main service thread - def run(self): - try: - if 'default' in emby.main.Emby.client: - self.service.Utils.window('emby_online', clear=True) - emby.main.Emby().close() - self.service.Monitor.LibraryStop() - - if self.close: - raise Exception("terminate default server thread") - - if self.retry and xbmc.Monitor().waitForAbort(self.retry) or not self.service.running: - raise Exception("abort default server thread") - - self.service.Monitor.Register() - self.service.Setup.setup() - self.service.Monitor.LibraryLoad() - self.service.Monitor.PlayMode = self.service.Utils.settings('useDirectPaths') - except Exception as error: - #LOG.error(error) # we don't really case, self.Utils.event will retrigger if need be. - pass - - self.service.server_thread.remove(self) + self.Monitor.EmbyServer_DisconnectAll() + self.Monitor.LibraryStopAll() if __name__ == "__main__": + serviceOBJ = Service() + while True: - serviceOBJ = None + serviceOBJ.ServerConnect() - try: - serviceOBJ = Service() + if serviceOBJ.WatchDog(): + serviceOBJ.Startup() + continue #Restart - if not serviceOBJ.Start(): - break + break - serviceOBJ.WatchDog() - except Exception as error: - if serviceOBJ is not None: - if 'ExitService' in error.args: - break - elif 'RestartService' in error.args: - continue - elif 'Unknown addon id' in error.args[0]: - break + serviceOBJ = None