From ba22e26c062c218bf39c3067314462d051c5868a Mon Sep 17 00:00:00 2001 From: angelblue05 Date: Fri, 28 Oct 2016 00:02:47 -0500 Subject: [PATCH] 2.3.0 (#70) --- CONTRIBUTING.md | 5 +- README.md | 8 +- addon.xml | 26 +- changelog.txt | 107 + contextmenu.py | 164 +- default.py | 188 +- resources/language/Dutch/strings.xml | 185 +- resources/language/English/strings.xml | 44 +- resources/language/French/strings.xml | 329 +++ resources/language/German/strings.xml | 350 +++ resources/language/Portuguese/strings.xml | 350 +++ resources/language/Russian/strings.xml | 41 +- resources/language/Swedish/strings.xml | 2 +- resources/lib/api.py | 269 +- resources/lib/artwork.py | 548 ++-- resources/lib/clientinfo.py | 101 +- resources/lib/connect.py | 257 -- resources/lib/{dialog => connect}/__init__.py | 0 resources/lib/connect/connectionmanager.py | 816 ++++++ resources/lib/connect/credentials.py | 147 ++ resources/lib/connectmanager.py | 231 ++ resources/lib/context_entry.py | 204 ++ resources/lib/dialog/loginconnect.py | 76 - resources/lib/dialogs/__init__.py | 6 + resources/lib/dialogs/context.py | 93 + resources/lib/dialogs/loginconnect.py | 136 + resources/lib/dialogs/loginmanual.py | 145 + resources/lib/dialogs/serverconnect.py | 145 + resources/lib/dialogs/servermanual.py | 145 + resources/lib/dialogs/usersconnect.py | 104 + resources/lib/downloadutils.py | 383 +-- resources/lib/embydb_functions.py | 43 +- resources/lib/entrypoint.py | 160 +- resources/lib/ga_client.py | 164 ++ resources/lib/image_cache_thread.py | 56 +- resources/lib/initialsetup.py | 231 +- resources/lib/itemtypes.py | 2338 +---------------- resources/lib/kodidb_functions.py | 1044 +------- resources/lib/kodimonitor.py | 284 +- resources/lib/librarysync.py | 827 ++---- resources/lib/loghandler.py | 23 +- resources/lib/musicutils.py | 2 +- resources/lib/objects/__init__.py | 5 + resources/lib/objects/_common.py | 202 ++ resources/lib/objects/_kodi_common.py | 813 ++++++ resources/lib/objects/_kodi_movies.py | 225 ++ resources/lib/objects/_kodi_music.py | 406 +++ resources/lib/objects/_kodi_musicvideos.py | 66 + resources/lib/objects/_kodi_tvshows.py | 170 ++ resources/lib/objects/movies.py | 453 ++++ resources/lib/objects/music.py | 709 +++++ resources/lib/objects/musicvideos.py | 299 +++ resources/lib/objects/tvshows.py | 818 ++++++ resources/lib/playbackutils.py | 82 +- resources/lib/player.py | 54 +- resources/lib/playlist.py | 178 +- resources/lib/playutils.py | 227 +- resources/lib/read_embyserver.py | 56 +- resources/lib/service_entry.py | 320 +++ resources/lib/userclient.py | 548 ++-- resources/lib/utils.py | 166 +- resources/lib/videonodes.py | 31 +- resources/lib/websocket_client.py | 467 ++-- resources/settings.xml | 45 +- .../script-emby-connect-login-manual.xml | 145 + .../1080i/script-emby-connect-login.xml | 84 +- .../script-emby-connect-server-manual.xml | 154 ++ .../1080i/script-emby-connect-server.xml | 280 ++ .../1080i/script-emby-connect-users.xml | 198 ++ .../default/1080i/script-emby-context.xml | 109 + .../script-emby-kodi-UserPreferences.xml | 129 - resources/skins/default/media/emby-icon.png | Bin 0 -> 1850 bytes .../{separator.png => emby-separator.png} | Bin .../skins/default/media/fading_circle.png | Bin 0 -> 1906 bytes resources/skins/default/media/network.png | Bin 0 -> 727 bytes resources/skins/default/media/user_image.png | Bin 0 -> 662 bytes .../skins/default/media/userflyoutdefault.png | Bin 0 -> 1338 bytes .../default/media/userflyoutdefault2.png | Bin 0 -> 1700 bytes resources/skins/default/media/wifi.png | Bin 0 -> 1095 bytes service.py | 309 +-- 80 files changed, 11580 insertions(+), 6945 deletions(-) create mode 100644 resources/language/French/strings.xml create mode 100644 resources/language/German/strings.xml create mode 100644 resources/language/Portuguese/strings.xml delete mode 100644 resources/lib/connect.py rename resources/lib/{dialog => connect}/__init__.py (100%) create mode 100644 resources/lib/connect/connectionmanager.py create mode 100644 resources/lib/connect/credentials.py create mode 100644 resources/lib/connectmanager.py create mode 100644 resources/lib/context_entry.py delete mode 100644 resources/lib/dialog/loginconnect.py create mode 100644 resources/lib/dialogs/__init__.py create mode 100644 resources/lib/dialogs/context.py create mode 100644 resources/lib/dialogs/loginconnect.py create mode 100644 resources/lib/dialogs/loginmanual.py create mode 100644 resources/lib/dialogs/serverconnect.py create mode 100644 resources/lib/dialogs/servermanual.py create mode 100644 resources/lib/dialogs/usersconnect.py create mode 100644 resources/lib/ga_client.py create mode 100644 resources/lib/objects/__init__.py create mode 100644 resources/lib/objects/_common.py create mode 100644 resources/lib/objects/_kodi_common.py create mode 100644 resources/lib/objects/_kodi_movies.py create mode 100644 resources/lib/objects/_kodi_music.py create mode 100644 resources/lib/objects/_kodi_musicvideos.py create mode 100644 resources/lib/objects/_kodi_tvshows.py create mode 100644 resources/lib/objects/movies.py create mode 100644 resources/lib/objects/music.py create mode 100644 resources/lib/objects/musicvideos.py create mode 100644 resources/lib/objects/tvshows.py create mode 100644 resources/lib/service_entry.py create mode 100644 resources/skins/default/1080i/script-emby-connect-login-manual.xml create mode 100644 resources/skins/default/1080i/script-emby-connect-server-manual.xml create mode 100644 resources/skins/default/1080i/script-emby-connect-server.xml create mode 100644 resources/skins/default/1080i/script-emby-connect-users.xml create mode 100644 resources/skins/default/1080i/script-emby-context.xml delete mode 100644 resources/skins/default/1080i/script-emby-kodi-UserPreferences.xml create mode 100644 resources/skins/default/media/emby-icon.png rename resources/skins/default/media/{separator.png => emby-separator.png} (100%) create mode 100644 resources/skins/default/media/fading_circle.png create mode 100644 resources/skins/default/media/network.png create mode 100644 resources/skins/default/media/user_image.png create mode 100644 resources/skins/default/media/userflyoutdefault.png create mode 100644 resources/skins/default/media/userflyoutdefault2.png create mode 100644 resources/skins/default/media/wifi.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e365a5a8..a220718d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,7 @@ Thanks you for contributing to Emby for Kodi! -* Please make pull requests towards the **develop** branch, not the master branch. -* Try to keep the maximum line length shorter than 100 characters to keep things clean and readable. +* Make pull requests towards the **develop** branch; +* Keep the maximum line length shorter than 100 characters to keep things clean and readable; +* Follow pep8 style as closely as possible: https://www.python.org/dev/peps/pep-0008/ * Add comments if necessary. diff --git a/README.md b/README.md index 849b3b20..750ea44f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, bea View this short [Youtube video](https://youtu.be/IaecDPcXI3I?t=119) to give you a better idea of the general process. 1. Install the Emby for Kodi repository, from the repo install the Emby addon. -2. Within a few seconds you should be prompted for your server-details (or it may be auto discovered). If not, try to restart Kodi +2. Within a few seconds you should be prompted for your server-details. 3. Once you're succesfully authenticated with your Emby server, the initial sync will start. 4. The first sync of the Emby server to the local Kodi database may take some time depending on your device and library size. 5. Once the full sync is done, you can browse your media in Kodi, and syncs will be done automatically in the background. @@ -38,15 +38,19 @@ Emby for Kodi is under constant development. The following features are currentl + TV Shows + Music Videos + Music + + Home Videos + + Pictures - Emby for Kodi context menu: + Mark content as favorite + Refresh content + Delete content - Direct play and transcode - Watched state/resume status sync: This is a 2-way synchronisation. Any watched state or resume status will be instantly (within seconds) reflected to or from Kodi and the server. +- Remote control your Kodi; send play commands from your Emby webclient or Emby mobile apps. - Copy Theme Music locally for use with the TV Tunes addon - Copy ExtraFanart (rotating backgrounds) across for use with skins that support it - Offer to delete content after playback +- **New!** Backup your emby kodi profile. See the [Emby backup option](https://github.com/MediaBrowser/plugin.video.emby/wiki/Create-and-restore-from-backup) - and more... ### What is being worked on @@ -56,5 +60,5 @@ Have a look at our [Trello board](https://trello.com/b/qBJ49ka4/emby-for-kodi) t Solutions to the following issues are unlikely due to Kodi limitations. - Chapter images are missing unless native playback mode is used. - Certain add-ons that depend on seeing where your content is located will not work unless native playback mode is selected. -- External subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly unless direct playing +- ~~External subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly unless direct playing~~ - Kodi only accepts direct paths for music content unlike the video library. Your Emby music library path will need to be formatted appropriately to work in Kodi (e.g: "\\\\server\Music\Album\song.ext"). See the [Emby wiki](https://github.com/MediaBrowser/Wiki/wiki/Path%20Substitution) for additional information. diff --git a/addon.xml b/addon.xml index 1dd2f9f5..4454b921 100644 --- a/addon.xml +++ b/addon.xml @@ -1,14 +1,14 @@ - - - - - + + + + + @@ -17,19 +17,19 @@ - - + + Settings for the Emby Server - [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1)] | !IsEmpty(ListItem.Property(embyid)) - - + [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(embyid))] + !IsEmpty(Window(10000).Property(emby_context)) + + all en GNU GENERAL PUBLIC LICENSE. Version 2, June 1991 - + http://emby.media/community/index.php?/forum/99-kodi/ http://emby.media/ - + https://github.com/MediaBrowser/plugin.video.emby Welcome to Emby for Kodi A whole new way to manage and view your media library. The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server. Emby for Kodi is the absolute best way to enjoy the incredible Kodi playback engine combined with the power of Emby's centralized database. Features: Direct integration with the Kodi library for native Kodi speed Instant synchronization with the Emby server Full support for Movie, TV and Music collections Emby Server direct stream and transcoding support - use Kodi when you are away from home! diff --git a/changelog.txt b/changelog.txt index f4f3314a..ca96f03e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,110 @@ +version 2.3.0 +- New stable version + +version 2.2.57 +- Fix for external subtitles while using HTTP playback +- Minor bug fixes + +version 2.2.52-54 +- Fix bugs that slipped in the few previous builds + +version 2.2.51 +- Rework manual sync +- Review OS +- Review view removal + +version 2.2.50 +- Add Kodi version to logging + +version 2.2.49 +- Add OS, Resolution and Lang to logging + +version 2.2.47 +- Small fix for logging + +version 2.2.46 +- Error logging improvments + +version 2.2.43 +- Review music +- Clean up syncing code + +version 2.2.41 +- Emergency update + +version 2.2.40 +- Review throttle +- Fix user migration for the new login method added after 2.2.19 (stable) +- Clean up code + +version 2.2.39 +- Update German translation +- Fix issue with throttle +- Fix server detection + +version 2.2.38 +- Fix series pooling + +version 2.2.37 +- Support favorite episodes +- Update Dutch translation +- Fix specials -1 bug +- minor fixes + +version 2.2.34 +- Repair sync can now be filtered by content type +- Automatically download external subs with language tag +- Fix broken music artwork +- Fix to music direct stream + +version 2.2.33 +- Fix manual sync crashing +- Update Portuguese translation +- Add support for German MPAA rating. You will need to reset your local database to apply the change. +- Add a backup option. Find out more: https://github.com/MediaBrowser/plugin.video.emby/wiki/Create-and-restore-from-backup + +version 2.2.32 +- Update the emby context menu +- Add option to disable the context menu in the add-on settings > extras tab + +version 2.2.31 +- Support series pooling. Will require to reset local database. Once content is resynced, proceed with a manual sync to apply the series pooling. +- Fix emby connect websocket issue, will also require updating server to beta 3.1.150 or higher +- Fix initial sync artwork dialog for Isengard + +version 2.2.30 +- Update German translation +- minor fixes + +version 2.2.28 +- Fix user selection when all users are hidden + +version 2.2.24 +- Filter music from fast sync response if music is disabled in the Kodi profile +- Fix restart server behavior in the add-on to fix post capabilities +- Fix ubuntu importerror crash +- Update Russian translation + +version 2.2.23 +- NEW! Emby connect integration. Find out more on the emby.media forums +- Sync season name for Jarvis and higher +- Expand video quality selection (25/30/35 Mpbs) +- Move deviceId to a permanent location to outlive reinstalls of the add-on +- Fix virtual episodes crashing sync +- Fix plugin listing such as home videos not loading +- Fix platform detection + +version 2.2.21 +- Fix new external subtitles from preventing playback in the event the subtitles had no language tag + +version 2.2.20 +- NEW: Default to HTTP playback when using add-on playback mode out of the box. +- Add string translation (German, French and Portuguese) +- Add option to download external subtitles when playing from HTTP (add-on settings > playback) +- Fix navigation not waking up display +- Fix fast sync being used to save update times when plugin is not installed +- Fix tv show detection when verifying if file exists. + version 2.2.19 - Fix transcode (logging error) diff --git a/contextmenu.py b/contextmenu.py index 454c7387..31323d4a 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -5,171 +5,33 @@ import logging import os import sys -import urlparse import xbmc import xbmcaddon -import xbmcgui ################################################################################################# -_addon = xbmcaddon.Addon(id='plugin.video.emby') -_addon_path = _addon.getAddonInfo('path').decode('utf-8') -_base_resource = xbmc.translatePath(os.path.join(_addon_path, 'resources', 'lib')).decode('utf-8') -sys.path.append(_base_resource) - -################################################################################################# - -import api -import artwork -import downloadutils -import librarysync -import read_embyserver as embyserver -import embydb_functions as embydb -import kodidb_functions as kodidb -import musicutils as musicutils -from utils import settings, language as lang, kodiSQL +_ADDON = xbmcaddon.Addon(id='plugin.video.emby') +_CWD = _ADDON.getAddonInfo('path').decode('utf-8') +_BASE_LIB = xbmc.translatePath(os.path.join(_CWD, 'resources', 'lib')).decode('utf-8') +sys.path.append(_BASE_LIB) ################################################################################################# import loghandler +from context_entry import ContextMenu + +################################################################################################# loghandler.config() log = logging.getLogger("EMBY.contextmenu") ################################################################################################# -# Kodi contextmenu item to configure the emby settings -if __name__ == '__main__': +if __name__ == "__main__": - kodiId = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') - itemType = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') - itemId = "" - - if not itemType: - - if xbmc.getCondVisibility("Container.Content(albums)"): - itemType = "album" - elif xbmc.getCondVisibility("Container.Content(artists)"): - itemType = "artist" - elif xbmc.getCondVisibility("Container.Content(songs)"): - itemType = "song" - elif xbmc.getCondVisibility("Container.Content(pictures)"): - itemType = "picture" - else: - log.info("ItemType is unknown.") - - if (not kodiId or kodiId == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"): - itemId = xbmc.getInfoLabel("ListItem.Property(embyid)") - - elif kodiId and itemType: - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(kodiId, itemType) - embycursor.close() - try: - itemId = item[0] - except TypeError: - pass - - - log.info("Found ItemId: %s ItemType: %s" % (itemId, itemType)) - if itemId: - - dialog = xbmcgui.Dialog() - - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemId) - API = api.API(item) - userdata = API.getUserData() - likes = userdata['Likes'] - favourite = userdata['Favorite'] - - options = [] - - if favourite: - # Remove from emby favourites - options.append(lang(30406)) - else: - # Add to emby favourites - options.append(lang(30405)) - - if itemType == "song": - # Set custom song rating - options.append(lang(30407)) - - # Refresh item - options.append(lang(30410)) - # Delete item - options.append(lang(30409)) - # Addon settings - options.append(lang(30408)) - - # Display select dialog and process results - resp = xbmcgui.Dialog().select(lang(30401), options) - if resp > -1: - selected = options[resp] - - if selected == lang(30410): - # Refresh item - emby.refreshItem(itemId) - elif selected == lang(30405): - # Add favourite - emby.updateUserRating(itemId, favourite=True) - elif selected == lang(30406): - # Delete favourite - emby.updateUserRating(itemId, favourite=False) - elif selected == lang(30407): - # Update song rating - kodiconn = kodiSQL('music') - kodicursor = kodiconn.cursor() - query = "SELECT rating FROM song WHERE idSong = ?" - kodicursor.execute(query, (kodiId,)) - try: - value = kodicursor.fetchone()[0] - current_value = int(round(float(value),0)) - except TypeError: - pass - else: - new_value = dialog.numeric(0, lang(30411), str(current_value)) - if new_value > -1: - - new_value = int(new_value) - if new_value > 5: - new_value = 5 - - if settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(new_value, API.getFilePath()) - - query = "UPDATE song SET rating = ? WHERE idSong = ?" - kodicursor.execute(query, (new_value, kodiId,)) - kodiconn.commit() - - '''if settings('enableExportSongRating') == "true": - like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(new_value) - emby.updateUserRating(itemId, like, favourite, deletelike)''' - finally: - kodicursor.close() - - elif selected == lang(30408): - # Open addon settings - xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)") - - elif selected == lang(30409): - # delete item from the server - delete = True - if settings('skipContextMenu') != "true": - resp = dialog.yesno( - heading=lang(29999), - line1=lang(33041)) - if not resp: - log.info("User skipped deletion for: %s." % itemId) - delete = False - - if delete: - log.info("Deleting request: %s" % itemId) - emby.deleteItem(itemId) - - xbmc.sleep(500) - xbmc.executebuiltin('Container.Refresh') \ No newline at end of file + try: + # Start the context menu + ContextMenu() + except Exception as error: + log.exception(error) diff --git a/default.py b/default.py index 9d9c098b..e52c524b 100644 --- a/default.py +++ b/default.py @@ -9,52 +9,75 @@ import urlparse import xbmc import xbmcaddon -import xbmcgui ################################################################################################# -_addon = xbmcaddon.Addon(id='plugin.video.emby') -_addon_path = _addon.getAddonInfo('path').decode('utf-8') -_base_resource = xbmc.translatePath(os.path.join(_addon_path, 'resources', 'lib')).decode('utf-8') -sys.path.append(_base_resource) +_ADDON = xbmcaddon.Addon(id='plugin.video.emby') +_CWD = _ADDON.getAddonInfo('path').decode('utf-8') +_BASE_LIB = xbmc.translatePath(os.path.join(_CWD, 'resources', 'lib')).decode('utf-8') +sys.path.append(_BASE_LIB) ################################################################################################# import entrypoint -import utils -from utils import window, language as lang +import loghandler +from utils import window, dialog, language as lang +from ga_client import GoogleAnalytics ################################################################################################# -import loghandler - loghandler.config() log = logging.getLogger("EMBY.default") ################################################################################################# -class Main(): - +class Main(object): # MAIN ENTRY POINT #@utils.profiling() + def __init__(self): # Parse parameters base_url = sys.argv[0] - params = urlparse.parse_qs(sys.argv[2][1:]) - log.warn("Parameter string: %s" % sys.argv[2]) + path = sys.argv[2] + params = urlparse.parse_qs(path[1:]) + log.warn("Parameter string: %s", path) try: mode = params['mode'][0] - itemid = params.get('id') - if itemid: - itemid = itemid[0] - except: - params = {} + except (IndexError, KeyError): mode = "" + if "/extrafanart" in base_url: + emby_path = path[1:] + emby_id = params.get('id', [""])[0] + entrypoint.getExtraFanArt(emby_id, emby_path) + + elif "/Extras" in base_url or "/VideoFiles" in base_url: + + emby_path = path[1:] + emby_id = params.get('id', [""])[0] + entrypoint.getVideoFiles(emby_id, emby_path) + + elif not self._modes(mode, params): + # Other functions + if mode == 'settings': + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + + elif mode in ('manualsync', 'fastsync', 'repair'): + self._library_sync(mode) + + elif mode == 'texturecache': + import artwork + artwork.Artwork().texture_cache_sync() + else: + entrypoint.doMainListing() + + @classmethod + def _modes(cls, mode, params): + import utils modes = { 'reset': utils.reset, @@ -72,78 +95,75 @@ class Main(): 'recentepisodes': entrypoint.getRecentEpisodes, 'refreshplaylist': entrypoint.refreshPlaylist, 'deviceid': entrypoint.resetDeviceId, - 'delete': entrypoint.deleteItem + 'delete': entrypoint.deleteItem, + 'connect': entrypoint.emby_connect, + 'backup': entrypoint.emby_backup } - - if "/extrafanart" in sys.argv[0]: - embypath = sys.argv[2][1:] - embyid = params.get('id',[""])[0] - entrypoint.getExtraFanArt(embyid,embypath) - return - - if "/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0]: - embypath = sys.argv[2][1:] - embyid = params.get('id',[""])[0] - entrypoint.getVideoFiles(embyid, embypath) - return - - if modes.get(mode): + if mode in modes: # Simple functions - if mode == "play": - dbid = params.get('dbid') - modes[mode](itemid, dbid) + action = modes[mode] + item_id = params.get('id') + if item_id: + item_id = item_id[0] - elif mode in ("nextup", "inprogressepisodes", "recentepisodes"): + if mode == 'play': + database_id = params.get('dbid') + action(item_id, database_id) + + elif mode in ('nextup', 'inprogressepisodes', 'recentepisodes'): limit = int(params['limit'][0]) - modes[mode](itemid, limit) - - elif mode in ("channels","getsubfolders"): - modes[mode](itemid) - - elif mode == "browsecontent": - modes[mode](itemid, params.get('type',[""])[0], params.get('folderid',[""])[0]) + action(item_id, limit) - elif mode == "channelsfolder": + elif mode in ('channels', 'getsubfolders'): + action(item_id) + + elif mode == 'browsecontent': + action(item_id, params.get('type', [""])[0], params.get('folderid', [""])[0]) + + elif mode == 'channelsfolder': folderid = params['folderid'][0] - modes[mode](itemid, folderid) - + action(item_id, folderid) else: - modes[mode]() - else: - # Other functions - if mode == "settings": - xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') - - elif mode in ("manualsync", "fastsync", "repair"): - - if window('emby_online') != "true": - # Server is not online, do not run the sync - xbmcgui.Dialog().ok(heading=lang(29999), - line1=lang(33034)) - log.warn("Not connected to the emby server.") - return - - if window('emby_dbScan') != "true": - import librarysync - lib = librarysync.LibrarySync() - if mode == "manualsync": - librarysync.ManualSync().sync() - elif mode == "fastsync": - lib.startSync() - else: - lib.fullSync(repair=True) - else: - log.warn("Database scan is already running.") - - elif mode == "texturecache": - import artwork - artwork.Artwork().fullTextureCacheSync() - - else: - entrypoint.doMainListing() + action() + + return True + + return False + + @classmethod + def _library_sync(cls, mode): + + if window('emby_online') != "true": + # Server is not online, do not run the sync + dialog(type_="ok", + heading="{emby}", + line1=lang(33034)) + log.warn("Not connected to the emby server") + + elif window('emby_dbScan') != "true": + import librarysync + library_sync = librarysync.LibrarySync() + + if mode == 'manualsync': + librarysync.ManualSync().sync() + elif mode == 'fastsync': + library_sync.startSync() + else: + library_sync.fullSync(repair=True) + else: + log.warn("Database scan is already running") + - if __name__ == "__main__": - log.info('plugin.video.emby started') - Main() - log.info('plugin.video.emby stopped') \ No newline at end of file + + log.info("plugin.video.emby started") + + try: + Main() + except Exception as error: + ga = GoogleAnalytics() + errStrings = ga.formatException() + ga.sendEventData("Exception", errStrings[0], errStrings[1]) + log.exception(error) + + log.info("plugin.video.emby stopped") diff --git a/resources/language/Dutch/strings.xml b/resources/language/Dutch/strings.xml index a75eb29f..ff48b16f 100644 --- a/resources/language/Dutch/strings.xml +++ b/resources/language/Dutch/strings.xml @@ -9,12 +9,12 @@ Geavanceerd Gebruikersnaam Poortnummer - Aantal recente muziekalbums die getoond worden: - Aantal recente films die getoond worden: - Aantal recente TV-series die getoond worden: + Aantal te tonen recente Music Albums: + Aantal te tonen recente Films: + Aantal te tonen recente TV-series: Vernieuwen Wissen - Ongeldige gebruikersnaam/wachtwoord + Ongeldige Gebruikersnaam/Wachtwoord Gebruikersnaam niet gevonden Wissen... Wacht op server voor wissen @@ -35,16 +35,16 @@ Musical Mysterie Romantiek - Science Fiction + Sciencefiction Kort Spanning Thriller Western Genre Filter Bevestig bestandsverwijdering - - Bekeken markeren - Onbekeken markeren + + Markeer als bekeken + Markeer als onbekeken Sorteer op Sorteer oplopend Sorteer aflopend @@ -52,70 +52,79 @@ Hervatten Hervatten vanaf Start vanaf begin - Toon verwijder mogelijkheid na het afspelen - + Verwijderen na afspelen aanbieden + Voor Afleveringen - + Voor Films - + Hervat-percentage toevoegen Afleveringnummer toevoegen Toon voortgang - Laden van content - Ontvang Data + Inhoud laden + Data ontvangen Gereed Waarschuwing - + Fout Zoeken Activeer speciale afbeeldingen (bijv. CoverArt) - + Metadata Afbeeldingen Video kwaliteit - + Direct Afspelen - + Transcoderen Server Detectie Geslaagd Gevonden server Adres: Onlangs toegevoegde TV-series + Niet afgekeken TV-series + Alle Muziek Kanalen + Onlangs toegevoegde films + Onlangs toegevoegde afleveringen + Onlangs toegevoegde albums Niet afgekeken films + Niet afgekeken afleveringen + Volgende (NextUp) afleveringen + Favoriete films + Favoriete TV-series - + Favoriete afleveringen Vaak afgespeelde albums Binnenkort op TV BoxSets Trailers - Muziek video\'s - Foto\'s - Onbekeken films - + Muziek video's + Foto's + Onbekeken Films + Film Genres - Film Studio\'s + Film Studio's Film Acteurs - Onbekeken afleveringen + Onbekeken Afleveringen TV Genres - TV Netwerken + TV Studio's TV Acteurs Afspeellijsten Weergaven instellen Selecteer gebruiker - + Kan niet verbinden met server - Nummers + Titels Albums Album artiesten Artiesten @@ -128,27 +137,36 @@ Willekeurige Films Willekeurige Afleveringen Willekeurige Items - + Aanbevolen Items - Extra\'s - - Synchroniseer Thema Muziek + + Extra's + + Synchroniseer Herkenningsmelodie Synchroniseer Extra Fanart Synchroniseer Film Boxsets Lokale Kodi database opnieuw instellen - + Activeer HTTPS + Forceer Transcoderen van Codecs Activeer server verbindings melding bij het opstarten - Onlangs bekeken Thuis Video\'s - Onlangs toegevoegde Foto\'s - Favoriete Home Video\'s - Favoriete Foto\'s + + Onlangs bekeken Thuis Video's + + Onlangs toegevoegde Foto's + + Favoriete Home Video's + + Favoriete Foto's + Favoriete Albums - Onlangs toegevoegde Muziek Video\'s - Niet afgekeken Muziek Video\'s - Onbekeken Muziek Video\'s - + Onlangs toegevoegde Muziek Video's + + Niet afgekeken Muziek Video's + + Onbekeken Muziek Video's + Actief Herstel naar standaard @@ -160,18 +178,18 @@ Afleveringen Muziek - artiesten Muziek - albums - Muziek Video\'s + Muziek Video's Muziek - nummers Kanalen Emby opties Toevoegen aan Emby favorieten Verwijderen uit Emby favorieten - Instellen aangepaste waardering nummers + Instellen aangepaste waardering titels Emby addon instellingen Verwijder item van de server Dit item vernieuwen - Instellen aangepaste waardering voor nummers (0-5) + Instellen aangepaste titel waardering (0-5) Controleer Host SSL Certificaat Client SSL Certificaat @@ -179,26 +197,26 @@ Alternatief Serveradres Gebruik alternatieve apparaatnaam [COLOR yellow]Probeer opnieuw in te loggen[/COLOR] - Synchronisatie Opties - Toon synchronisatie voortgang + Synchronisatie + Toon voortgang indien meer items dan Synchroniseer lege TV-series Activeer Muziekbibliotheek Direct Stream muziekbibliotheek Afspeelmodus Forceer afbeelding caching Limiteer afbeelding cache threads (aanbevolen voor rpi) - Activeer \"fast startup\" (vereist server plugin) - Maximaal aantal items om tegelijk van de server op te vragen + Activeer [COLOR yellow]Fast Startup[/COLOR] (vereist server plugin) + Maximaal aantal ineens van de server op te vragen items Afspelen Netwerkreferenties Activeer Emby bioscoopmodus Vragen om trailers af te spelen - Sla Emby verwijderings bevestiging over voor het contextmenu (Gebruik op eigen risico) + Wis bevestiging in contextmenu overslaan (Eigen risico!) Spring terug op hervatten (in seconden) Forceer transcoderen H265 Muziek metadata opties (niet compatibel met direct stream) - Importeer muziek lied waardering direct uit bestanden - Converteer muziek lied waardering naar Emby waardering + Importeer muziek titel waardering direct uit bestanden + Converteer muziek titel waardering naar Emby waardering Toestaan dat waardeing in muziekbestanden worden bijgewerkt Negeer specials in de volgende afleveringen Gebruikers permanent toevoegen aan de sessie @@ -212,6 +230,33 @@ Sync als screensaver is uitgeschakeld Forceer Transcoderen Hi10P Uitgeschakeld + Inloggen + Handmatig inloggen + Emby Connect + Server + Gebruikersnaam of e-mail + Activeer database Vergrendeld fix (synchronisatie proces zal vertragen) + Activeer server offline bericht + + Meld je aan met Emby Connect + Wachtwoord + Zie onze gebruiksvoorwaarden. Het gebruik van alle Emby software betekent acceptatie van deze voorwaarden. + Scan mij + Aanmelden + Annuleren + Hoofdserver selecteren + Gebruikersnaam of wachtwoord mag niet leeg zijn + Kan geen verbinding maken met de geselecteerde server + Verbinding maken met + + Handmatig server toevoegen + Meld u aub aan + Gebruikersnaam mag niet leeg zijn + Verbinding maken met server + Host + Verbinden + Server of poort kan niet leeg zijn + Wissel van Emby Connect gebruiker Welkom Fout bij maken van verbinding @@ -223,36 +268,36 @@ Toegang is ingeschakeld Voer het wachtwoord in voor de gebruiker: Foutieve gebruikersnaam of wachtwoord - Niet te vaak te authenticeren + Te vaak mislukt te authenticeren Niet in staat om direct af te spelen Direct afspelen is 3x mislukt. Afspelen vanaf HTTP Ingeschakeld. Kies het audiokanaal Kies de ondertiteling - Verwijder bestand van Emby server? + Bestand van uw Emby server verwijderen? Trailers afspelen? - Verzamelen films van: - verzamelen boxsets - Verzamelen muziek-video\'s van: - Verzamelen TV-series van: + Films verzamelen van: + Boxsets verzamelen + Muziek-video's verzamelen van: + TV-series verzamelen van: Verzamelen: Gedetecteerd dat de database moet worden vernieuwd voor deze versie van Emby voor Kodi. Doorgaan? Emby voor Kodi kan mogelijk niet correct werken tot de database is teruggezet. Database synchronisatie proces geannuleerd. De huidige Kodi versie wordt niet ondersteund. voltooid in: - Vergelijken van films uit: - Vergelijken van boxsets - Vergelijken van muziek-video\'s uit: - Vergelijken van TV-series uit: - Vergelijken van afleveringen uit: + Films vergelijken met: + Boxsets vergelijken met + Muziek-video's vergelijken met: + TV-series vergelijken met: + Afleveringen vergelijken met: Vergelijken: - Mislukt een nieuw apparaat Id te genereren. Zie je logs voor meer informatie. - Er is een nieuw apparaat Id gegenereerd. Kodi zal nu opnieuw opstarten. + Nieuw apparaat Id genereren mislukt. Zie je logs voor meer informatie. + Nieuw apparaat Id gegenereerd. Kodi zal nu opnieuw opstarten. Verder gaan met de volgende server? - LET OP! Als u de modus Native kiest, zullen bepaalde Emby functies ontbreken, zoals: Emby bioscoop-modus, directe afspeel/transcodeer opties en ouderlijk-toezicht planner. + LET OP! Als u de Native modus kiest, zullen bepaalde Emby functies ontbreken, zoals: Emby bioscoop-modus, directe afspeel/transcodeer opties en de ouderlijk-toezicht planner. Addon (Standaard) Native (Directe paden) - Voeg netwerkreferenties toe aan Kodi om toegang tot uw content toe te staan? Belangrijk: Kodi moet opnieuw worden opgestart om de referenties te zien. Zij kunnen ook later worden toegevoegd. - Emby muziekbibliotheek uitschakelen? + Voeg netwerkreferenties toe aan Kodi om toegang tot uw inhoud toe te staan? Belangrijk: Kodi moet opnieuw worden opgestart om de referenties te zien. Zij kunnen ook later worden toegevoegd. + De-activeer Emby muziekbibliotheek? Direct Stream de muziekbibliotheek? Selecteer deze optie als de muziekbibliotheek op afstand worden benaderd. Bestand(en) van de Emby Server verwijderen? Dit zal de bestanden ook van de schijf verwijderen! Het uitvoeren van de caching proces kan enige tijd duren. Toch verder gaan? @@ -273,7 +318,7 @@ Reparatie lokale database (forceer-bijwerken van alle inhoud) Lokale database herstellen Cache alle afbeeldingen - Sync Emby Thema Media naar Kodi + Sync herkenningsmelodie/leaders van Emby naar Kodi Gebruiker toevoegen/verwijderen van de sessie Gebruiker toevoegen Gebruiker verwijderen @@ -285,9 +330,9 @@ De taak is geslaagd De taak is mislukt Direct Stream - Afspeel methode voor uw thema\'s + Afspeel methode voor uw herkenningsmelodie/leaders Het instellingenbestand bestaat niet in TV-Tunes. Wijzig een instelling en voer de taak opnieuw uit. - Weet u zeker dat u uw lokale Kodi database opnieuw wilt instellen? + Weet u zeker dat u uw lokale Kodi database opnieuw in wilt stellen? Wijzig/verwijder netwerkreferenties Wijzig Verwijder diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 1f626dd9..1061b58e 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -3,13 +3,14 @@ Emby for Kodi - Primary Server Address - Play from HTTP instead of SMB - Log level - Device Name + Server address + Server name + Play from HTTP instead of SMB + Log level + Device Name Advanced - Username - Port Number + Username + Port Number Number of recent Music Albums to show: Number of recent Movies to show: @@ -182,13 +183,14 @@ Delete item from the server Refresh this item Set custom song rating (0-5) + Transcode Verify Host SSL Certificate Client SSL certificate Use alternate address Alternate Server Address - Use altername device Name + Use altername device name [COLOR yellow]Retry login[/COLOR] Sync Options Show progress if item count greater than @@ -200,7 +202,7 @@ Limit artwork cache threads (recommended for rpi) Enable fast startup (requires server plugin) Maximum items to request from the server at once - Playback + Video Playback Network credentials Enable Emby cinema mode Ask to play trailers @@ -229,15 +231,29 @@ Server Username or email Enable database locked fix (will slow syncing process) + Enable server offline message + Enable analytic metric logging + Display message (in seconds) Sign in with Emby Connect - Username or email: - Password: + Password Please see our terms of use. The use of any Emby software constitutes acceptance of these terms. Scan me Sign in - Remind me later + Cancel + Select main server + Username or password cannot be empty + Unable to connect to the selected server + Connect to + Manually add server + Please sign in + Username cannot be empty + Connect to server + Host + Connect + Server or port cannot be empty + Change Emby Connect user Welcome @@ -330,5 +346,11 @@ Remove all cached artwork? Reset all Emby add-on settings? Database reset has completed, Kodi will now restart to apply the changes. + Enter folder name for backup + Replace existing backup? + Create backup at: + Create a backup + Backup folder + Select content type to repair \ No newline at end of file diff --git a/resources/language/French/strings.xml b/resources/language/French/strings.xml new file mode 100644 index 00000000..9481c755 --- /dev/null +++ b/resources/language/French/strings.xml @@ -0,0 +1,329 @@ + + + + Emby pour Kodi + Adresse principale du serveur + + Lire avec HTTP à la place de SMB + + Niveau de journalisation + + Nom de l'appareil + + Avancé + Nom d'utilisateur + + Numéro de port + + Nombre d'album de musique récents à afficher: + Nombre de films récents à afficher: + Nombre d'épisodes télévisés récents à afficher: + Actualiser + Supprimer + Nom d'utilisateur/Mot de passe incorrect + Nom d'utilisateur introuvable + Suppression + En attente du serveur pour la suppression + Trier par + Aucun + Action + Aventure + Animation + Crime + Comédie + Documentaire + Drame + Fantaisie + Étranger + Historique + Horreur + Musique + Musical + Mystère + Romance + Science Fiction + Court + Suspense + Thriller + Western + Filtre de Genre + Confirmer la suppression du fichier + + Marquer comme lu + Marquer comme non vu + Trier par + Ordre de Trie décroissant + Ordre de Trie croissant + + Reprendre + Reprendre à partir de + Lire depuis le début + Offrir la possibilité de supprimer après la lecture + + Pour Épisodes + + Pour Films + + Ajouter un pourcentage de reprise + Ajouter Numéro Épisode + Afficher la progression du chargement + Chargement du contenu + Récupération des données + Fait + Avertissement + + Erreur + Rechercher + Activer les images améliorées (eg Coverart) + + Métadonnées + Artwork + Qualité vidéo + + Lecture directe + + Transcodage + Détection du serveur Réussi + Serveur trouvé + Addresse: + + Séries TV Récemment Ajouté + + Séries TV En cours + + Toute la musique + Chaînes + + Films récemment ajoutés + + Épisodes récemment ajoutés + + Albums récemment ajoutés + Films en Cours + + Épisodes en Cours + + Prochain Épisodes + + Films Favoris + + Séries Favorites + + Épisodes Favoris + Albums fréquemment joués + TV à venir + Collections + Bandes-annonces + Vidéo Clips + Photos + Films Non vu + + Film Genres + Film Studios + Film Acteurs + Épisodes Non vu + TV Genres + TV Réseaux + TV Acteurs + Listes de lecture + Définir Vues + Sélectionner l'utilisateur + + Impossible de se connecter au serveur + Chansons + Albums + Artiste de l'album + Artistes + Music Genres + Derniers + En cours + Prochain + Vues de l'utilisateur + Rapport Metrics + Films aléatoire + Épisodes aléatoire + Objets aléatoire + + Élements recommandés + + Extras + + Sync Thème Musique + Sync Extra Fanart + Sync Saga Films + Réinitialiser la base de données locale de Kodi + + Activer HTTPS + + Forcer le transcodage Codecs + Activer le message de connexion au serveur pendant le démarrage + + Vidéos personnel récemment ajoutés + + Photos récemment ajoutés + + Vidéos personnelles favorites + + Photos favorites + + Albums favoris + Vidéo Clips récemment ajoutés + + Vidéo Clips en cours + + Vidéo Clips non vu + + + Actif + Effacer les Paramètres + Films + Collections + Bandes-annonces + Séries + Saisons + Épisodes + Artistes musicaux + Albums de musique + Vidéo Clips + Pistes Musicales + Chaînes + + Emby options + Ajouter aux favoris Emby + Supprimer des favoris Emby + Définir une note personnalisée de la chanson + Paramètres addon Emby + Supprimer un élément du serveur + Actualiser cet article + Définir une note personnalisée de la chanson (0-5) + + Vérifier certificat SSL + Certificat SSL client + Utiliser une adresse alternative + Adresse du serveur alternatif + Utiliser un nom alternatif de périphérique + [COLOR yellow]Relancez la connexion[/COLOR] + Options de synchronisation + Afficher l'avancement de la synchro + Sync Séries TV vides + Activer la bibliothèque musicale + Direct stream bibliothèque musicale + Mode de lecture + Force mise en cache d'artwork + Limiter artwork en cache (recommandé pour rpi) + Activer le démarrage rapide (nécessite le plugin du serveur) + Nombre maximum d'éléments à demander à partir du serveur en une seule fois + Lecture + Identifiants réseau + Activer le mode cinéma + Demander à jouer des bandes annonces + Ne pas demander de confirmation de suppression pour le menu contextuel (utiliser à vos risques et périls) + Aller en arrière à la reprise (en secondes) + Force transcode H265 + Options métadonnées musique (non compatibles avec direct stream) + Importation de la note de la chanson directement à partir des fichiers + Convertir la note du morceau pour Emby note + Autoriser les notes dans les fichiers des chansons à être mis à jour + Ignorer les spéciaux dans les épisodes suivants + Utilisateurs permanents à ajouter à la session + Délai de démarrage (en secondes) + Activer le message redémarrage du serveur + Activer une notification nouveau contenu + Durée de la fenêtre de la bibliothèque vidéo (en secondes) + Durée de la fenêtre de la bibliothèque musical (en secondes) + Messages du serveur + Générer un nouveau Id d'appareil + Sync si l'écran est désactivé + Force Transcode Hi10P + Désactivé + + Bienvenue + Erreur de connexion + Le Serveur est inaccessible + Le Serveur est en ligne + Éléments ajoutés à la liste de lecture + Éléments en file d'attente à la liste de lecture + Le serveur redémarre + L'accès est activé + Entrer le mot de passe pour l'utilisateur: + Utilisateur ou mot de passe invalide + Échec de l'authentification de trop nombreuses fois + Lecture directe impossible + Lecture directe a échoué 3 fois. Activer la lecture a partir de HTTP. + Choisissez le flux audio + Choisissez le flux de sous-titres + Supprimer le fichier de votre serveur Emby? + Lire bande-annonces ? + Récupération des films à partir de: + Récupération collections + Récupération des vidéo clips à partir de: + Récupération des séries Tv à partir de: + Récupération: + La base de données doit être créé pour cette version de Emby pour Kodi. Procéder ? + Emby pour Kodi peut ne pas fonctionner correctement jusqu'à ce que la base de données soit remise à zéro. + Annulation du processus de synchronisation de la base de données. La version actuelle de Kodi n’est pas prise en charge. + complété en: + Comparaison des films à partir de: + Comparaison des collections + Comparaison des vidéo clips à partir de: + Comparaison des séries tv à partir de: + Comparaison des épisodes à partir de: + Comparaison: + Impossible de générer un nouvel ID de périphérique. Consultez les fichiers journaux pour plus d'informations. + Un nouvel Id de périphérique a été généré. Kodi va redémarrer maintenant. + Procédez avec le serveur suivant ? + Avertissement ! Si vous choisissez le mode natif, certaines fonctionnalités de Emby seront manquantes, tels que: Emby mode cinéma, direct stream/transcode et planification d'accès parental. + Addon (Par défaut) + Natif (Chemins directs) + Ajouter les informations d'identification du réseau pour permettre l'accès à votre contenu Kodi ? Important: Kodi devra être redémarré pour voir les informations d'identification. Elles peuvent également être ajoutés à un moment ultérieur. + Désactiver bibliothèque musicale Emby? + Direct stream la bibliothèque musicale ? Sélectionnez cette option si la bibliothèque musicale sera accessible à distance. + Supprimer les fichiers de Emby Server ? Cela permettra également de supprimer les fichiers du disque ! + L'exécution du processus de mise en cache peut prendre un certain temps. + Artwork cache sync + Réinitialiser le cache des artwork existant ? + Mise à jour du cache des artwork : + Attendre que tous les sujets aient quitter : + Kodi ne peut pas localiser le fichier: + Vous devrez peut-être vérifier vos informations d'identification de réseau dans les paramètres add-on ou utiliser la substitution de chemin Emby pour formater votre chemin correctement (Emby tableau de bord> bibliothèque). Arrêter la synchronisation ? + Ajoutée: + Si vous ne parvenez pas à vous connecter de trop nombreuses fois, le serveur Emby peut verrouiller votre compte. Continuer quand même ? + Chaînes TV en direct (expérimental) + Enregistrements TV en direct (expérimental) + Paramètres + Ajouter l'utilisateur à la session + Actualiser listes de lecture/nœuds vidéo d'Emby + Effectuer une sync manuelle + Réparer la base de données locale (force la mise à jour de tout le contenu) + Effectuer une réinitialisation de la base de données locale + Mettre en cache tout les artwork + Sync Emby Thème Media pour Kodi + Ajouter/Supprimer l'utilisateur de la session + Ajouter un utilisateur + Supprimer un utilisateur + Supprimer l'utilisateur de la session + Réussi ! + Suppression de la session de visualisation: + Ajouté à la session de visualisation: + Impossible d'ajouter/supprimer l'utilisateur de la session. + La tâche a réussi + La tâche a échoué + Direct Stream + Méthode de lecture pour votre thèmes + Le fichier de paramètres n'existe pas dans Tunes TV. Modifier un paramètre et exécutez à nouveau la tâche. + Êtes-vous sûr de vouloir réinitialiser votre base de données locale Kodi ? + Modifier/Supprimer les informations d'identification du réseau + Modifier + Supprimer + Supprimé: + Entrer le nom d'utilisateur réseau + Entrer le mot de passe réseau + Ajouter des informations d'identification de réseau pour: + Entrez le nom du serveur ou l'adresse IP comme indiqué dans vos chemins de bibliothèque de Emby. Par exemple, le nom du serveur: \\\\SERVEUR-PC\\chemin\\ est \"SERVEUR-PC\" + Modifier le nom du serveur ou l'adresse IP + Entrer le nom du serveur ou l'adresse IP + Impossible de réinitialiser la base de données. Réessayer. + Supprimer toutes les artwork en cache? + Réinitialiser tous les réglages de l'addon Emby? + La réinitialisation de la base de données est terminée, Kodi va maintenant redémarrer pour appliquer les modifications. + diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml new file mode 100644 index 00000000..fa92956d --- /dev/null +++ b/resources/language/German/strings.xml @@ -0,0 +1,350 @@ + + + + Emby für Kodi + Primäre Serveradresse + Via HTTP abspielen anstatt SMB + Log Level + Gerätename + Erweitert + Benutzername + Portnummer + Anzahl der zuletzt hinzugefügten Musikalben: + Anzahl der zuletzt hinzugefügten Filme, die gezeigt werden: + Anzahl der zuletzt hinzugefügten Episoden, die gezeigt werden: + Aktualisieren + Löschen + Benutzername/Passwort falsch + Benutzername nicht gefunden + Löschen + Lösche von Server + Sortiere nach + Kein Filter + Action + Abenteuer + Animation + Krimi + Komödie + Dokumentation + Drama + Fantasy + Ausländisch + Geschichte + Horror + Musik + Musical + Mystery + Romanze + Science Fiction + Kurzfilm + Spannung + Thriller + Western + Genre-Filter + Löschen von Dateien bestätigen + + Als 'gesehen' markieren + Als 'ungesehen' markieren + Sortieren nach + Sortierreihenfolge absteigend + Sortierreihenfolge aufsteigend + + Fortsetzen + Fortsetzen bei + Am Anfang starten + Löschen von Medien nach dem Abspielen anbieten + + Für Episoden + + Für Filme + + Prozentanzeige für Fortsetzen + Episodennummer hinzufügen + Ladefortschritt anzeigen + Lade Inhalt + Lade Daten + Fertig + Warnung + + Fehler + Suche + Aktiviere erweiterte Bilder (z.B. CoverArt) + + Metadaten + Artwork + Videoqualität + + Direkte Wiedergabe + + Transkodierung + Serversuche erfolgreich + Server gefunden + Addresse: + + Zuletzt hinzugefügte Serien + + Begonnene Serien + + Alles an Musik + Kanäle + + Zuletzt hinzugefügte Filme + + Zuletzt hinzugefügte Episoden + + Zuletzt hinzugefügte Alben + Begonnene Filme + + Begonnene Episoden + + Nächste Episoden + + Favorisierte Filme + + Favorisierte Serien + + Favorisierte Episoden + Häufig gespielte Alben + Anstehende Serien + Sammlungen + Trailer + Musikvideos + Fotos + Ungesehene Filme + + Filmgenres + Studios + Filmdarsteller + Ungesehene Episoden + Seriengenres + Fernsehsender + Seriendarsteller + Wiedergabelisten + Ansichten festlegen + Wähle Benutzer + + Verbindung zum Server fehlgeschlagen + Songs + Alben + Album-Interpreten + Interpreten + Musik-Genres + Zuletzt hinzugefügte + Begonnene + Anstehende + Benutzerdefinierte Ansichten + Statistiken senden + Zufällige Filme + Zufällige Episoden + Zufallseintrag + + Empfohlene Medien + + Extras + + Synchronisiere Themen-Musik + Synchronisiere Extra-Fanart + Synchronisiere Film-BoxSets + Lokale Kodi-Datenbank zurücksetzen + + Aktiviere HTTPS + + Erzwinge Codec-Transkodierung + Aktiviere Server-Verbindungsmeldungen beim Starten + + Kürzliche hinzugefügte Heimvideos + + Kürzlich hinzugefügte Fotos + + Favorisierte Heim Videos + + Favorisierte Fotos + + Favorisierte Alben + Kürzlich hinzugefügte Musikvideos + + Begonnene Musikvideos + + Ungesehene Musikvideos + + + Aktiviert + Zurücksetzen + Filme + Sammlungen + Trailer + Serien + Staffeln + Episoden + Interpreten + Musikalben + Musikvideos + Musikstücke + Kanäle + + Emby Einstellungen + Zu Emby Favoriten hinzufügen + Entferne von Emby Favoriten + Setze eigenes Song-Rating + Emby Addon Einstellungen + Lösche Element vom Server + Dieses Element aktualisieren + Setze eigenes Song-Rating (0-5) + + Überprüfe Host-SSL-Zertifikat + Client-SSL-Zertifikat + Benutze alternative Adresse + Alternative Serveradresse + Benutze alternativen Gerätenamen + [COLOR yellow]Erneut versuchen[/COLOR] + Synchronisations Einstellungen + Zeige Fortschritt, wenn die Datensatzanzahl größer ist als + Synchronisiere leere Serien + Aktiviere Musik Bibliothek + Direktes streamen der Musikbibliothek + Wiedergabemodus + Erzwinge Artworkcaching + Limitiere Artworkcache-Threads (empfohlen für rpi) + Aktiviere Schnellstart (benötigt Server plugin) + Maximale zeitgleiche Abfrageanzahl der Elemente vom Server + Wiedergabe + Netzwerkanmeldung + Aktiviere Emby Kino-Modus + Frage nach Trailerwiedergabe + Überspringe Emby Löschanfrage für das Kontextmenü (Nutzung auf eigene Gefahr) + Rücksprung beim Fortsetzen (in Sekunden) + Erzwinge H.265-Transkodierung + Musik-Metadateneinstellungen (nicht kompatibel mit direktem Streamen) + Importiere Songrating direkt aus Datei + Konvertiere Songrating zu Emby-Rating + Erlaube Aktualisierung des Ratings in Musikdateien + Ignoriere Bonusmaterial bei nächsten Episoden + Permanentes Hinzufügen von Benutzern zu dieser Sitzung + Startverzögerung (in Sekunden) + Aktiviere Serverneustart-Meldung + Aktiviere Benachrichtigung bei neuen Inhalten + Dauer der Anzeige für Videobibliotheks-PopUp (in Sekunden) + Dauer der Anzeige für Musikbibliotheks-PopUp (in Sekunden) + Server-Nachrichten + Erstelle neue Geräte-ID + Synchronisieren bei deakiviertem Bildschirmschoner + Erzwinge Hi10P-Transkodierung + Deaktiviert + Login + Manueller Login + Emby Connect + Server + Nutzername oder E-Mail + Aktiviere Datenbank Lock Fix (verlangsamt Synchronisationsprozess) + Aktiviere Server Offline-Nachricht + + Mit Emby Connect anmelden + Passwort + Bitte sieh in unsere Nutzungsbedingungen. Die Nutzung jeglicher Emby Software stellt die Akzeptanz dieser Bedingungen dar. + Scanne mich + Anmelden + Abbrechen + Wähle Hauptserver + Nutzername oder Passwort können nicht leer sein + Verbindung zum ausgewählten Server fehlgeschlagen + Verbinden zu + + Server manuell hinzufügen + Bitte anmelden + Nutzername kann nicht leer sein + Mit Server verbinden + Hostrechner + Verbinden + Server oder Port können nicht leer sein + Emby Connect-Nutzer wechseln + + Willkommen + Fehler bei Verbindung + Server kann nicht erreicht werden + Server ist online + Elemente zur Playlist hinzugefügt + Elemente in Playlist eingereiht + Server startet neu + Zugang erlaubt + Nutzerpasswort eingeben: + Falscher Benutzername oder Passwort + Authentifizierung zu oft fehlgeschlagen + Direkte Wiedergabe der Datei nicht möglich + Direkte Wiedergabe 3 mal fehlgeschlagen. Aktiviere Wiedergabe über HTTP. + Wähle Audiostream + Wähle Untertitelstream + Datei von deinem Emby Server löschen? + Trailer abspielen? + Erfasse Filme von: + Erfasse Sammlungen + Erfasse Musikvideos von: + Erfasse Serien von: + Erfassung: + Für diese Version von 'Emby für Kodi' muss die Datenbank neu erstellt werden. Fortfahren? + 'Emby für Kodi' funktioniert womöglich nicht richtig, bis die Datenbank zurückgesetzt wurde. + Beende Datenbank-Synchronisationsprozess. Die aktuelle Kodi-Version wird nicht unterstützt. + abgeschlossen in: + Vergleiche Filme von: + Vergleiche Boxsets + Vergleiche Musikvideos von: + Vergleiche Serien von: + Vergleiche Episoden von: + Vergleiche: + Erstellung einer neuen Geräte-ID fehlgeschlagen. Schau in die Logs für weitere Informationen. + Einer neue Geräte-ID wurde erstellt. Kodi startet nun neu. + Mit dem folgendem Server fortfahren? + Achtung! Wenn du den 'Nativen Modus' auswählst, fehlen einige Emby Funktionalitäten, wie: Emby Kinomodus, Direct Stream/Transkodiereigenschaften und elterliche Zugangsplanung. + Addon (Standard) + Nativ (Direkte Pfade) + Netzwerkanmeldeinformationen hinzufügen, um Kodi Zugriff auf die Inhalte zu geben? Wichtig: Kodi muss neugestartet werden, um die Netzwerk -Anmeldeinformationen zu sehen. Die Daten können auch später hinzugefügt werden. + Deakiviere Emby Musikbibliothek? + Direct Stream für die Musikbibliothek aktivieren? Wählen Sie diese Option wenn auf die Bibliothek später außerhalb des eigenen Netzwerkes zugegriffen wird. + Datei(en) vom Emby-Server löschen? Die Datei(en) werden auch von der Festplatte gelöscht! + Der Caching-Prozess dauert etwas. Weitermachen? + Artworkcache Synchronisation + Vorhandenen Artworkcache zurücksetzen? + Aktualisiere Artworkcache: + Warte auf Beendigung aller Threads: + Kodi kann Datei nicht finden: + Sie müssen Ihre Netzwerk -Anmeldeinformationen in den Addon-Einstellungen bestätigen oder Ihre Pfadersetzungen innerhalb des Emby-Servers korrekt setzen (Emby Dashboard -> Bibliothek). Synchronisation beenden? + Hinzugefügt: + Wenn Sie sich zu oft falsch anmelden, blockiert der Emby Server möglicherwiese den Account. Trotzdem weiter? + Live-TV Kanäle (experimentell) + Live-TV Aufnahmen (experimentell) + Einstellungen + Füge Benutzer zur Sitzung hinzu + Aktualisiere Emby Abspiellisten/Videoknoten + Manuelle Synchronisation ausführen + Repariere lokale Datenbank (erzwinge Aktualisierung des gesamten Inhalts) + Lokale Datenbank zurücksetzen + Gesamtes Artwork cachen + Emby Theme-Medien (Video/Musik-Themen) mit Kodi synchronisieren + Hinzufügen/Entfernen von Benutzer zur Sitzung + Benutzer hinzufügen + Benutzer entfernen + Benutzer von Sitzung entfernen + Erfolg! + Von der Videositzung entfernt: + Zur Videositzung hinzugefügt: + Benutzer von Session entfernen nicht möglich. + Task erfolgreich ausgeführt + Task fehlgeschlagen + Direktes Streamen + Playbackmethode für Ihre Themes + Die Einstellungsdatei existiert nicht in TV Tunes. Ändern Sie eine Einstellung und starten den Task erneut. + Sind Sie sicher, dass Sie die lokale Kodi Datenbank zurücksetzen möchten? + Bearbeiten/Entfernen der Netzwerkanmeldeinformationen + Ändern + Entfernen + Entfernt: + Netzwerk-Nutzernamen eingeben + Netzwerk-Passwort eingeben + Netzwerkanmeldeinformationen hinzugefügt für: + Servername oder IP-Adresse, wie in der Emby Server Bibliothek angezeigt, eingeben. (Bsp Servername: \"\\\\SERVER-PC\\Pfad\\\" ist \"SERVER-PC\") + Servername oder IP-Adresse ändern + Servername oder IP-Adresse eingeben + Konnte die Datenbank nicht zurücksetzen. Nochmal versuchen. + Alle zwischengespeicherten Bilder entfernen? + Alle Emby Addon-Einstellungen zurücksetzen? + Zurücksetzen der Datenbank abgeschlossen, Kodi wird nun neustarten um die Änderungen anzuwenden. + diff --git a/resources/language/Portuguese/strings.xml b/resources/language/Portuguese/strings.xml new file mode 100644 index 00000000..fe9800f1 --- /dev/null +++ b/resources/language/Portuguese/strings.xml @@ -0,0 +1,350 @@ + + + + Emby for Kodi + Endereço do Servidor Primário + Reproduzir por HTTP ao invés de SMB + Nível do log + Nome do Dispositivo + Avançado + Nome do usuário + Número da Porta + Número de Álbuns de Música recentes a exibir: + Número de Filmes recentes a exibir: + Número de episódios de TV recentes a exibir: + Atualizar + Excluir + Nome de usuário/Senha incorretos + Nome de usuário não encontrado + Excluindo + Aguardando pelo servidor para excluir + Classificar por + Nenhum + Ação + Aventura + Animação + Crime + Comédia + Documentário + Drama + Fantasia + Estrangeiro + História + Terror + Música + Musical + Mistério + Romance + Ficção Científica + Curta Metragem + Suspense + Suspense + Western + Filtro do Gênero + Confirmar exclusão do arquivo + + Marcar como assistido + Marcar como não-assistido + Classificar por + Classificar em Ordem Descendente + Classificar em Ordem Ascendente + + Retomar + Retomar a partir de + Iniciar do começo + Disponibilizar exclusão depois da reprodução + + Para Episódios + + Para Filmes + + Adicionar a Porcentagem para Retomar + Adicionar o Número do Episódio + Exibir Progresso do Carregamento + Carregando Conteúdo + Recuperando Dados + Feito + Aviso + + Erro + Busca + Ativar Imagens Melhoradas (ex. Capa) + + Metadados + Artwork + Qualidade do Vídeo + + Reprodução Direta + + Transcodificação + Sucesso na Detecção do Servidor + Servidor Encontrado + Endereço: + + Séries Recentemente Adicionadas + + Séries em Reprodução + + Todas as Músicas + Canais + + Filmes Recentemente Adicionados + + Episódios Recentemente Adicionados + + Álbuns Recentemente Adicionados + Filmes em Reprodução + + Episódios em Reprodução + + Próximos Episódios + + Filmes Favoritos + + Séries Favoritas + + Episódios Favoritos + Álbuns Mais Reproduzidos + Séries a Estrear + BoxSets + Trailers + Vídeos de Música + Fotos + Filmes Não-Assistidos + + Gêneros do Filme + Estúdios do Filme + Atores do Filme + Episódios Não-Assistidos + Gêneros da Série + Redes de TV + Atores da Série + Listas de Reprodução + Definir Visualizações + Selecionar Usuário + + Não foi possível conectar ao servidor + Músicas + Álbuns + Artistas do Álbum + Artistas + Gêneros da Música + Mais Recentes + Em Reprodução + Próxima + Visualizações do Usuário + Métricas do Relatório + Filmes Aleatórios + Episódios Aleatórios + Itens Aleatórios + + Itens Recomendados + + Extras + + Sincronizar Música-Tema + Sincronizar Extra Fanart + Sincronizar Imagens de Coletâneas + Repor base de dados local do Kodi + + Ativar HTTPS + + Forçar Codecs de Transcodificação + Ativar mensagem de conexão do servidor ao iniciar + + Vídeos Caseiros adicionados recentemente + + Fotos adicionadas recentemente + + Vídeos Caseiros Favoritos + + Fotos Favoritas + + Álbuns Favoritos + Vídeos de Música adicionados recentemente + + Vídeos de Música em Reprodução + + Vídeos de Música não-assistidos + + + Ativo + Limpar Definições + Filmes + BoxSets + Trailers + Séries + Temporadas + Episódios + Artistas da Música + Álbuns de Música + Vídeos de Música + Trilhas de Música + Canais + + Opções do Emby + Adicionar aos favoritos do Emby + Remover dos Favoritos do Emby + Definir a avaliação personalizada da música + Ajustes do addon do Emby + Excluir o item do servidor + Atualizar este item + Definir a avaliação personalizada da música (0-5) + + Verificar o Certificado SSL do Servidor + Certificado SSL do cliente + Usar endereço alternativo + Endereço Alternativo do Servidor + Usar Nome alternativo do dispositivo + [COLOR yellow]Tentar Login Novamente[/COLOR] + Opções de Sincronização + Exibir progresso se a contagem de itens for maior que + Sincronizar Séries de TV vazias + Ativar Biblioteca de Música + Stream direto da biblioteca de música + Modo de Reprodução + Forçar o caching de artwork + Limitar as threads de cache de artwork (recomendado para rpi) + Ativar inicialização rápida (é necessário o plugin do servidor) + Máximo de itens para solicitar ao servidor de uma única vez + Reprodução + Credenciais de rede + Ativar cinema mode do Emby + Confirmar a reprodução de trailers + Ignorar a confirmação de exclusão no Emby para o menú de contexto (use sob seu próprio risco) + intervalo para voltar na função retomar (em segundos) + Foçar transcodificação H265 + Opções de metadados de música (não compatível com stream direto) + Importar avaliação da música diretamente dos arquivos + Converter a avaliação da música para a avaliação Emby. + Permitir que as avaliações nos arquivos de música sejam atualizadas + Ignorar especiais nos próximos episódios + Usuários permanentes para adicionar à sessão + Atraso na inicialização (em segundos) + Ativar mensagem de reinicialização do servidor + Ativar notificação de novo conteúdo + Duração do janela da biblioteca de vídeo (em segundos) + Duração da janela da biblioteca de música (em segundos) + Mensagens do servidor + Gerar um novo id do dispositivo + Sincronizar quando o protetor de tela está desativado + Forçar Transcodificação Hi10P + Desativado + Login + Login Manual + Emby Connect + Servidor + Nome do usuário ou email + Ativar a correção para base de dados bloqueada (deixará o processo de sincronização mais lento) + Ativar mensagens offline do servidor + + Entrar com o Emby Connect + Senha + Por favor leia os termos de uso. O uso de qualquer software Emby constitui a aceitação desses termos. + Rastrear-me + Entrar + Cancelar + Selecione o servidor principal + O nome do usuário ou senha não podem estar em branco + Não foi possível conectar ao servidor selecionado + Conectar a + + Adicionar servidor manualmente + Por favor, inicie a sessão + O nome do usuário não pode estar em branco + Conectar ao servidor + Servidor + Conectar + O servidor ou porta não podem estar em branco + Alterar o usuário do Emby Connect + + Bem vindo + Erro na conexão + Servidor não pode ser encontrado + Servidor está ativo + Itens adicionados à lista de reprodução + Itens enfileirados na lista de reprodução + Servidor está reiniciando + O acesso está ativo + Digite a senha do usuário: + Nome de usuário ou senha inválidos + Falha ao autenticar inúmeras vezes + Não é possível a reprodução direta + A reprodução direta falhou 3 vezes. Ative a reprodução por HTTP. + Escolha o stream de áudio + Escolha o stream de legendas + Excluir arquivo de seu Servidor Emby? + Reproduzir trailers? + Coletando filmes de: + Coletando boxsets + Coletando vídeos de música de: + Coletando séries de: + Coletando: + Foi detectado que a base de dados necessita ser recriada para esta versão do Emby for Kodi. Prosseguir? + O Emby for Kodi pode não funcionar corretamente até que a base de dados seja atualizada. + Cancelando o processo de sincronização da base de dados. A versão atual do Kodi não é suportada. + completa em: + Comparando filmes de: + Comparando coletâneas + Comparando vídeos clipes de: + Comparando séries de tv de: + Comparando episódios de: + Comparando: + Falha ao gerar um novo Id de dispositivo. Veja seus logs para mais informações. + Um novo Id de dispositivo foi gerado. Kodi irá reiniciar. + Prosseguir com o seguinte servidor? + Cuidado! Se escolher modo Nativo, certas funções do Emby serão perdidas, como: Emby cinema mode, opções de stream/transcodificação direta e agendamento de acesso pelos pais. + Addon (Padrão) + Nativo (Locais Diretos) + Adicionar credenciais de rede para permitir que o Kodi acesse seu conteúdo? Importante: Kodi precisará ser reiniciado para ver as credenciais. Elas também podem ser adicionadas mais tarde. + Desativar biblioteca de músicas do Emby? + Fazer stream direto da biblioteca de músicas? Selecione esta opção se a biblioteca de músicas for acessada remotamente. + Excluir arquivo(s) do Servidor Emby? Esta opção também excluirá o(s) arquivo(s) do disco! + Executar o processo de caching pode levar bastante tempo. Continuar assim mesmo? + Sincronização do cache de artwork + Limpar cache de artwork atual? + Atualizando o cache de artwork: + Aguardando que todos os processos terminem: + Kodi não pode localizar o arquivo: + Você precisa verificar suas credenciais de rede nos ajustes de add-on ou usar substituição de locais no Emby para formatar seu local corretamente (Painel Emby > biblioteca). Parar de sincronizar? + Adicionado: + Se falhar para entrar diversas vezes, o servidor Emby pode bloquear sua conta, Deseja prosseguir? + Canais de TV ao Vivo (experimental) + Gravações de TV ao Vivo (experimental) + Ajustes + Adicionar usuário à sessão + Atualizar nós de listas de reprodução/Vídeo + Executar sincronização manual + Reparar base de dados local (forçar atualização para todo o conteúdo) + Executar reset da base de dados local + Colocar toda artwork no cache + Sincronizar Mídia de Tema do Emby para o Kodi + Adicionar/Remover usuário da sessão + Adicionar usuário + Remover usuário + Remover usuário da sessão + Sucesso! + Removido da seguinte sessão: + Adicionado à seguinte sessão: + Não foi possível adicionar/remover usuário da sessão. + Sucesso na tarefa + Falha na tarefa + Stream Direto + Método de reprodução para seus temas + O arquivo de ajustes não existe na TV Tunes. Altere o ajuste e execute a tarefa novamente. + Deseja realmente resetar a base de dados local do Kodi? + Modificar/Remover as credenciais de rede + Modificar + Remover + Removido: + Digite o nome de usuário da rede + Digite a senha da rede + Credenciais de rede adicionadas para: + Digite o nome do servidor ou endereço IP como indicado nos locais de sua biblioteca do emby. Por exemplo, o nome do servidor \\\\SERVIDOR-PC\\local\\ é \"SERVIDOR-PC\" + Modificar o nome do servidor ou endereço IP + Digite o nome do servidor ou endereço IP + Não é possível resetar a base de dados. Tente novamente. + Remover toda artwork do cache? + Resetar todos os ajustes do add-on Emby? + O reset da base de dados foi concluída, Kodi irá reiniciar para aplicar as alterações. + diff --git a/resources/language/Russian/strings.xml b/resources/language/Russian/strings.xml index 146d8cc7..92ac4d55 100644 --- a/resources/language/Russian/strings.xml +++ b/resources/language/Russian/strings.xml @@ -3,18 +3,12 @@ Emby для Kodi Основной адрес сервера - Воспроизводить по HTTP вместо SMB - Уровень журналирования - Название устройства - Расширенное Имя пользователя - Номер порта - Число последних музыкальных альбомов для просмотра: Число последних фильмов для просмотра: Число последних ТВ-эпизодов для просмотра: @@ -204,7 +198,7 @@ Использовать альтернативное имя устройства [COLOR yellow]Войти повторно[/COLOR] Параметры синхронизации - Показать прогресс синхронизации + Показать прогресс если число элементов более чем Синхронизировать пустые ТВ-программы Включить медиатеку музыки Прямая трансляция медиатеки музыки @@ -215,7 +209,7 @@ Максимальное число элементов для запроса с сервера за раз Воспроизведение Сетевые учётные данные - Включить режим кинотеатра Emby + Включить режим кинозала Emby Запрашивать для воспроизведения трейлеров Пропускать подтверждение удаления в Emby для контекстного меню (используйте на свой страх и риск) Переход назад при возобновлении, с @@ -236,6 +230,33 @@ Синхронизировать, когда отключен хранитель экрана Принудительно перекодировать Hi10P Выключено + Вход + Войти вручную + Emby Connect + Сервер + Имя пользователя или Э-почта + Включить исправление блокировки базы данных (будет тормозиться процесс синхронизации) + Включить сообщение об отключении сервера + + Вход через Emby Connect + Пароль + Ознакомьтесь с нашими Условиями пользования. Использование любого ПО Emby означает принятие этих условий. + Сканировать меня + Войти + Отменить + Выбор головного сервера + Имя пользователя или пароль не могут быть пустыми + Не удалось подсоединиться к выбранному серверу + Соединение с + + Добавить сервер вручную + Выполните вход + Имя пользователя не может быть пустым + Соединение с сервером + Хост + Подсоединиться + Сервр или порт не могут быть пустыми + Сменить пользователя Emby Connect Начало работы Ошибка соединения @@ -248,7 +269,7 @@ Ввести пароль для пользователя: Недопустимое имя пользователя или пароль. Не удалось проверить подлинность слишком много раз - Прямое воспроизведение невозможно + Прямое воспроизведение файла невозможно Прямое воспроизведение не удалось 3 раза. Включено воспроизведение с HTTP. Выбрать поток аудио Выбрать поток субтитров @@ -272,7 +293,7 @@ Генерирование Id нового устройства не удалось. Просмотрите ваши журналы для более подробной информации. Было сгенерирован Id нового устройства. Kodi теперь перезапустится. Приступить к следующему серверу? - Осторожно! Если вы выбрали режим Собственный, некоторые функции Emby будут отсутствовать, например, режим кинотеатра Emby, прямой трансляция / варианты перекодировки и расписание доступа. + Осторожно! Если вы выбрали режим Собственный, некоторые функции Emby будут отсутствовать, например, режим кинозала Emby, прямой трансляция / варианты перекодировки и расписание доступа. Надстройка (по умолчанию) Собственный (непосредственные пути) Добавить сетевые учётные данные, чтобы разрешить доступ для Kodi к вашему содержанию? Важно: Чтобы увидеть учётные данные, необходимо перезапустить Kodi. Также они могут быть добавлены позднее. diff --git a/resources/language/Swedish/strings.xml b/resources/language/Swedish/strings.xml index 7530f729..8c6c5f88 100644 --- a/resources/language/Swedish/strings.xml +++ b/resources/language/Swedish/strings.xml @@ -273,7 +273,7 @@ Kunde inte generera ett nytt enhetsID. Se i loggarna för mer information. Ett nytt enhetsID har genererats. Kodi kommer nu starta om. Fortsätt med följande server? - OBS! Om du väljer \'Native\'-läget så tappar du vissa funktioner i Emby, som; Emby bioläge, direktströmning/omkodning och schema för föräldralås. + OBS! Om du väljer 'Native'-läget så tappar du vissa funktioner i Emby, som; Emby bioläge, direktströmning/omkodning och schema för föräldralås. Tillägg (Standard) Native (Direkta Sökvägar) Lägg till nätverksuppgifter för att ge Kodi åtkomst till ditt innehåll? Viktigt: Kodi kommer behöva startas om för att se uppgifterna. Dom kan också läggas till vid ett senare tillfälle. diff --git a/resources/lib/api.py b/resources/lib/api.py index 0d7332b5..8eb430f2 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -14,45 +14,32 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## -class API(): +class API(object): def __init__(self, item): - # item is the api response self.item = item - def getUserData(self): + def get_userdata(self): # Default favorite = False likes = None playcount = None played = False - lastPlayedDate = None + last_played = None resume = 0 - userrating = 0 try: userdata = self.item['UserData'] - except KeyError: # No userdata found. pass - else: favorite = userdata['IsFavorite'] likes = userdata.get('Likes') - # Userrating is based on likes and favourite - if favorite: - userrating = 5 - elif likes: - userrating = 3 - elif likes == False: - userrating = 0 - else: - userrating = 1 - lastPlayedDate = userdata.get('LastPlayedDate') - if lastPlayedDate: - lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ") + last_played = userdata.get('LastPlayedDate') + if last_played: + last_played = last_played.split('.')[0].replace('T', " ") if userdata['Played']: # Playcount is tied to the watch status @@ -61,12 +48,12 @@ class API(): if playcount == 0: playcount = 1 - if lastPlayedDate is None: - lastPlayedDate = self.getDateCreated() + if last_played is None: + last_played = self.get_date_created() - playbackPosition = userdata.get('PlaybackPositionTicks') - if playbackPosition: - resume = playbackPosition / 10000000.0 + playback_position = userdata.get('PlaybackPositionTicks') + if playback_position: + resume = playback_position / 10000000.0 return { @@ -74,12 +61,11 @@ class API(): 'Likes': likes, 'PlayCount': playcount, 'Played': played, - 'LastPlayedDate': lastPlayedDate, - 'Resume': resume, - 'UserRating': userrating + 'LastPlayedDate': last_played, + 'Resume': resume } - def getPeople(self): + def get_people(self): # Process People director = [] writer = [] @@ -87,21 +73,19 @@ class API(): try: people = self.item['People'] - except KeyError: pass - else: for person in people: - type = person['Type'] + type_ = person['Type'] name = person['Name'] - if "Director" in type: + if type_ == 'Director': director.append(name) - elif "Actor" in type: + elif type_ == 'Actor': cast.append(name) - elif type in ("Writing", "Writer"): + elif type_ in ('Writing', 'Writer'): writer.append(name) return { @@ -111,101 +95,115 @@ class API(): 'Cast': cast } - def getMediaStreams(self): - videotracks = [] - audiotracks = [] - subtitlelanguages = [] + def get_media_streams(self): + + video_tracks = [] + audio_tracks = [] + subtitle_languages = [] try: media_streams = self.item['MediaSources'][0]['MediaStreams'] except KeyError: - if not self.item.get("MediaStreams"): return None + if not self.item.get("MediaStreams"): + return None media_streams = self.item['MediaStreams'] for media_stream in media_streams: # Sort through Video, Audio, Subtitle stream_type = media_stream['Type'] - codec = media_stream.get('Codec', "").lower() - profile = media_stream.get('Profile', "").lower() if stream_type == "Video": - # Height, Width, Codec, AspectRatio, AspectFloat, 3D - track = { - - 'codec': codec, - 'height': media_stream.get('Height'), - 'width': media_stream.get('Width'), - 'video3DFormat': self.item.get('Video3DFormat'), - 'aspect': 1.85 - } - - try: - container = self.item['MediaSources'][0]['Container'].lower() - except: - container = "" - - # Sort codec vs container/profile - if "msmpeg4" in codec: - track['codec'] = "divx" - elif "mpeg4" in codec: - if "simple profile" in profile or not profile: - track['codec'] = "xvid" - elif "h264" in codec: - if container in ("mp4", "mov", "m4v"): - track['codec'] = "avc1" - - # Aspect ratio - if self.item.get('AspectRatio'): - # Metadata AR - aspect = self.item['AspectRatio'] - else: # File AR - aspect = media_stream.get('AspectRatio', "0") - - try: - aspectwidth, aspectheight = aspect.split(':') - track['aspect'] = round(float(aspectwidth) / float(aspectheight), 6) - - except (ValueError, ZeroDivisionError): - width = track.get('width') - height = track.get('height') - - if width and height: - track['aspect'] = round(float(width / height), 6) - else: - track['aspect'] = 1.85 - - if self.item.get("RunTimeTicks"): - track['duration'] = self.item.get("RunTimeTicks") / 10000000.0 - - videotracks.append(track) + self._video_stream(video_tracks, media_stream) elif stream_type == "Audio": - # Codec, Channels, language - track = { - - 'codec': codec, - 'channels': media_stream.get('Channels'), - 'language': media_stream.get('Language') - } - - if "dca" in codec and "dts-hd ma" in profile: - track['codec'] = "dtshd_ma" - - audiotracks.append(track) + self._audio_stream(audio_tracks, media_stream) elif stream_type == "Subtitle": - # Language - subtitlelanguages.append(media_stream.get('Language', "Unknown")) + subtitle_languages.append(media_stream.get('Language', "Unknown")) return { - 'video': videotracks, - 'audio': audiotracks, - 'subtitle': subtitlelanguages + 'video': video_tracks, + 'audio': audio_tracks, + 'subtitle': subtitle_languages } - def getRuntime(self): + def _video_stream(self, video_tracks, stream): + + codec = stream.get('Codec', "").lower() + profile = stream.get('Profile', "").lower() + + # Height, Width, Codec, AspectRatio, AspectFloat, 3D + track = { + + 'codec': codec, + 'height': stream.get('Height'), + 'width': stream.get('Width'), + 'video3DFormat': self.item.get('Video3DFormat'), + 'aspect': 1.85 + } + + try: + container = self.item['MediaSources'][0]['Container'].lower() + except Exception: + container = "" + + # Sort codec vs container/profile + if "msmpeg4" in codec: + track['codec'] = "divx" + elif "mpeg4" in codec: + if "simple profile" in profile or not profile: + track['codec'] = "xvid" + elif "h264" in codec: + if container in ("mp4", "mov", "m4v"): + track['codec'] = "avc1" + + # Aspect ratio + if 'AspectRatio' in self.item: + # Metadata AR + aspect = self.item['AspectRatio'] + else: # File AR + aspect = stream.get('AspectRatio', "0") + + try: + aspect_width, aspect_height = aspect.split(':') + track['aspect'] = round(float(aspect_width) / float(aspect_height), 6) + + except (ValueError, ZeroDivisionError): + + width = track.get('width') + height = track.get('height') + + if width and height: + track['aspect'] = round(float(width / height), 6) + else: + track['aspect'] = 1.85 + + if 'RunTimeTicks' in self.item: + track['duration'] = self.get_runtime() + + video_tracks.append(track) + + def _audio_stream(self, audio_tracks, stream): + + codec = stream.get('Codec', "").lower() + profile = stream.get('Profile', "").lower() + # Codec, Channels, language + track = { + + 'codec': codec, + 'channels': stream.get('Channels'), + 'language': stream.get('Language') + } + + if "dca" in codec and "dts-hd ma" in profile: + track['codec'] = "dtshd_ma" + + audio_tracks.append(track) + + def get_runtime(self): + try: runtime = self.item['RunTimeTicks'] / 10000000.0 @@ -214,7 +212,8 @@ class API(): return runtime - def adjustResume(self, resume_seconds): + @classmethod + def adjust_resume(cls, resume_seconds): resume = 0 if resume_seconds: @@ -226,24 +225,23 @@ class API(): return resume - def getStudios(self): + def get_studios(self): # Process Studios studios = [] - try: studio = self.item['SeriesStudio'] - studios.append(self.verifyStudio(studio)) + studios.append(self.verify_studio(studio)) except KeyError: - studioList = self.item['Studios'] - for studio in studioList: + for studio in self.item['Studios']: name = studio['Name'] - studios.append(self.verifyStudio(name)) + studios.append(self.verify_studio(name)) return studios - def verifyStudio(self, studioName): + @classmethod + def verify_studio(cls, studio_name): # Convert studio for Kodi to properly detect them studios = { @@ -254,9 +252,9 @@ class API(): 'wgn america': "WGN" } - return studios.get(studioName.lower(), studioName) + return studios.get(studio_name.lower(), studio_name) - def getChecksum(self): + def get_checksum(self): # Use the etags checksum and userdata userdata = self.item['UserData'] @@ -265,7 +263,7 @@ class API(): self.item['Etag'], userdata['Played'], userdata['IsFavorite'], - userdata.get('Likes',''), + userdata.get('Likes', ""), userdata['PlaybackPositionTicks'], userdata.get('UnplayedItemCount', ""), userdata.get('LastPlayedDate', "") @@ -273,7 +271,7 @@ class API(): return checksum - def getGenres(self): + def get_genres(self): all_genres = "" genres = self.item.get('Genres', self.item.get('SeriesGenres')) @@ -282,17 +280,17 @@ class API(): return all_genres - def getDateCreated(self): + def get_date_created(self): try: - dateadded = self.item['DateCreated'] - dateadded = dateadded.split('.')[0].replace('T', " ") + date_added = self.item['DateCreated'] + date_added = date_added.split('.')[0].replace('T', " ") except KeyError: - dateadded = None + date_added = None - return dateadded + return date_added - def getPremiereDate(self): + def get_premiere_date(self): try: premiere = self.item['PremiereDate'] @@ -302,7 +300,7 @@ class API(): return premiere - def getOverview(self): + def get_overview(self): try: overview = self.item['Overview'] @@ -314,7 +312,7 @@ class API(): return overview - def getTagline(self): + def get_tagline(self): try: tagline = self.item['Taglines'][0] @@ -323,16 +321,16 @@ class API(): return tagline - def getProvider(self, providername): + def get_provider(self, name): try: - provider = self.item['ProviderIds'][providername] + provider = self.item['ProviderIds'][name] except KeyError: provider = None return provider - def getMpaa(self): + def get_mpaa(self): # Convert more complex cases mpaa = self.item.get('OfficialRating', "") @@ -340,18 +338,21 @@ class API(): # Kodi seems to not like NR, but will accept Not Rated mpaa = "Not Rated" + if "FSK-" in mpaa: + mpaa = mpaa.replace("-", " ") + return mpaa - def getCountry(self): + def get_country(self): try: country = self.item['ProductionLocations'][0] - except IndexError: + except (IndexError, KeyError): country = None return country - def getFilePath(self): + def get_file_path(self): try: filepath = self.item['Path'] diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4b0124f7..2b828a2d 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -2,9 +2,7 @@ ################################################################################################# -import json import logging -import requests import os import urllib from sqlite3 import OperationalError @@ -12,9 +10,10 @@ from sqlite3 import OperationalError import xbmc import xbmcgui import xbmcvfs +import requests import image_cache_thread -from utils import window, settings, language as lang, kodiSQL +from utils import window, settings, dialog, language as lang, kodiSQL, JSONRPC ################################################################################################## @@ -23,58 +22,54 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## -class Artwork(): +class Artwork(object): xbmc_host = 'localhost' xbmc_port = None xbmc_username = None xbmc_password = None - imageCacheThreads = [] - imageCacheLimitThreads = 0 + image_cache_threads = [] + image_cache_limit = 0 def __init__(self): - self.enableTextureCache = settings('enableTextureCache') == "true" - self.imageCacheLimitThreads = int(settings('imageCacheLimit')) - self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5) - log.info("Using Image Cache Thread Count: %s" % self.imageCacheLimitThreads) + self.enable_texture_cache = settings('enableTextureCache') == "true" + self.image_cache_limit = int(settings('imageCacheLimit')) * 5 + log.debug("image cache thread count: %s", self.image_cache_limit) - if not self.xbmc_port and self.enableTextureCache: - self.setKodiWebServerDetails() + if not self.xbmc_port and self.enable_texture_cache: + self._set_webserver_details() - self.userId = window('emby_currUser') - self.server = window('emby_server%s' % self.userId) + self.user_id = window('emby_currUser') + self.server = window('emby_server%s' % self.user_id) - def double_urlencode(self, text): + def _double_urlencode(self, text): + text = self.single_urlencode(text) text = self.single_urlencode(text) return text - def single_urlencode(self, text): + @classmethod + def single_urlencode(cls, text): # urlencode needs a utf- string - text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) + text = urllib.urlencode({'blahblahblah': text.encode('utf-8')}) text = text[13:] - return text.decode("utf-8") #return the result again as unicode + return text.decode('utf-8') #return the result again as unicode - def setKodiWebServerDetails(self): + def _set_webserver_details(self): # Get the Kodi webserver details - used to set the texture cache + get_setting_value = JSONRPC('Settings.GetSettingValue') + web_query = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserver" - } + "setting": "services.webserver" } - result = xbmc.executeJSONRPC(json.dumps(web_query)) - result = json.loads(result) + result = get_setting_value.execute(web_query) try: xbmc_webserver_enabled = result['result']['value'] except (KeyError, TypeError): @@ -82,95 +77,61 @@ class Artwork(): if not xbmc_webserver_enabled: # Enable the webserver, it is disabled + set_setting_value = JSONRPC('Settings.SetSettingValue') + web_port = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - - "setting": "services.webserverport", - "value": 8080 - } + "setting": "services.webserverport", + "value": 8080 } - result = xbmc.executeJSONRPC(json.dumps(web_port)) + set_setting_value.execute(web_port) self.xbmc_port = 8080 web_user = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.SetSettingValue", - "params": { - - "setting": "services.webserver", - "value": True - } + "setting": "services.webserver", + "value": True } - result = xbmc.executeJSONRPC(json.dumps(web_user)) + set_setting_value.execute(web_user) self.xbmc_username = "kodi" - # Webserver already enabled web_port = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverport" - } + "setting": "services.webserverport" } - result = xbmc.executeJSONRPC(json.dumps(web_port)) - result = json.loads(result) + result = get_setting_value.execute(web_port) try: self.xbmc_port = result['result']['value'] - except TypeError: + except (TypeError, KeyError): pass web_user = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverusername" - } + "setting": "services.webserverusername" } - result = xbmc.executeJSONRPC(json.dumps(web_user)) - result = json.loads(result) + result = get_setting_value.execute(web_user) try: self.xbmc_username = result['result']['value'] - except TypeError: + except (TypeError, KeyError): pass web_pass = { - "jsonrpc": "2.0", - "id": 1, - "method": "Settings.GetSettingValue", - "params": { - - "setting": "services.webserverpassword" - } + "setting": "services.webserverpassword" } - result = xbmc.executeJSONRPC(json.dumps(web_pass)) - result = json.loads(result) + result = get_setting_value.execute(web_pass) try: self.xbmc_password = result['result']['value'] - except TypeError: + except (TypeError, KeyError): pass - def fullTextureCacheSync(self): + def texture_cache_sync(self): # This method will sync all Kodi artwork to textures13.db # and cache them locally. This takes diskspace! - dialog = xbmcgui.Dialog() - - if not dialog.yesno( - heading=lang(29999), - line1=lang(33042)): + if not dialog(type_="yesno", + heading="{emby}", + line1=lang(33042)): return log.info("Doing Image Cache Sync") @@ -179,135 +140,147 @@ class Artwork(): pdialog.create(lang(29999), lang(33043)) # ask to rest all existing or not - if dialog.yesno(lang(29999), lang(33044)): - log.info("Resetting all cache data first.") - - # Remove all existing textures first - path = xbmc.translatePath('special://thumbnails/').decode('utf-8') - if xbmcvfs.exists(path): - allDirs, allFiles = xbmcvfs.listdir(path) - for dir in allDirs: - allDirs, allFiles = xbmcvfs.listdir(path+dir) - for file in allFiles: - if os.path.supports_unicode_filenames: - path = os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')) - xbmcvfs.delete(path) - else: - xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file)) - - # remove all existing data from texture DB - connection = kodiSQL('texture') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tableName = row[0] - if tableName != "version": - cursor.execute("DELETE FROM " + tableName) - connection.commit() - cursor.close() + if dialog(type_="yesno", heading="{emby}", line1=lang(33044)): + log.info("Resetting all cache data first") + self.delete_cache() # Cache all entries in video DB - connection = kodiSQL('video') - cursor = connection.cursor() + self._cache_all_video_entries(pdialog) + # Cache all entries in music DB + self._cache_all_music_entries(pdialog) + + pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) + log.info("Waiting for all threads to exit") + + while len(self.image_cache_threads): + for thread in self.image_cache_threads: + if thread.is_finished: + self.image_cache_threads.remove(thread) + pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) + log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads)) + xbmc.sleep(500) + + pdialog.close() + + def _cache_all_video_entries(self, pdialog): + + conn = kodiSQL('video') + cursor = conn.cursor() cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors result = cursor.fetchall() total = len(result) - log.info("Image cache sync about to process %s images" % total) + log.info("Image cache sync about to process %s images", total) cursor.close() count = 0 for url in result: - + if pdialog.iscanceled(): break percentage = int((float(count) / float(total))*100) - message = "%s of %s (%s)" % (count, total, self.imageCacheThreads) + message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cacheTexture(url[0]) + self.cache_texture(url[0]) count += 1 - - # Cache all entries in music DB - connection = kodiSQL('music') - cursor = connection.cursor() + + def _cache_all_music_entries(self, pdialog): + + conn = kodiSQL('music') + cursor = conn.cursor() cursor.execute("SELECT url FROM art") result = cursor.fetchall() total = len(result) - log.info("Image cache sync about to process %s images" % total) + log.info("Image cache sync about to process %s images", total) cursor.close() count = 0 for url in result: - + if pdialog.iscanceled(): break percentage = int((float(count) / float(total))*100) message = "%s of %s" % (count, total) pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cacheTexture(url[0]) + self.cache_texture(url[0]) count += 1 - - pdialog.update(100, "%s %s" % (lang(33046), len(self.imageCacheThreads))) - log.info("Waiting for all threads to exit") - - while len(self.imageCacheThreads): - for thread in self.imageCacheThreads: - if thread.isFinished: - self.imageCacheThreads.remove(thread) - pdialog.update(100, "%s %s" % (lang(33046), len(self.imageCacheThreads))) - log.info("Waiting for all threads to exit: %s" % len(self.imageCacheThreads)) - xbmc.sleep(500) - pdialog.close() + @classmethod + def delete_cache(cls): + # Remove all existing textures first + path = xbmc.translatePath('special://thumbnails/').decode('utf-8') + if xbmcvfs.exists(path): + dirs, ignore_files = xbmcvfs.listdir(path) + for directory in dirs: + ignore_dirs, files = xbmcvfs.listdir(path + directory) + for file_ in files: - def addWorkerImageCacheThread(self, url): + if os.path.supports_unicode_filenames: + filename = os.path.join(path + directory.decode('utf-8'), + file_.decode('utf-8')) + else: + filename = os.path.join(path.encode('utf-8') + directory, file_) + + xbmcvfs.delete(filename) + log.debug("deleted: %s", filename) + + # remove all existing data from texture DB + conn = kodiSQL('texture') + cursor = conn.cursor() + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + table_name = row[0] + if table_name != "version": + cursor.execute("DELETE FROM " + table_name) + conn.commit() + cursor.close() + + def _add_worker_image_thread(self, url): while True: # removed finished - for thread in self.imageCacheThreads: - if thread.isFinished: - self.imageCacheThreads.remove(thread) + for thread in self.image_cache_threads: + if thread.is_finished: + self.image_cache_threads.remove(thread) # add a new thread or wait and retry if we hit our limit - if len(self.imageCacheThreads) < self.imageCacheLimitThreads: - newThread = image_cache_thread.image_cache_thread() - newThread.setUrl(self.double_urlencode(url)) - newThread.setHost(self.xbmc_host, self.xbmc_port) - newThread.setAuth(self.xbmc_username, self.xbmc_password) - newThread.start() - self.imageCacheThreads.append(newThread) + if len(self.image_cache_threads) < self.image_cache_limit: + + new_thread = image_cache_thread.ImageCacheThread() + new_thread.set_url(self._double_urlencode(url)) + new_thread.set_host(self.xbmc_host, self.xbmc_port) + new_thread.set_auth(self.xbmc_username, self.xbmc_password) + + new_thread.start() + self.image_cache_threads.append(new_thread) return else: - log.info("Waiting for empty queue spot: %s" % len(self.imageCacheThreads)) + log.info("Waiting for empty queue spot: %s", len(self.image_cache_threads)) xbmc.sleep(50) - def cacheTexture(self, url): + def cache_texture(self, url): # Cache a single image url to the texture cache - if url and self.enableTextureCache: - log.debug("Processing: %s" % url) + if url and self.enable_texture_cache: + log.debug("Processing: %s", url) - if not self.imageCacheLimitThreads: - # Add image to texture cache by simply calling it at the http endpoint - - url = self.double_urlencode(url) - try: # Extreme short timeouts so we will have a exception. - response = requests.head( - url=("http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, url)), - auth=(self.xbmc_username, self.xbmc_password), - timeout=(0.01, 0.01)) - # We don't need the result - except: pass + if not self.image_cache_limit: + url = self._double_urlencode(url) + try: # Add image to texture cache by simply calling it at the http endpoint + requests.head(url=("http://%s:%s/image/image://%s" + % (self.xbmc_host, self.xbmc_port, url)), + auth=(self.xbmc_username, self.xbmc_password), + timeout=(0.01, 0.01)) + except Exception: # We don't need the result + pass else: - self.addWorkerImageCacheThread(url) + self._add_worker_image_thread(url) - - def addArtwork(self, artwork, kodiId, mediaType, cursor): + def add_artwork(self, artwork, kodi_id, media_type, cursor): # Kodi conversion table - kodiart = { + kodi_artwork = { 'Primary': ["thumb", "poster"], 'Banner': "banner", @@ -318,15 +291,14 @@ class Artwork(): 'Backdrop': "fanart", 'BoxRear': "poster" } - # Artwork is a dictionary - for art in artwork: + for artwork_type in artwork: - if art == "Backdrop": + if artwork_type == 'Backdrop': # Backdrop entry is a list # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...) - backdrops = artwork[art] - backdropsNumber = len(backdrops) + backdrops = artwork[artwork_type] + backdrops_number = len(backdrops) query = ' '.join(( @@ -336,10 +308,10 @@ class Artwork(): "AND media_type = ?", "AND type LIKE ?" )) - cursor.execute(query, (kodiId, mediaType, "fanart%",)) + cursor.execute(query, (kodi_id, media_type, "fanart%",)) rows = cursor.fetchall() - if len(rows) > backdropsNumber: + if len(rows) > backdrops_number: # More backdrops in database. Delete extra fanart. query = ' '.join(( @@ -348,47 +320,40 @@ class Artwork(): "AND media_type = ?", "AND type LIKE ?" )) - cursor.execute(query, (kodiId, mediaType, "fanart_",)) + cursor.execute(query, (kodi_id, media_type, "fanart_",)) # Process backdrops and extra fanart - index = "" - for backdrop in backdrops: - self.addOrUpdateArt( - imageUrl=backdrop, - kodiId=kodiId, - mediaType=mediaType, - imageType="%s%s" % ("fanart", index), - cursor=cursor) + for index, backdrop in enumerate(backdrops): - if backdropsNumber > 1: - try: # Will only fail on the first try, str to int. - index += 1 - except TypeError: - index = 1 + self.add_update_art(image_url=backdrop, + kodi_id=kodi_id, + media_type=media_type, + image_type=("fanart" if not index else "%s%s" + % ("fanart", index)), + cursor=cursor) - elif art == "Primary": + elif artwork_type == 'Primary': # Primary art is processed as thumb and poster for Kodi. - for artType in kodiart[art]: - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=artType, - cursor=cursor) + for art_type in kodi_artwork[artwork_type]: + self.add_update_art(image_url=artwork[artwork_type], + kodi_id=kodi_id, + media_type=media_type, + image_type=art_type, + cursor=cursor) - elif kodiart.get(art): + elif artwork_type in kodi_artwork: # Process the rest artwork type that Kodi can use - self.addOrUpdateArt( - imageUrl=artwork[art], - kodiId=kodiId, - mediaType=mediaType, - imageType=kodiart[art], - cursor=cursor) + self.add_update_art(image_url=artwork[artwork_type], + kodi_id=kodi_id, + media_type=media_type, + image_type=kodi_artwork[artwork_type], + cursor=cursor) - def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): + def add_update_art(self, image_url, kodi_id, media_type, image_type, cursor): # Possible that the imageurl is an empty string - if imageUrl: - cacheimage = False + if image_url: + + cache_image = False query = ' '.join(( @@ -398,13 +363,13 @@ class Artwork(): "AND media_type = ?", "AND type = ?" )) - cursor.execute(query, (kodiId, mediaType, imageType,)) + cursor.execute(query, (kodi_id, media_type, image_type,)) try: # Update the artwork url = cursor.fetchone()[0] except TypeError: # Add the artwork - cacheimage = True - log.debug("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl)) + cache_image = True + log.debug("Adding Art Link for kodiId: %s (%s)", kodi_id, image_url) query = ( ''' @@ -413,20 +378,21 @@ class Artwork(): VALUES (?, ?, ?, ?) ''' ) - cursor.execute(query, (kodiId, mediaType, imageType, imageUrl)) + cursor.execute(query, (kodi_id, media_type, image_type, image_url)) else: # Only cache artwork if it changed - if url != imageUrl: - cacheimage = True + if url != image_url: + + cache_image = True # Only for the main backdrop, poster if (window('emby_initialScan') != "true" and - imageType in ("fanart", "poster")): + image_type in ("fanart", "poster")): # Delete current entry before updating with the new one - self.deleteCachedArtwork(url) + self.delete_cached_artwork(url) - log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)" - % (imageType, kodiId, url, imageUrl)) + log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)", + image_type, kodi_id, url, image_url) query = ' '.join(( @@ -436,13 +402,13 @@ class Artwork(): "AND media_type = ?", "AND type = ?" )) - cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) + cursor.execute(query, (image_url, kodi_id, media_type, image_type)) # Cache fanart and poster in Kodi texture cache - if cacheimage and imageType in ("fanart", "poster"): - self.cacheTexture(imageUrl) + if cache_image and image_type in ("fanart", "poster"): + self.cache_texture(image_url) - def deleteArtwork(self, kodiId, mediaType, cursor): + def delete_artwork(self, kodi_id, media_type, cursor): query = ' '.join(( @@ -451,85 +417,83 @@ class Artwork(): "WHERE media_id = ?", "AND media_type = ?" )) - cursor.execute(query, (kodiId, mediaType,)) + cursor.execute(query, (kodi_id, media_type,)) rows = cursor.fetchall() for row in rows: url = row[0] - imageType = row[1] - if imageType in ("poster", "fanart"): - self.deleteCachedArtwork(url) + image_type = row[1] + if image_type in ("poster", "fanart"): + self.delete_cached_artwork(url) - def deleteCachedArtwork(self, url): + @classmethod + def delete_cached_artwork(cls, url): # Only necessary to remove and apply a new backdrop or poster - connection = kodiSQL('texture') - cursor = connection.cursor() + conn = kodiSQL('texture') + cursor = conn.cursor() try: cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) - cachedurl = cursor.fetchone()[0] + cached_url = cursor.fetchone()[0] except TypeError: - log.info("Could not find cached url.") + log.info("Could not find cached url") except OperationalError: log.info("Database is locked. Skip deletion process.") else: # Delete thumbnail as well as the entry - thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8') - log.info("Deleting cached thumbnail: %s" % thumbnails) + thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8') + log.info("Deleting cached thumbnail: %s", thumbnails) xbmcvfs.delete(thumbnails) try: cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) - connection.commit() + conn.commit() except OperationalError: log.debug("Issue deleting url from cache. Skipping.") finally: cursor.close() - def getPeopleArtwork(self, people): + def get_people_artwork(self, people): # append imageurl if existing for person in people: - personId = person['Id'] - tag = person.get('PrimaryImageTag') - image = "" - if tag: + person_id = person['Id'] + + if "PrimaryImageTag" in person: image = ( "%s/emby/Items/%s/Images/Primary?" "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" - % (self.server, personId, tag)) + % (self.server, person_id, person['PrimaryImageTag'])) person['imageurl'] = image return people - def getUserArtwork(self, itemId, itemType): + def get_user_artwork(self, item_id, item_type): # Load user information set by UserClient - image = ("%s/emby/Users/%s/Images/%s?Format=original" - % (self.server, itemId, itemType)) - return image + return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, item_id, item_type) - def getAllArtwork(self, item, parentInfo=False): + def get_all_artwork(self, item, parent_info=False): - itemid = item['Id'] + item_id = item['Id'] artworks = item['ImageTags'] backdrops = item.get('BackdropImageTags', []) - maxHeight = 10000 - maxWidth = 10000 - customquery = "" + max_height = 10000 + max_width = 10000 + custom_query = "" if settings('compressArt') == "true": - customquery = "&Quality=90" + custom_query = "&Quality=90" if settings('enableCoverArt') == "false": - customquery += "&EnableImageEnhancers=false" + custom_query += "&EnableImageEnhancers=false" - allartworks = { + all_artwork = { 'Primary': "", 'Art': "", @@ -540,71 +504,53 @@ class Artwork(): 'Backdrop': [] } + def get_backdrops(item_id, backdrops): + + for index, tag in enumerate(backdrops): + artwork = ("%s/emby/Items/%s/Images/Backdrop/%s?" + "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" + % (self.server, item_id, index, max_width, max_height, + tag, custom_query)) + all_artwork['Backdrop'].append(artwork) + + def get_artwork(item_id, type_, tag): + + artwork = ("%s/emby/Items/%s/Images/%s/0?" + "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" + % (self.server, item_id, type_, max_width, max_height, tag, custom_query)) + all_artwork[type_] = artwork + # Process backdrops - for index, tag in enumerate(backdrops): - artwork = ( - "%s/emby/Items/%s/Images/Backdrop/%s?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, itemid, index, maxWidth, maxHeight, tag, customquery)) - allartworks['Backdrop'].append(artwork) + get_backdrops(item_id, backdrops) # Process the rest of the artwork - for art in artworks: + for artwork in artworks: # Filter backcover - if art != "BoxRear": - tag = artworks[art] - artwork = ( - "%s/emby/Items/%s/Images/%s/0?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, itemid, art, maxWidth, maxHeight, tag, customquery)) - allartworks[art] = artwork + if artwork != "BoxRear": + get_artwork(item_id, artwork, artworks[artwork]) # Process parent items if the main item is missing artwork - if parentInfo: - + if parent_info: # Process parent backdrops - if not allartworks['Backdrop']: + if not all_artwork['Backdrop']: - parentId = item.get('ParentBackdropItemId') - if parentId: - # If there is a parentId, go through the parent backdrop list - parentbackdrops = item['ParentBackdropImageTags'] - - for index, tag in enumerate(parentbackdrops): - artwork = ( - "%s/emby/Items/%s/Images/Backdrop/%s?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, parentId, index, maxWidth, maxHeight, tag, customquery)) - allartworks['Backdrop'].append(artwork) + if 'ParentBackdropItemId' in item: + # If there is a parent_id, go through the parent backdrop list + get_backdrops(item['ParentBackdropItemId'], item['ParentBackdropImageTags']) # Process the rest of the artwork - parentartwork = ['Logo', 'Art', 'Thumb'] - for parentart in parentartwork: + for parent_artwork in ('Logo', 'Art', 'Thumb'): - if not allartworks[parentart]: + if not all_artwork[parent_artwork]: - parentId = item.get('Parent%sItemId' % parentart) - if parentId: - - parentTag = item['Parent%sImageTag' % parentart] - artwork = ( - "%s/emby/Items/%s/Images/%s/0?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, parentId, parentart, - maxWidth, maxHeight, parentTag, customquery)) - allartworks[parentart] = artwork + if 'Parent%sItemId' % parent_artwork in item: + get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork, + item['Parent%sImageTag' % parent_artwork]) # Parent album works a bit differently - if not allartworks['Primary']: + if not all_artwork['Primary']: - parentId = item.get('AlbumId') - if parentId and item.get('AlbumPrimaryImageTag'): + if 'AlbumId' in item and 'AlbumPrimaryImageTag' in item: + get_artwork(item['AlbumId'], 'Primary', item['AlbumPrimaryImageTag']) - parentTag = item['AlbumPrimaryImageTag'] - artwork = ( - "%s/emby/Items/%s/Images/Primary/0?" - "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" - % (self.server, parentId, maxWidth, maxHeight, parentTag, customquery)) - allartworks['Primary'] = artwork - - return allartworks \ No newline at end of file + return all_artwork diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 7fd485e3..10bc6a7e 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -19,40 +19,39 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## -class ClientInfo(): +class ClientInfo(object): def __init__(self): - self.addon = xbmcaddon.Addon() - self.addonName = self.getAddonName() + self.addon = xbmcaddon.Addon(self.get_addon_id()) + @staticmethod + def get_addon_id(): + return "plugin.video.emby" - def getAddonName(self): + def get_addon_name(self): # Used for logging return self.addon.getAddonInfo('name').upper() - def getAddonId(self): - - return "plugin.video.emby" - - def getVersion(self): - + def get_version(self): return self.addon.getAddonInfo('version') - def getDeviceName(self): + @classmethod + def get_device_name(cls): if settings('deviceNameOpt') == "false": # Use Kodi's deviceName - deviceName = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8') + device_name = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8') else: - deviceName = settings('deviceName') - deviceName = deviceName.replace("\"", "_") - deviceName = deviceName.replace("/", "_") + device_name = settings('deviceName') + device_name = device_name.replace("\"", "_") + device_name = device_name.replace("/", "_") - return deviceName + return device_name - def getPlatform(self): + @classmethod + def get_platform(cls): if xbmc.getCondVisibility('system.platform.osx'): return "OSX" @@ -62,42 +61,52 @@ class ClientInfo(): return "iOS" elif xbmc.getCondVisibility('system.platform.windows'): return "Windows" - elif xbmc.getCondVisibility('system.platform.linux'): - return "Linux/RPi" - elif xbmc.getCondVisibility('system.platform.android'): + 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 getDeviceId(self, reset=False): + def get_device_id(self, reset=False): - clientId = window('emby_deviceId') - if clientId: - return clientId + client_id = window('emby_deviceId') + if client_id: + return client_id - addon_path = self.addon.getAddonInfo('path').decode('utf-8') - if os.path.supports_unicode_filenames: - path = os.path.join(addon_path, "machine_guid") - else: - path = os.path.join(addon_path.encode('utf-8'), "machine_guid") - - GUID_file = xbmc.translatePath(path).decode('utf-8') - - if reset and xbmcvfs.exists(GUID_file): + emby_guid = xbmc.translatePath("special://temp/emby_guid").decode('utf-8') + + ###$ Begin migration $### + if not xbmcvfs.exists(emby_guid): + addon_path = self.addon.getAddonInfo('path').decode('utf-8') + if os.path.supports_unicode_filenames: + path = os.path.join(addon_path, "machine_guid") + else: + path = os.path.join(addon_path.encode('utf-8'), "machine_guid") + + guid_file = xbmc.translatePath(path).decode('utf-8') + if xbmcvfs.exists(guid_file): + xbmcvfs.copy(guid_file, emby_guid) + log.info("guid migration completed") + ###$ End migration $### + + if reset and xbmcvfs.exists(emby_guid): # Reset the file - xbmcvfs.delete(GUID_file) + xbmcvfs.delete(emby_guid) - GUID = xbmcvfs.File(GUID_file) - clientId = GUID.read() - if not clientId: - log.info("Generating a new deviceid...") - clientId = str("%012X" % uuid4()) - GUID = xbmcvfs.File(GUID_file, 'w') - GUID.write(clientId) + guid = xbmcvfs.File(emby_guid) + client_id = guid.read() + if not client_id: + log.info("Generating a new guid...") + client_id = str("%012X" % uuid4()) + guid = xbmcvfs.File(emby_guid, 'w') + guid.write(client_id) - GUID.close() + guid.close() - log.info("DeviceId loaded: %s" % clientId) - window('emby_deviceId', value=clientId) - - return clientId \ No newline at end of file + log.info("DeviceId loaded: %s", client_id) + window('emby_deviceId', value=client_id) + + return client_id diff --git a/resources/lib/connect.py b/resources/lib/connect.py deleted file mode 100644 index aa3082a7..00000000 --- a/resources/lib/connect.py +++ /dev/null @@ -1,257 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import json -import requests -import logging - -import clientinfo -from utils import window - -################################################################################################## - -# Disable requests logging -from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################## - - -class ConnectUtils(): - - # Borg - multiple instances, shared state - _shared_state = {} - clientInfo = clientinfo.ClientInfo() - - # Requests session - c = None - timeout = 30 - - - def __init__(self): - - self.__dict__ = self._shared_state - - - def setUserId(self, userId): - # Reserved for userclient only - self.userId = userId - log.debug("Set connect userId: %s" % userId) - - def setServer(self, server): - # Reserved for userclient only - self.server = server - log.debug("Set connect server: %s" % server) - - def setToken(self, token): - # Reserved for userclient only - self.token = token - log.debug("Set connect token: %s" % token) - - - def startSession(self): - - self.deviceId = self.clientInfo.getDeviceId() - - # User is identified from this point - # Attach authenticated header to the session - verify = False - header = self.getHeader() - - # If user enabled host certificate verification - try: - verify = self.sslverify - if self.sslclient is not None: - verify = self.sslclient - except: - log.info("Could not load SSL settings.") - - # Start session - self.c = requests.Session() - self.c.headers = header - self.c.verify = verify - # Retry connections to the server - self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) - self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - - log.info("Requests session started on: %s" % self.server) - - def stopSession(self): - try: - self.c.close() - except Exception: - log.warn("Requests session could not be terminated") - - def getHeader(self, authenticate=True): - - version = self.clientInfo.getVersion() - - if not authenticate: - # If user is not authenticated - header = { - - 'X-Application': "Kodi/%s" % version, - 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Accept': "application/json" - } - log.info("Header: %s" % header) - - else: - token = self.token - # Attached to the requests session - header = { - - 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Accept': "application/json", - 'X-Application': "Kodi/%s" % version, - 'X-Connect-UserToken': token - } - log.info("Header: %s" % header) - - return header - - def doUrl(self, url, data=None, postBody=None, rtype="GET", - parameters=None, authenticate=True, timeout=None): - - log.debug("=== ENTER connectUrl ===") - - default_link = "" - - if timeout is None: - timeout = self.timeout - - # Get requests session - try: - # If connect user is authenticated - if authenticate: - try: - c = self.c - # Replace for the real values - url = url.replace("{server}", self.server) - url = url.replace("{UserId}", self.userId) - - # Prepare request - if rtype == "GET": - r = c.get(url, json=postBody, params=parameters, timeout=timeout) - elif rtype == "POST": - r = c.post(url, data=data, timeout=timeout) - elif rtype == "DELETE": - r = c.delete(url, json=postBody, timeout=timeout) - - except AttributeError: - # request session does not exists - self.server = "https://connect.emby.media/service" - self.userId = window('embyco_currUser') - self.token = window('embyco_accessToken%s' % self.userId) - - header = self.getHeader() - verifyssl = False - - # If user enables ssl verification - try: - verifyssl = self.sslverify - if self.sslclient is not None: - verifyssl = self.sslclient - except AttributeError: - pass - - # Prepare request - if rtype == "GET": - r = requests.get(url, - json=postBody, - params=parameters, - headers=header, - timeout=timeout, - verify=verifyssl) - - elif rtype == "POST": - r = requests.post(url, - data=data, - headers=header, - timeout=timeout, - verify=verifyssl) - # If user is not authenticated - else: - header = self.getHeader(authenticate=False) - verifyssl = False - - # If user enables ssl verification - try: - verifyssl = self.sslverify - if self.sslclient is not None: - verifyssl = self.sslclient - except AttributeError: - pass - - # Prepare request - if rtype == "GET": - r = requests.get(url, - json=postBody, - params=parameters, - headers=header, - timeout=timeout, - verify=verifyssl) - - elif rtype == "POST": - r = requests.post(url, - data=data, - headers=header, - timeout=timeout, - verify=verifyssl) - - ##### THE RESPONSE ##### - log.info(r.url) - log.info(r) - - if r.status_code == 204: - # No body in the response - log.info("====== 204 Success ======") - - elif r.status_code == requests.codes.ok: - - try: - # UNICODE - JSON object - r = r.json() - log.info("====== 200 Success ======") - log.info("Response: %s" % r) - return r - - except: - if r.headers.get('content-type') != "text/html": - log.info("Unable to convert the response for: %s" % url) - else: - r.raise_for_status() - - ##### EXCEPTIONS ##### - - except requests.exceptions.ConnectionError as e: - # Make the addon aware of status - pass - - except requests.exceptions.ConnectTimeout as e: - log.warn("Server timeout at: %s" % url) - - except requests.exceptions.HTTPError as e: - - if r.status_code == 401: - # Unauthorized - pass - - elif r.status_code in (301, 302): - # Redirects - pass - elif r.status_code == 400: - # Bad requests - pass - - except requests.exceptions.SSLError as e: - log.warn("Invalid SSL certificate for: %s" % url) - - except requests.exceptions.RequestException as e: - log.warn("Unknown error connecting to: %s" % url) - - return default_link \ No newline at end of file diff --git a/resources/lib/dialog/__init__.py b/resources/lib/connect/__init__.py similarity index 100% rename from resources/lib/dialog/__init__.py rename to resources/lib/connect/__init__.py diff --git a/resources/lib/connect/connectionmanager.py b/resources/lib/connect/connectionmanager.py new file mode 100644 index 00000000..74daca2d --- /dev/null +++ b/resources/lib/connect/connectionmanager.py @@ -0,0 +1,816 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import hashlib +import json +import logging +import requests +import socket +import time +from datetime import datetime + +import credentials as cred + +################################################################################################# + +# Disable requests logging +from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning, SNIMissingWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) +requests.packages.urllib3.disable_warnings(InsecurePlatformWarning) +requests.packages.urllib3.disable_warnings(SNIMissingWarning) + +log = logging.getLogger("EMBY."+__name__.split('.')[-1]) + +################################################################################################# + +ConnectionState = { + 'Unavailable': 0, + 'ServerSelection': 1, + 'ServerSignIn': 2, + 'SignedIn': 3, + 'ConnectSignIn': 4, + 'ServerUpdateNeeded': 5 +} + +ConnectionMode = { + 'Local': 0, + 'Remote': 1, + 'Manual': 2 +} + +################################################################################################# + +def getServerAddress(server, mode): + + modes = { + ConnectionMode['Local']: server.get('LocalAddress'), + ConnectionMode['Remote']: server.get('RemoteAddress'), + ConnectionMode['Manual']: server.get('ManualAddress') + } + return (modes.get(mode) or + server.get('ManualAddress',server.get('LocalAddress',server.get('RemoteAddress')))) + + +class ConnectionManager(object): + + default_timeout = 20 + apiClients = [] + minServerVersion = "3.0.5930" + connectUser = None + + + def __init__(self, appName, appVersion, deviceName, deviceId, capabilities=None, devicePixelRatio=None): + + log.info("Begin ConnectionManager constructor") + + self.credentialProvider = cred.Credentials() + self.appName = appName + self.appVersion = appVersion + self.deviceName = deviceName + self.deviceId = deviceId + self.capabilities = capabilities + self.devicePixelRatio = devicePixelRatio + + + def setFilePath(self, path): + # Set where to save persistant data + self.credentialProvider.setPath(path) + + def _getAppVersion(self): + return self.appVersion + + def _getCapabilities(self): + return self.capabilities + + def _getDeviceId(self): + return self.deviceId + + def _connectUserId(self): + return self.credentialProvider.getCredentials().get('ConnectUserId') + + def _connectToken(self): + return self.credentialProvider.getCredentials().get('ConnectAccessToken') + + def getServerInfo(self, id_): + + servers = self.credentialProvider.getCredentials()['Servers'] + + for s in servers: + if s['Id'] == id_: + return s + + def _getLastUsedServer(self): + + servers = self.credentialProvider.getCredentials()['Servers'] + + if not len(servers): + return + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + return servers[0] + + def _mergeServers(self, list1, list2): + + for i in range(0, len(list2), 1): + try: + self.credentialProvider.addOrUpdateServer(list1, list2[i]) + except KeyError: + continue + + return list1 + + def _connectUser(self): + + return self.connectUser + + def _resolveFailure(self): + + return { + 'State': ConnectionState['Unavailable'], + 'ConnectUser': self._connectUser() + } + + def _getMinServerVersion(self, val=None): + + if val is not None: + self.minServerVersion = val + + return self.minServerVersion + + def _updateServerInfo(self, server, systemInfo): + + server['Name'] = systemInfo['ServerName'] + server['Id'] = systemInfo['Id'] + + if systemInfo.get('LocalAddress'): + server['LocalAddress'] = systemInfo['LocalAddress'] + if systemInfo.get('WanAddress'): + server['RemoteAddress'] = systemInfo['WanAddress'] + if systemInfo.get('MacAddress'): + server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}] + + def _getHeaders(self, request): + + headers = request.setdefault('headers', {}) + + if request.get('dataType') == "json": + headers['Accept'] = "application/json" + request.pop('dataType') + + headers['X-Application'] = self._addAppInfoToConnectRequest() + headers['Content-type'] = request.get('contentType', + 'application/x-www-form-urlencoded; charset=UTF-8') + + def requestUrl(self, request): + + if not request: + raise AttributeError("Request cannot be null") + + self._getHeaders(request) + request['timeout'] = request.get('timeout') or self.default_timeout + request['verify'] = request.get('ssl') or False + + action = request['type'] + request.pop('type', None) + request.pop('ssl', None) + + log.debug("ConnectionManager requesting %s" % request) + + try: + r = self._requests(action, **request) + log.info("ConnectionManager response status: %s" % r.status_code) + r.raise_for_status() + + except Exception as e: # Elaborate on exceptions? + log.error(e) + raise + + else: + try: + return r.json() + except ValueError: + r.content # Read response to release connection + return + + def _requests(self, action, **kwargs): + + if action == "GET": + r = requests.get(**kwargs) + elif action == "POST": + r = requests.post(**kwargs) + + return r + + def getEmbyServerUrl(self, baseUrl, handler): + return "%s/emby/%s" % (baseUrl, handler) + + def getConnectUrl(self, handler): + return "https://connect.emby.media/service/%s" % handler + + def _findServers(self, foundServers): + + servers = [] + + for foundServer in foundServers: + + server = self._convertEndpointAddressToManualAddress(foundServer) + + info = { + 'Id': foundServer['Id'], + 'LocalAddress': server or foundServer['Address'], + 'Name': foundServer['Name'] + } + info['LastConnectionMode'] = ConnectionMode['Manual'] if info.get('ManualAddress') else ConnectionMode['Local'] + + servers.append(info) + else: + return servers + + def _convertEndpointAddressToManualAddress(self, info): + + if info.get('Address') and info.get('EndpointAddress'): + address = info['EndpointAddress'].split(':')[0] + + # Determine the port, if any + parts = info['Address'].split(':') + if len(parts) > 1: + portString = parts[len(parts)-1] + + try: + address += ":%s" % int(portString) + return self._normalizeAddress(address) + except ValueError: + pass + + return None + + def _serverDiscovery(self): + + MULTI_GROUP = ("", 7359) + MESSAGE = "who is EmbyServer?" + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(1.0) # This controls the socket.timeout exception + + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) + + log.debug("MultiGroup : %s" % str(MULTI_GROUP)) + log.debug("Sending UDP Data: %s" % MESSAGE) + + servers = [] + + try: + sock.sendto(MESSAGE, MULTI_GROUP) + except Exception as error: + log.error(error) + return servers + + while True: + try: + data, addr = sock.recvfrom(1024) # buffer size + servers.append(json.loads(data)) + + except socket.timeout: + log.info("Found Servers: %s" % servers) + return servers + + except Exception as e: + log.error("Error trying to find servers: %s" % e) + return servers + + def _normalizeAddress(self, address): + # Attempt to correct bad input + address = address.strip() + address = address.lower() + + if 'http' not in address: + address = "http://%s" % address + + return address + + def connectToAddress(self, address, options={}): + + if not address: + return False + + address = self._normalizeAddress(address) + + def _onFail(): + log.error("connectToAddress %s failed" % address) + return self._resolveFailure() + + try: + publicInfo = self._tryConnect(address, options=options) + except Exception: + return _onFail() + else: + log.info("connectToAddress %s succeeded" % address) + server = { + 'ManualAddress': address, + 'LastConnectionMode': ConnectionMode['Manual'] + } + self._updateServerInfo(server, publicInfo) + server = self.connectToServer(server, options) + if server is False: + return _onFail() + else: + return server + + def onAuthenticated(self, result, options={}): + + credentials = self.credentialProvider.getCredentials() + for s in credentials['Servers']: + if s['Id'] == result['ServerId']: + server = s + break + else: # Server not found? + return + + if options.get('updateDateLastAccessed') is not False: + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + server['UserId'] = result['User']['Id'] + server['AccessToken'] = result['AccessToken'] + + self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + self._saveUserInfoIntoCredentials(server, result['User']) + self.credentialProvider.getCredentials(credentials) + + def _tryConnect(self, url, timeout=None, options={}): + + url = self.getEmbyServerUrl(url, "system/info/public") + log.info("tryConnect url: %s" % url) + + return self.requestUrl({ + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'timeout': timeout, + 'ssl': options.get('ssl') + }) + + def _addAppInfoToConnectRequest(self): + return "%s/%s" % (self.appName, self.appVersion) + + def _getConnectServers(self, credentials): + + log.info("Begin getConnectServers") + + servers = [] + + if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): + return servers + + url = self.getConnectUrl("servers?userId=%s" % credentials['ConnectUserId']) + request = { + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': credentials['ConnectAccessToken'] + } + } + for server in self.requestUrl(request): + + servers.append({ + + 'ExchangeToken': server['AccessKey'], + 'ConnectServerId': server['Id'], + 'Id': server['SystemId'], + 'Name': server['Name'], + 'RemoteAddress': server['Url'], + 'LocalAddress': server['LocalAddress'], + 'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser", + }) + + return servers + + def getAvailableServers(self): + + log.info("Begin getAvailableServers") + + # Clone the array + credentials = self.credentialProvider.getCredentials() + + connectServers = self._getConnectServers(credentials) + foundServers = self._findServers(self._serverDiscovery()) + + servers = list(credentials['Servers']) + self._mergeServers(servers, foundServers) + self._mergeServers(servers, connectServers) + + servers = self._filterServers(servers, connectServers) + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + credentials['Servers'] = servers + self.credentialProvider.getCredentials(credentials) + + return servers + + def _filterServers(self, servers, connectServers): + + filtered = [] + + for server in servers: + # It's not a connect server, so assume it's still valid + if server.get('ExchangeToken') is None: + filtered.append(server) + continue + + for connectServer in connectServers: + if server['Id'] == connectServer['Id']: + filtered.append(server) + break + else: + return filtered + + def _getConnectPasswordHash(self, password): + + password = self._cleanConnectPassword(password) + + return hashlib.md5(password).hexdigest() + + def _saveUserInfoIntoCredentials(self, server, user): + + info = { + 'Id': user['Id'], + 'IsSignedInOffline': True + } + + self.credentialProvider.addOrUpdateUser(server, info) + + def _compareVersions(self, a, b): + """ + -1 a is smaller + 1 a is larger + 0 equal + """ + a = a.split('.') + b = b.split('.') + + for i in range(0, max(len(a), len(b)), 1): + try: + aVal = a[i] + except IndexError: + aVal = 0 + + try: + bVal = b[i] + except IndexError: + bVal = 0 + + if aVal < bVal: + return -1 + + if aVal > bVal: + return 1 + + return 0 + + def connectToServer(self, server, options={}): + + log.info("begin connectToServer") + + tests = [] + + if server.get('LastConnectionMode') is not None: + #tests.append(server['LastConnectionMode']) + pass + if ConnectionMode['Manual'] not in tests: + tests.append(ConnectionMode['Manual']) + if ConnectionMode['Local'] not in tests: + tests.append(ConnectionMode['Local']) + if ConnectionMode['Remote'] not in tests: + tests.append(ConnectionMode['Remote']) + + # TODO: begin to wake server + + log.info("beginning connection tests") + return self._testNextConnectionMode(tests, 0, server, options) + + def _stringEqualsIgnoreCase(self, str1, str2): + + return (str1 or "").lower() == (str2 or "").lower() + + def _testNextConnectionMode(self, tests, index, server, options): + + if index >= len(tests): + log.info("Tested all connection modes. Failing server connection.") + return self._resolveFailure() + + mode = tests[index] + address = getServerAddress(server, mode) + enableRetry = False + skipTest = False + timeout = self.default_timeout + + if mode == ConnectionMode['Local']: + enableRetry = True + timeout = 8 + + if self._stringEqualsIgnoreCase(address, server.get('ManualAddress')): + log.info("skipping LocalAddress test because it is the same as ManualAddress") + skipTest = True + + elif mode == ConnectionMode['Manual']: + + if self._stringEqualsIgnoreCase(address, server.get('LocalAddress')): + enableRetry = True + timeout = 8 + + if skipTest or not address: + log.info("skipping test at index: %s" % index) + return self._testNextConnectionMode(tests, index+1, server, options) + + log.info("testing connection mode %s with server %s" % (mode, server['Name'])) + try: + result = self._tryConnect(address, timeout, options) + + except Exception: + log.error("test failed for connection mode %s with server %s" % (mode, server['Name'])) + + if enableRetry: + # TODO: wake on lan and retry + return self._testNextConnectionMode(tests, index+1, server, options) + else: + return self._testNextConnectionMode(tests, index+1, server, options) + else: + + if self._compareVersions(self._getMinServerVersion(), result['Version']) == 1: + log.warn("minServerVersion requirement not met. Server version: %s" % result['Version']) + return { + 'State': ConnectionState['ServerUpdateNeeded'], + 'Servers': [server] + } + else: + log.info("calling onSuccessfulConnection with connection mode %s with server %s" + % (mode, server['Name'])) + return self._onSuccessfulConnection(server, result, mode, options) + + def _onSuccessfulConnection(self, server, systemInfo, connectionMode, options): + + credentials = self.credentialProvider.getCredentials() + + if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False: + + if self._ensureConnectUser(credentials) is not False: + + if server.get('ExchangeToken'): + + self._addAuthenticationInfoFromConnect(server, connectionMode, credentials, options) + + return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, True, options) + + def _afterConnectValidated(self, server, credentials, systemInfo, connectionMode, verifyLocalAuthentication, options): + + if options.get('enableAutoLogin') is False: + server['UserId'] = None + server['AccessToken'] = None + + elif (verifyLocalAuthentication and server.get('AccessToken') and + options.get('enableAutoLogin') is not False): + + if self._validateAuthentication(server, connectionMode, options) is not False: + return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, False, options) + + return + + self._updateServerInfo(server, systemInfo) + server['LastConnectionMode'] = connectionMode + + if options.get('updateDateLastAccessed') is not False: + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + self.credentialProvider.getCredentials(credentials) + + result = { + 'Servers': [], + 'ConnectUser': self._connectUser() + } + result['State'] = ConnectionState['SignedIn'] if (server.get('AccessToken') and options.get('enableAutoLogin') is not False) else ConnectionState['ServerSignIn'] + result['Servers'].append(server) + + # Connected + return result + + def _validateAuthentication(self, server, connectionMode, options={}): + + url = getServerAddress(server, connectionMode) + request = { + + 'type': "GET", + 'url': self.getEmbyServerUrl(url, "System/Info"), + 'ssl': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-MediaBrowser-Token': server['AccessToken'] + } + } + try: + systemInfo = self.requestUrl(request) + self._updateServerInfo(server, systemInfo) + + if server.get('UserId'): + user = self.requestUrl({ + + 'type': "GET", + 'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']), + 'ssl': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-MediaBrowser-Token': server['AccessToken'] + } + }) + + except Exception: + server['UserId'] = None + server['AccessToken'] = None + return False + + def loginToConnect(self, username, password): + + if not username: + raise AttributeError("username cannot be empty") + + if not password: + raise AttributeError("password cannot be empty") + + md5 = self._getConnectPasswordHash(password) + request = { + 'type': "POST", + 'url': self.getConnectUrl("user/authenticate"), + 'data': { + 'nameOrEmail': username, + 'password': md5 + }, + 'dataType': "json" + } + try: + result = self.requestUrl(request) + except Exception as e: # Failed to login + log.error(e) + return False + else: + credentials = self.credentialProvider.getCredentials() + credentials['ConnectAccessToken'] = result['AccessToken'] + credentials['ConnectUserId'] = result['User']['Id'] + credentials['ConnectUser'] = result['User']['DisplayName'] + self.credentialProvider.getCredentials(credentials) + # Signed in + self._onConnectUserSignIn(result['User']) + + return result + + def _onConnectUserSignIn(self, user): + + self.connectUser = user + log.info("connectusersignedin %s" % user) + + def _getConnectUser(self, userId, accessToken): + + if not userId: + raise AttributeError("null userId") + + if not accessToken: + raise AttributeError("null accessToken") + + url = self.getConnectUrl('user?id=%s' % userId) + + return self.requestUrl({ + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': accessToken + } + }) + + def _addAuthenticationInfoFromConnect(self, server, connectionMode, credentials, options={}): + + if not server.get('ExchangeToken'): + raise KeyError("server['ExchangeToken'] cannot be null") + + if not credentials.get('ConnectUserId'): + raise KeyError("credentials['ConnectUserId'] cannot be null") + + url = getServerAddress(server, connectionMode) + url = self.getEmbyServerUrl(url, "Connect/Exchange?format=json") + auth = ('MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"' + % (self.appName, self.deviceName, self.deviceId, self.appVersion)) + try: + auth = self.requestUrl({ + + 'url': url, + 'type': "GET", + 'dataType': "json", + 'ssl': options.get('ssl'), + 'params': { + 'ConnectUserId': credentials['ConnectUserId'] + }, + 'headers': { + 'X-MediaBrowser-Token': server['ExchangeToken'], + 'X-Emby-Authorization': auth + } + }) + except Exception: + server['UserId'] = None + server['AccessToken'] = None + return False + else: + server['UserId'] = auth['LocalUserId'] + server['AccessToken'] = auth['AccessToken'] + return auth + + def _ensureConnectUser(self, credentials): + + if self.connectUser and self.connectUser['Id'] == credentials['ConnectUserId']: + return + + elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): + + self.connectUser = None + + try: + result = self._getConnectUser(credentials['ConnectUserId'], credentials['ConnectAccessToken']) + self._onConnectUserSignIn(result) + except Exception: + return False + + def connect(self, options={}): + + log.info("Begin connect") + + servers = self.getAvailableServers() + return self._connectToServers(servers, options) + + def _connectToServers(self, servers, options): + + log.info("Begin connectToServers, with %s servers" % len(servers)) + + if len(servers) == 1: + result = self.connectToServer(servers[0], options) + if result and result.get('State') == ConnectionState['Unavailable']: + result['State'] = ConnectionState['ConnectSignIn'] if result['ConnectUser'] == None else ConnectionState['ServerSelection'] + + log.info("resolving connectToServers with result['State']: %s" % result) + return result + + firstServer = self._getLastUsedServer() + # See if we have any saved credentials and can auto sign in + if firstServer: + + result = self.connectToServer(firstServer, options) + if result and result.get('State') == ConnectionState['SignedIn']: + return result + + # Return loaded credentials if exists + credentials = self.credentialProvider.getCredentials() + self._ensureConnectUser(credentials) + + return { + 'Servers': servers, + 'State': ConnectionState['ConnectSignIn'] if (not len(servers) and not self._connectUser()) else ConnectionState['ServerSelection'], + 'ConnectUser': self._connectUser() + } + + def _cleanConnectPassword(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 clearData(self): + + log.info("connection manager clearing data") + + self.connectUser = None + credentials = self.credentialProvider.getCredentials() + credentials['ConnectAccessToken'] = None + credentials['ConnectUserId'] = None + credentials['Servers'] = [] + self.credentialProvider.getCredentials(credentials) \ No newline at end of file diff --git a/resources/lib/connect/credentials.py b/resources/lib/connect/credentials.py new file mode 100644 index 00000000..4712a7c2 --- /dev/null +++ b/resources/lib/connect/credentials.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import os +import time +from datetime import datetime + +################################################################################################# + +log = logging.getLogger("EMBY."+__name__.split('.')[-1]) + +################################################################################################# + + +class Credentials(object): + + _shared_state = {} # Borg + credentials = None + path = "" + + + def __init__(self): + self.__dict__ = self._shared_state + + def setPath(self, path): + # Path to save persistant data.txt + self.path = path + + def _ensure(self): + + if self.credentials is None: + try: + with open(os.path.join(self.path, 'data.txt')) as infile: + self.credentials = json.load(infile) + + except Exception as e: # File is either empty or missing + log.warn(e) + self.credentials = {} + + log.info("credentials initialized with: %s" % self.credentials) + self.credentials['Servers'] = self.credentials.setdefault('Servers', []) + + def _get(self): + + self._ensure() + return self.credentials + + def _set(self, data): + + if data: + self.credentials = data + # Set credentials to file + with open(os.path.join(self.path, 'data.txt'), 'w') as outfile: + json.dump(data, outfile, indent=4, ensure_ascii=False) + else: + self._clear() + + log.info("credentialsupdated") + + def _clear(self): + + self.credentials = None + # Remove credentials from file + with open(os.path.join(self.path, 'data.txt'), 'w'): pass + + def getCredentials(self, data=None): + + if data is not None: + self._set(data) + + return self._get() + + def addOrUpdateServer(self, list_, server): + + if server.get('Id') is None: + raise KeyError("Server['Id'] cannot be null or empty") + + # Add default DateLastAccessed if doesn't exist. + server.setdefault('DateLastAccessed', "2001-01-01T00:00:00Z") + + for existing in list_: + if existing['Id'] == server['Id']: + + # Merge the data + if server.get('DateLastAccessed'): + if self._dateObject(server['DateLastAccessed']) > self._dateObject(existing['DateLastAccessed']): + existing['DateLastAccessed'] = server['DateLastAccessed'] + + if server.get('UserLinkType'): + existing['UserLinkType'] = server['UserLinkType'] + + if server.get('AccessToken'): + existing['AccessToken'] = server['AccessToken'] + existing['UserId'] = server['UserId'] + + if server.get('ExchangeToken'): + existing['ExchangeToken'] = server['ExchangeToken'] + + if server.get('RemoteAddress'): + existing['RemoteAddress'] = server['RemoteAddress'] + + if server.get('ManualAddress'): + existing['ManualAddress'] = server['ManualAddress'] + + if server.get('LocalAddress'): + existing['LocalAddress'] = server['LocalAddress'] + + if server.get('Name'): + existing['Name'] = server['Name'] + + if server.get('WakeOnLanInfos'): + existing['WakeOnLanInfos'] = server['WakeOnLanInfos'] + + if server.get('LastConnectionMode') is not None: + existing['LastConnectionMode'] = server['LastConnectionMode'] + + if server.get('ConnectServerId'): + existing['ConnectServerId'] = server['ConnectServerId'] + + return existing + else: + list_.append(server) + return server + + def addOrUpdateUser(self, server, user): + + for existing in server.setdefault('Users', []): + if existing['Id'] == user['Id']: + # Merge the data + existing['IsSignedInOffline'] = True + break + else: + server['Users'].append(user) + + def _dateObject(self, date): + # Convert string to date + try: + date_obj = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + except (ImportError, TypeError): + # TypeError: attribute of type 'NoneType' is not callable + # Known Kodi/python error + date_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) + + return date_obj \ No newline at end of file diff --git a/resources/lib/connectmanager.py b/resources/lib/connectmanager.py new file mode 100644 index 00000000..99a4eae0 --- /dev/null +++ b/resources/lib/connectmanager.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcaddon +import xbmcvfs + +import clientinfo +import read_embyserver as embyserver +import connect.connectionmanager as connectionmanager +from dialogs import ServerConnect, UsersConnect, LoginConnect, LoginManual, ServerManual +from ga_client import GoogleAnalytics +from utils import window + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon(id='plugin.video.emby') + +STATE = connectionmanager.ConnectionState +XML_PATH = (addon.getAddonInfo('path'), "default", "1080i") + +################################################################################################## + +class ConnectManager(object): + + _shared_state = {} # Borg + state = {} + + + def __init__(self): + + self.__dict__ = self._shared_state + + client_info = clientinfo.ClientInfo() + self.emby = embyserver.Read_EmbyServer() + + version = client_info.get_version() + device_name = client_info.get_device_name() + device_id = client_info.get_device_id() + self._connect = connectionmanager.ConnectionManager(appName="Kodi", + appVersion=version, + deviceName=device_name, + deviceId=device_id) + path = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/").decode('utf-8') + + if not xbmcvfs.exists(path): + xbmcvfs.mkdirs(path) + + self._connect.setFilePath(path) + + if window('emby_state.json'): + self.state = window('emby_state.json') + + elif not self.state: + self.state = self._connect.connect() + log.info("Started with: %s", self.state) + window('emby_state.json', value=self.state) + + + def update_state(self): + self.state = self._connect.connect({'updateDateLastAccessed': False}) + return self.get_state() + + def get_state(self): + window('emby_state.json', value=self.state) + return self.state + + def get_server(self, server, options={}): + self.state = self._connect.connectToAddress(server, options) + return self.get_state() + + @classmethod + def get_address(cls, server): + return connectionmanager.getServerAddress(server, server['LastConnectionMode']) + + def clear_data(self): + self._connect.clearData() + + def select_servers(self): + # Will return selected server or raise error + state = self._connect.connect({'enableAutoLogin': False}) + user = state.get('ConnectUser') or {} + + dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH) + kwargs = { + 'connect_manager': self._connect, + 'username': user.get('DisplayName', ""), + 'user_image': user.get('ImageUrl'), + 'servers': state.get('Servers') or [], + 'emby_connect': False if user else True + } + dialog.set_args(**kwargs) + dialog.doModal() + + if dialog.is_server_selected(): + log.debug("Server selected") + return dialog.get_server() + + elif dialog.is_connect_login(): + log.debug("Login with Emby Connect") + try: # Login to emby connect + self.login_connect() + except RuntimeError: + pass + return self.select_servers() + + elif dialog.is_manual_server(): + log.debug("Add manual server") + try: # Add manual server address + return self.manual_server() + except RuntimeError: + return self.select_servers() + else: + raise RuntimeError("No server selected") + + def manual_server(self): + # Return server or raise error + dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) + dialog.set_connect_manager(self._connect) + dialog.doModal() + + if dialog.is_connected(): + return dialog.get_server() + else: + raise RuntimeError("Server is not connected") + + def login_connect(self): + # Return connect user or raise error + dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH) + dialog.set_connect_manager(self._connect) + dialog.doModal() + + self.update_state() + + if dialog.is_logged_in(): + return dialog.get_user() + else: + raise RuntimeError("Connect user is not logged in") + + def login(self, server=None): + + ga = GoogleAnalytics() + ga.sendEventData("Connect", "UserLogin") + + # Return user or raise error + server = server or self.state['Servers'][0] + server_address = connectionmanager.getServerAddress(server, server['LastConnectionMode']) + users = self.emby.getUsers(server_address) + + if not users: + try: + return self.login_manual(server_address) + except RuntimeError: + raise RuntimeError("No user selected") + + dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH) + dialog.set_server(server_address) + dialog.set_users(users) + dialog.doModal() + + if dialog.is_user_selected(): + + user = dialog.get_user() + username = user['Name'] + + if user['HasPassword']: + log.debug("User has password, present manual login") + try: + return self.login_manual(server_address, username) + except RuntimeError: + return self.login(server) + else: + user = self.emby.loginUser(server_address, username) + self._connect.onAuthenticated(user) + return user + + elif dialog.is_manual_login(): + try: + return self.login_manual(server_address) + except RuntimeError: + return self.login(server) + else: + raise RuntimeError("No user selected") + + def login_manual(self, server, user=None): + # Return manual login user authenticated or raise error + dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH) + dialog.set_server(server) + dialog.set_user(user) + dialog.doModal() + + if dialog.is_logged_in(): + user = dialog.get_user() + self._connect.onAuthenticated(user) + return user + else: + raise RuntimeError("User is not authenticated") + + def update_token(self, server): + + credentials = self._connect.credentialProvider.getCredentials() + self._connect.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + + for server in self.get_state()['Servers']: + for cred_server in credentials['Servers']: + if server['Id'] == cred_server['Id']: + # Update token saved in current state + server.update(cred_server) + # Update the token in data.txt + self._connect.credentialProvider.getCredentials(credentials) + + def get_connect_servers(self): + + connect_servers = [] + servers = self._connect.getAvailableServers() + for server in servers: + if 'ExchangeToken' in server: + result = self.connect_server(server) + if result['State'] == STATE['SignedIn']: + connect_servers.append(server) + + log.info(connect_servers) + return connect_servers + + def connect_server(self, server): + return self._connect.connectToServer(server, {'updateDateLastAccessed': False}) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py new file mode 100644 index 00000000..882b0489 --- /dev/null +++ b/resources/lib/context_entry.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import xbmc +import xbmcaddon + +import api +import read_embyserver as embyserver +import embydb_functions as embydb +import musicutils as musicutils +from utils import settings, dialog, language as lang, kodiSQL +from dialogs import context + +################################################################################################# + +log = logging.getLogger("EMBY."+__name__) +OPTIONS = { + + 'Refresh': lang(30410), + 'Delete': lang(30409), + 'Addon': lang(30408), + 'AddFav': lang(30405), + 'RemoveFav': lang(30406), + 'RateSong': lang(30407), + 'Transcode': lang(30412) +} + +################################################################################################# + + +class ContextMenu(object): + + _selected_option = None + + + def __init__(self): + + self.emby = embyserver.Read_EmbyServer() + + self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') + self.item_type = self._get_item_type() + self.item_id = self._get_item_id(self.kodi_id, self.item_type) + + log.info("Found item_id: %s item_type: %s", self.item_id, self.item_type) + + if self.item_id: + + self.item = self.emby.getItem(self.item_id) + self.api = api.API(self.item) + + if self._select_menu(): + self._action_menu() + + if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], + OPTIONS['RemoveFav'], OPTIONS['RateSong']): + log.info("refreshing container") + xbmc.sleep(500) + xbmc.executebuiltin('Container.Refresh') + + @classmethod + def _get_item_type(cls): + + item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') + + if not item_type: + + if xbmc.getCondVisibility('Container.Content(albums)'): + item_type = "album" + elif xbmc.getCondVisibility('Container.Content(artists)'): + item_type = "artist" + elif xbmc.getCondVisibility('Container.Content(songs)'): + item_type = "song" + elif xbmc.getCondVisibility('Container.Content(pictures)'): + item_type = "picture" + else: + log.info("item_type is unknown") + + return item_type + + @classmethod + def _get_item_id(cls, kodi_id, item_type): + + item_id = xbmc.getInfoLabel('ListItem.Property(embyid)') + + if not item_id and kodi_id and item_type: + + conn = kodiSQL('emby') + cursor = conn.cursor() + emby_db = embydb.Embydb_Functions(cursor) + item = emby_db.getItem_byKodiId(kodi_id, item_type) + cursor.close() + try: + item_id = item[0] + except TypeError: + pass + + return item_id + + def _select_menu(self): + # Display select dialog + userdata = self.api.get_userdata() + options = [] + + if self.item_type in ("movie", "episode", "song"): + #options.append(OPTIONS['Transcode']) + pass + + if userdata['Favorite']: + # Remove from emby favourites + options.append(OPTIONS['RemoveFav']) + else: + # Add to emby favourites + options.append(OPTIONS['AddFav']) + + if self.item_type == "song": + # Set custom song rating + options.append(OPTIONS['RateSong']) + + # Refresh item + options.append(OPTIONS['Refresh']) + # Delete item + options.append(OPTIONS['Delete']) + # Addon settings + options.append(OPTIONS['Addon']) + + addon = xbmcaddon.Addon('plugin.video.emby') + context_menu = context.ContextMenu("script-emby-context.xml", addon.getAddonInfo('path'), + "default", "1080i") + 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._selected_option + + if selected == OPTIONS['Transcode']: + pass + + elif selected == OPTIONS['Refresh']: + self.emby.refreshItem(self.item_id) + + elif selected == OPTIONS['AddFav']: + self.emby.updateUserRating(self.item_id, favourite=True) + + elif selected == OPTIONS['RemoveFav']: + self.emby.updateUserRating(self.item_id, favourite=False) + + elif selected == OPTIONS['RateSong']: + self._rate_song() + + elif selected == OPTIONS['Addon']: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)') + + elif selected == OPTIONS['Delete']: + self._delete_item() + + def _rate_song(self): + + conn = kodiSQL('music') + cursor = conn.cursor() + query = "SELECT rating FROM song WHERE idSong = ?" + cursor.execute(query, (self.kodi_id,)) + try: + value = cursor.fetchone()[0] + current_value = int(round(float(value), 0)) + except TypeError: + pass + else: + new_value = dialog("numeric", 0, lang(30411), str(current_value)) + if new_value > -1: + + new_value = int(new_value) + if new_value > 5: + new_value = 5 + + if settings('enableUpdateSongRating') == "true": + musicutils.updateRatingToFile(new_value, self.api.get_file_path()) + + query = "UPDATE song SET rating = ? WHERE idSong = ?" + cursor.execute(query, (new_value, self.kodi_id,)) + conn.commit() + finally: + cursor.close() + + def _delete_item(self): + + delete = True + if settings('skipContextMenu') != "true": + + if not dialog(type_="yesno", heading="{emby}", line1=lang(33041)): + log.info("User skipped deletion for: %s", self.item_id) + delete = False + + if delete: + log.info("Deleting request: %s", self.item_id) + self.emby.deleteItem(self.item_id) diff --git a/resources/lib/dialog/loginconnect.py b/resources/lib/dialog/loginconnect.py deleted file mode 100644 index 62651e4f..00000000 --- a/resources/lib/dialog/loginconnect.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import os - -import xbmcgui -import xbmcaddon - -################################################################################################## - -addon = xbmcaddon.Addon('plugin.video.emby') - -ACTION_BACK = 92 -SIGN_IN = 200 -REMIND_LATER = 201 - - -class LoginConnect(xbmcgui.WindowXMLDialog): - - - def __init__(self, *args, **kwargs): - - xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - - def __add_editcontrol(self, x, y, height, width, password=0): - - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlEdit(0,0,0,0, - label="User", - font="font10", - textColor="ff464646", - focusTexture=os.path.join(media, "button-focus.png"), - noFocusTexture=os.path.join(media, "button-focus.png"), - isPassword=password) - - control.setPosition(x,y) - control.setHeight(height) - control.setWidth(width) - - self.addControl(control) - return control - - def onInit(self): - - self.user_field = self.__add_editcontrol(685,385,40,500) - self.setFocus(self.user_field) - self.password_field = self.__add_editcontrol(685,470,40,500, password=1) - self.signin_button = self.getControl(SIGN_IN) - self.remind_button = self.getControl(REMIND_LATER) - - self.user_field.controlUp(self.remind_button) - self.user_field.controlDown(self.password_field) - self.password_field.controlUp(self.user_field) - self.password_field.controlDown(self.signin_button) - self.signin_button.controlUp(self.password_field) - self.remind_button.controlDown(self.user_field) - - def onClick(self, control): - - if control == SIGN_IN: - # Sign in to emby connect - self.user = self.user_field.getText() - __password = self.password_field.getText() - - ### REVIEW ONCE CONNECT MODULE IS MADE - self.close() - - elif control == REMIND_LATER: - # Remind me later - self.close() - - def onAction(self, action): - - if action == ACTION_BACK: - self.close() \ No newline at end of file diff --git a/resources/lib/dialogs/__init__.py b/resources/lib/dialogs/__init__.py new file mode 100644 index 00000000..b6c69bf6 --- /dev/null +++ b/resources/lib/dialogs/__init__.py @@ -0,0 +1,6 @@ +# Dummy file to make this directory a package. +from serverconnect import ServerConnect +from usersconnect import UsersConnect +from loginconnect import LoginConnect +from loginmanual import LoginManual +from servermanual import ServerManual diff --git a/resources/lib/dialogs/context.py b/resources/lib/dialogs/context.py new file mode 100644 index 00000000..1f47f625 --- /dev/null +++ b/resources/lib/dialogs/context.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +from utils import window + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +LIST = 155 +USER_IMAGE = 150 + +################################################################################################## + + +class ContextMenu(xbmcgui.WindowXMLDialog): + + _options = [] + selected_option = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_options(self, options=[]): + self._options = options + + def is_selected(self): + return True if self.selected_option else False + + def get_selected(self): + return self.selected_option + + def onInit(self): + + if window('EmbyUserImage'): + self.getControl(USER_IMAGE).setImage(window('EmbyUserImage')) + + height = 479 + (len(self._options) * 55) + log.info("options: %s", self._options) + self.list_ = self.getControl(LIST) + + for option in self._options: + self.list_.addItem(self._add_listitem(option)) + + self.background = self._add_editcontrol(730, height, 30, 450) + self.setFocus(self.list_) + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + option = self.list_.getSelectedItem() + self.selected_option = option.getLabel() + log.info('option selected: %s', self.selected_option) + + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.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) + control.setWidth(width) + + self.addControl(control) + return control + + @classmethod + def _add_listitem(cls, label): + return xbmcgui.ListItem(label) diff --git a/resources/lib/dialogs/loginconnect.py b/resources/lib/dialogs/loginconnect.py new file mode 100644 index 00000000..db7c39cc --- /dev/null +++ b/resources/lib/dialogs/loginconnect.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +SIGN_IN = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class LoginConnect(xbmcgui.WindowXMLDialog): + + _user = None + error = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_connect_manager(self, connect_manager): + self.connect_manager = connect_manager + + def is_logged_in(self): + return True if self._user else False + + def get_user(self): + return self._user + + + def onInit(self): + + self.user_field = self._add_editcontrol(725, 385, 40, 500) + self.setFocus(self.user_field) + self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1) + self.signin_button = self.getControl(SIGN_IN) + self.remind_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + + self.user_field.controlUp(self.remind_button) + self.user_field.controlDown(self.password_field) + self.password_field.controlUp(self.user_field) + self.password_field.controlDown(self.signin_button) + self.signin_button.controlUp(self.password_field) + self.remind_button.controlDown(self.user_field) + + def onClick(self, control): + + if control == SIGN_IN: + # Sign in to emby connect + self._disable_error() + + user = self.user_field.getText() + password = self.password_field.getText() + + if not user or not password: + # Display error + self._error(ERROR['Empty'], lang(30608)) + log.error("Username or password cannot be null") + + elif self._login(user, password): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if (self.error == ERROR['Empty'] + and self.user_field.getText() and self.password_field.getText()): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ff525252", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png"), + isPassword=password) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _login(self, username, password): + + result = self.connect_manager.loginToConnect(username, password) + if result is False: + self._error(ERROR['Invalid'], lang(33009)) + return False + else: + self._user = result + return True + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/loginmanual.py b/resources/lib/dialogs/loginmanual.py new file mode 100644 index 00000000..7db1adcf --- /dev/null +++ b/resources/lib/dialogs/loginmanual.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +import read_embyserver as embyserver +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +SIGN_IN = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class LoginManual(xbmcgui.WindowXMLDialog): + + _user = None + error = None + username = None + + + def __init__(self, *args, **kwargs): + + self.emby = embyserver.Read_EmbyServer() + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def is_logged_in(self): + return True if self._user else False + + def set_server(self, server): + self.server = server + + def set_user(self, user): + self.username = user or {} + + def get_user(self): + return self._user + + def onInit(self): + + self.signin_button = self.getControl(SIGN_IN) + self.cancel_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + self.user_field = self._add_editcontrol(725, 400, 40, 500) + self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1) + + if self.username: + self.user_field.setText(self.username) + self.setFocus(self.password_field) + else: + self.setFocus(self.user_field) + + self.user_field.controlUp(self.cancel_button) + self.user_field.controlDown(self.password_field) + self.password_field.controlUp(self.user_field) + self.password_field.controlDown(self.signin_button) + self.signin_button.controlUp(self.password_field) + self.cancel_button.controlDown(self.user_field) + + def onClick(self, control): + + if control == SIGN_IN: + # Sign in to emby connect + self._disable_error() + + user = self.user_field.getText() + password = self.password_field.getText() + + if not user: + # Display error + self._error(ERROR['Empty'], lang(30613)) + log.error("Username cannot be null") + + elif self._login(user, password): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if self.error == ERROR['Empty'] and self.user_field.getText(): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ff525252", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png"), + isPassword=password) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _login(self, username, password): + + result = self.emby.loginUser(self.server, username, password) + if not result: + self._error(ERROR['Invalid'], lang(33009)) + return False + else: + self._user = result + return True + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/serverconnect.py b/resources/lib/dialogs/serverconnect.py new file mode 100644 index 00000000..541ca6f9 --- /dev/null +++ b/resources/lib/dialogs/serverconnect.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui + +import connect.connectionmanager as connectionmanager +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +CONN_STATE = connectionmanager.ConnectionState +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +USER_IMAGE = 150 +USER_NAME = 151 +LIST = 155 +CANCEL = 201 +MESSAGE_BOX = 202 +MESSAGE = 203 +BUSY = 204 +EMBY_CONNECT = 205 +MANUAL_SERVER = 206 + +################################################################################################## + + +class ServerConnect(xbmcgui.WindowXMLDialog): + + username = "" + user_image = None + servers = [] + + _selected_server = None + _connect_login = False + _manual_server = False + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_args(self, **kwargs): + # connect_manager, username, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + def is_server_selected(self): + return True if self._selected_server else False + + def get_server(self): + return self._selected_server + + def is_connect_login(self): + return self._connect_login + + def is_manual_server(self): + return self._manual_server + + + def onInit(self): + + self.message = self.getControl(MESSAGE) + self.message_box = self.getControl(MESSAGE_BOX) + self.busy = self.getControl(BUSY) + self.list_ = self.getControl(LIST) + + for server in self.servers: + server_type = "wifi" if server.get('ExchangeToken') else "network" + self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) + + self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8'))) + + if self.user_image is not None: + self.getControl(USER_IMAGE).setImage(self.user_image) + + if not self.emby_connect: # Change connect user + self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]") + + if self.servers: + self.setFocus(self.list_) + + @classmethod + def _add_listitem(cls, label, server_id, server_type): + + item = xbmcgui.ListItem(label) + item.setProperty('id', server_id) + item.setProperty('server_type', server_type) + + return item + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + server = self.list_.getSelectedItem() + selected_id = server.getProperty('id') + log.info('Server Id selected: %s', selected_id) + + if self._connect_server(selected_id): + self.message_box.setVisibleCondition('False') + self.close() + + def onClick(self, control): + + if control == EMBY_CONNECT: + self.connect_manager.clearData() + self._connect_login = True + self.close() + + elif control == MANUAL_SERVER: + self._manual_server = True + self.close() + + elif control == CANCEL: + self.close() + + def _connect_server(self, server_id): + + server = self.connect_manager.getServerInfo(server_id) + self.message.setLabel("%s %s..." % (lang(30610), server['Name'])) + self.message_box.setVisibleCondition('True') + self.busy.setVisibleCondition('True') + result = self.connect_manager.connectToServer(server) + + if result['State'] == CONN_STATE['Unavailable']: + self.busy.setVisibleCondition('False') + self.message.setLabel(lang(30609)) + return False + else: + xbmc.sleep(1000) + self._selected_server = result['Servers'][0] + return True diff --git a/resources/lib/dialogs/servermanual.py b/resources/lib/dialogs/servermanual.py new file mode 100644 index 00000000..d54199eb --- /dev/null +++ b/resources/lib/dialogs/servermanual.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +import connect.connectionmanager as connectionmanager +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +CONN_STATE = connectionmanager.ConnectionState +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +CONNECT = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class ServerManual(xbmcgui.WindowXMLDialog): + + _server = None + error = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_connect_manager(self, connect_manager): + self.connect_manager = connect_manager + + def is_connected(self): + return True if self._server else False + + def get_server(self): + return self._server + + def onInit(self): + + self.connect_button = self.getControl(CONNECT) + self.cancel_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + self.host_field = self._add_editcontrol(725, 400, 40, 500) + self.port_field = self._add_editcontrol(725, 525, 40, 500) + + self.port_field.setText('8096') + self.setFocus(self.host_field) + + self.host_field.controlUp(self.cancel_button) + self.host_field.controlDown(self.port_field) + self.port_field.controlUp(self.host_field) + self.port_field.controlDown(self.connect_button) + self.connect_button.controlUp(self.port_field) + self.cancel_button.controlDown(self.host_field) + + def onClick(self, control): + + if control == CONNECT: + # Sign in to emby connect + self._disable_error() + + server = self.host_field.getText() + port = self.port_field.getText() + + if not server or not port: + # Display error + self._error(ERROR['Empty'], lang(30617)) + log.error("Server or port cannot be null") + + elif self._connect_to_server(server, port): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText(): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ffc2c2c2", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png")) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _connect_to_server(self, server, port): + + server_address = "%s:%s" % (server, port) + self._message("%s %s..." % (lang(30610), server_address)) + result = self.connect_manager.connectToAddress(server_address) + + if result['State'] == CONN_STATE['Unavailable']: + self._message(lang(30609)) + return False + else: + self._server = result['Servers'][0] + return True + + def _message(self, message): + + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py new file mode 100644 index 00000000..770b0a2c --- /dev/null +++ b/resources/lib/dialogs/usersconnect.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +LIST = 155 +MANUAL = 200 +CANCEL = 201 + +################################################################################################## + + +class UsersConnect(xbmcgui.WindowXMLDialog): + + _user = None + _manual_login = False + + + def __init__(self, *args, **kwargs): + + self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_server(self, server): + self.server = server + + def set_users(self, users): + self.users = users + + def is_user_selected(self): + return True if self._user else False + + def get_user(self): + return self._user + + def is_manual_login(self): + return self._manual_login + + + def onInit(self): + + self.list_ = self.getControl(LIST) + for user in self.users: + user_image = ("userflyoutdefault2.png" if 'PrimaryImageTag' not in user + else self._get_user_artwork(user['Id'], 'Primary')) + self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) + + self.setFocus(self.list_) + + def _add_listitem(self, label, user_id, user_image): + + item = xbmcgui.ListItem(label) + item.setProperty('id', user_id) + if self.kodi_version > 15: + item.setArt({'Icon': user_image}) + else: + item.setIconImage(user_image) + + return item + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + user = self.list_.getSelectedItem() + selected_id = user.getProperty('id') + log.info('User Id selected: %s', selected_id) + + for user in self.users: + if user['Id'] == selected_id: + self._user = user + break + + self.close() + + def onClick(self, control): + + if control == MANUAL: + self._manual_login = True + self.close() + + elif control == CANCEL: + self.close() + + def _get_user_artwork(self, user_id, item_type): + # Load user information set by UserClient + return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 47a5db59..57373204 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -3,14 +3,14 @@ ################################################################################################## import json -import requests import logging +import requests -import xbmc import xbmcgui import clientinfo -from utils import window, settings +import connect.connectionmanager as connectionmanager +from utils import window, settings, language as lang ################################################################################################## @@ -24,52 +24,71 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## -class DownloadUtils(): +class DownloadUtils(object): # Borg - multiple instances, shared state _shared_state = {} - clientInfo = clientinfo.ClientInfo() # Requests session - s = None + session = {} + session_requests = None + servers = {} # Multi server setup default_timeout = 30 def __init__(self): self.__dict__ = self._shared_state + self.client_info = clientinfo.ClientInfo() - def setUsername(self, username): + def set_session(self, **kwargs): # Reserved for userclient only - self.username = username - log.debug("Set username: %s" % username) + info = {} + for key in kwargs: + info[key] = kwargs[key] - def setUserId(self, userId): + self.session.update(info) + window('emby_server.json', value=self.session) + + log.debug("Set info for server %s: %s", self.session['ServerId'], self.session) + + def add_server(self, server, ssl): # Reserved for userclient only - self.userId = userId - log.debug("Set userId: %s" % userId) + server_id = server['Id'] + info = { + 'UserId': server['UserId'], + 'Server': connectionmanager.getServerAddress(server, server['LastConnectionMode']), + 'Token': server['AccessToken'], + 'SSL': ssl + } + for server_info in self.servers: + if server_info == server_id: + server_info.update(info) + # Set window prop + self._set_server_properties(server_id, server['Name'], info) + log.info("updating %s to available servers: %s", server_id, self.servers) + break + else: + self.servers[server_id] = info + self._set_server_properties(server_id, server['Name'], json.dumps(info)) + log.info("adding %s to available servers: %s", server_id, self.servers) - def setServer(self, server): + def reset_server(self, server_id): # Reserved for userclient only - self.server = server - log.debug("Set server: %s" % server) + for server in self.servers: + if server['ServerId'] == server_id: + self.servers.pop(server) + window('emby_server%s.json' % server_id, clear=True) + window('emby_server%s.name' % server_id, clear=True) + log.info("removing %s from available servers", server_id) - def setToken(self, token): - # Reserved for userclient only - self.token = token - log.debug("Set token: %s" % token) - - def setSSL(self, ssl, sslclient): - # Reserved for userclient only - self.sslverify = ssl - self.sslclient = sslclient - log.debug("Verify SSL host certificate: %s" % ssl) - log.debug("SSL client side certificate: %s" % sslclient) - - - def postCapabilities(self, deviceId): + @staticmethod + def _set_server_properties(server_id, name, info): + window('emby_server%s.json' % server_id, value=info) + window('emby_server%s.name' % server_id, value=name) + def post_capabilities(self, device_id): # Post settings to session url = "{server}/emby/Sessions/Capabilities/Full?format=json" data = { @@ -90,171 +109,126 @@ class DownloadUtils(): ) } - log.debug("Capabilities URL: %s" % url) - log.debug("Postdata: %s" % data) - self.downloadUrl(url, postBody=data, action_type="POST") - log.debug("Posted capabilities to %s" % self.server) + log.debug("Posted capabilities to %s", self.session['Server']) # Attempt at getting sessionId - url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId + url = "{server}/emby/Sessions?DeviceId=%s&format=json" % device_id result = self.downloadUrl(url) try: - sessionId = result[0]['Id'] + session_id = result[0]['Id'] except (KeyError, TypeError): - log.info("Failed to retrieve sessionId.") + log.error("Failed to retrieve the session id.") else: - log.debug("Session: %s" % result) - log.info("SessionId: %s" % sessionId) - window('emby_sessionId', value=sessionId) + log.info("SessionId: %s", session_id) + window('emby_sessionId', value=session_id) # Post any permanent additional users - additionalUsers = settings('additionalUsers') - if additionalUsers: + additional_users = settings('additionalUsers') + if additional_users: - additionalUsers = additionalUsers.split(',') - log.info("List of permanent users added to the session: %s" % additionalUsers) + additional_users = additional_users.split(',') + log.info("List of permanent users added to the session: %s", additional_users) # Get the user list from server to get the userId url = "{server}/emby/Users?format=json" result = self.downloadUrl(url) - for additional in additionalUsers: - addUser = additional.decode('utf-8').lower() + for additional in additional_users: + add_user = additional.decode('utf-8').lower() # Compare to server users to list of permanent additional users for user in result: username = user['Name'].lower() - if username in addUser: - userId = user['Id'] - url = ( - "{server}/emby/Sessions/%s/Users/%s?format=json" - % (sessionId, userId) - ) + if username in add_user: + user_id = user['Id'] + url = ("{server}/emby/Sessions/%s/Users/%s?format=json" + % (session_id, user_id)) self.downloadUrl(url, postBody={}, action_type="POST") - - def startSession(self): - - self.deviceId = self.clientInfo.getDeviceId() - + def start_session(self): # User is identified from this point # Attach authenticated header to the session - verify = False - header = self.getHeader() - - # If user enabled host certificate verification - try: - verify = self.sslverify - if self.sslclient is not None: - verify = self.sslclient - except: - log.info("Could not load SSL settings.") - - # Start session - self.s = requests.Session() - self.s.headers = header - self.s.verify = verify + session = requests.Session() + session.headers = self.get_header() + session.verify = self.session['SSL'] # Retry connections to the server - self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) - self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) + session.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) + session.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) + self.session_requests = session - log.info("Requests session started on: %s" % self.server) + log.info("requests session started on: %s", self.session['Server']) - def stopSession(self): + def stop_session(self): try: - self.s.close() - except Exception: - log.warn("Requests session could not be terminated.") + self.session_requests.close() + except Exception as error: + log.error(error) + log.warn("requests session could not be terminated") - def getHeader(self, authenticate=True): + def get_header(self, server_id=None, authenticate=True): - deviceName = self.clientInfo.getDeviceName() - deviceName = deviceName.encode('utf-8') - deviceId = self.clientInfo.getDeviceId() - version = self.clientInfo.getVersion() + device_name = self.client_info.get_device_name().encode('utf-8') + device_id = self.client_info.get_device_id() + version = self.client_info.get_version() if authenticate: + + user = self._get_session_info(server_id) + user_id = user['UserId'] + token = user['Token'] + auth = ( 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' - % (self.userId, deviceName, deviceId, version)) + % (user_id, device_name, device_id, version) + ) header = { - - 'Content-type': 'application/json', - 'Accept-encoding': 'gzip', - 'Accept-Charset': 'UTF-8,*', 'Authorization': auth, - 'X-MediaBrowser-Token': self.token + 'X-MediaBrowser-Token': token } else: - # If user is not authenticated auth = ( 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' - % (deviceName, deviceId, version)) - header = { - - 'Content-type': 'application/json', - 'Accept-encoding': 'gzip', - 'Accept-Charset': 'UTF-8,*', - 'Authorization': auth - } + % (device_name, device_id, version) + ) + header = {'Authorization': auth} + header.update({ + 'Content-type': 'application/json', + 'Accept-encoding': 'gzip', + 'Accept-Charset': 'UTF-8,*', + }) return header def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None, - authenticate=True): + authenticate=True, server_id=None): log.debug("===== ENTER downloadUrl =====") - - session = requests + kwargs = {} default_link = "" try: - if authenticate: - - if self.s is not None: - session = self.s - else: - # request session does not exists - # Get user information - self.userId = window('emby_currUser') - self.server = window('emby_server%s' % self.userId) - self.token = window('emby_accessToken%s' % self.userId) - verifyssl = False - - # IF user enables ssl verification - if settings('sslverify') == "true": - verifyssl = True - if settings('sslcert') != "None": - verifyssl = settings('sslcert') - - kwargs.update({ - 'verify': verifyssl, - 'headers': self.getHeader() - }) - - # Replace for the real values - url = url.replace("{server}", self.server) - url = url.replace("{UserId}", self.userId) - - else: # User is not authenticated - # If user enables ssl verification - try: - verifyssl = self.sslverify - if self.sslclient is not None: - verifyssl = self.sslclient - except AttributeError: - verifyssl = False + # Ensure server info is loaded + self._ensure_server(server_id) + server = self.session if server_id is None else self.servers[server_id] + if server_id is None and self.session_requests is not None: # Main server + session = self.session_requests + else: + session = requests kwargs.update({ - 'verify': verifyssl, - 'headers': self.getHeader(authenticate=False) + 'verify': server['SSL'], + 'headers': self.get_header(server_id, authenticate) }) + # Replace for the real values + url = url.replace("{server}", server['Server']) + url = url.replace("{UserId}", server['UserId']) + ##### PREPARE REQUEST ##### kwargs.update({ 'url': url, @@ -265,95 +239,128 @@ class DownloadUtils(): ##### THE RESPONSE ##### log.debug(kwargs) - r = self._requests(action_type, session, **kwargs) + response = self._requests(action_type, session, **kwargs) + #response = requests.get('http://httpbin.org/status/400') - if r.status_code == 204: + if response.status_code == 204: # No body in the response log.debug("====== 204 Success ======") # Read response to release connection - r.content + response.content - elif r.status_code == requests.codes.ok: + elif response.status_code == requests.codes.ok: try: # UNICODE - JSON object - r = r.json() + response = response.json() log.debug("====== 200 Success ======") - log.debug("Response: %s" % r) - return r + log.debug("Response: %s", response) + return response - except: - if r.headers.get('content-type') != "text/html": - log.info("Unable to convert the response for: %s" % url) + except Exception: + if response.headers.get('content-type') != "text/html": + log.info("Unable to convert the response for: %s", url) else: # Bad status code - log.error("=== Bad status response: %s ===" % r.status_code) - r.raise_for_status() + log.error("=== Bad status response: %s ===", response.status_code) + response.raise_for_status() ##### EXCEPTIONS ##### - except requests.exceptions.ConnectionError as e: + except requests.exceptions.SSLError as error: + log.error("invalid SSL certificate for: %s", url) + + except requests.exceptions.ConnectTimeout as error: + log.error("Server timeout at: %s", url) + + except requests.exceptions.ConnectionError as error: # Make the addon aware of status if window('emby_online') != "false": - log.warn("Server unreachable at: %s" % url) + log.error("Server unreachable at: %s", url) window('emby_online', value="false") - except requests.exceptions.ConnectTimeout as e: - log.warn("Server timeout at: %s" % url) + except requests.exceptions.HTTPError as error: - except requests.exceptions.HTTPError as e: + if response.status_code == 400: + log.error("Malformed request: %s", error) + raise Warning('400') - if r.status_code == 401: + if response.status_code == 401: # Unauthorized status = window('emby_serverStatus') - if 'X-Application-Error-Code' in r.headers: + if 'X-Application-Error-Code' in response.headers: # Emby server errors - if r.headers['X-Application-Error-Code'] == "ParentalControl": + if response.headers['X-Application-Error-Code'] == "ParentalControl": # Parental control - access restricted + if status != "restricted": + xbmcgui.Dialog().notification(heading=lang(29999), + message="Access restricted.", + icon=xbmcgui.NOTIFICATION_ERROR, + time=5000) window('emby_serverStatus', value="restricted") - xbmcgui.Dialog().notification( - heading="Emby server", - message="Access restricted.", - icon=xbmcgui.NOTIFICATION_ERROR, - time=5000) - return False + raise Warning('restricted') - elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException": + elif (response.headers['X-Application-Error-Code'] == + "UnauthorizedAccessException"): # User tried to do something his emby account doesn't allow pass elif status not in ("401", "Auth"): # Tell userclient token has been revoked. window('emby_serverStatus', value="401") - log.warn("HTTP Error: %s" % e) - xbmcgui.Dialog().notification( - heading="Error connecting", - message="Unauthorized.", - icon=xbmcgui.NOTIFICATION_ERROR) - return 401 + log.error("HTTP Error: %s", error) + xbmcgui.Dialog().notification(heading="Error connecting", + message="Unauthorized.", + icon=xbmcgui.NOTIFICATION_ERROR) + raise Warning('401') - elif r.status_code in (301, 302): - # Redirects - pass - elif r.status_code == 400: - # Bad requests - pass - - except requests.exceptions.SSLError as e: - log.warn("Invalid SSL certificate for: %s" % url) - - except requests.exceptions.RequestException as e: - log.warn("Unknown error connecting to: %s" % url) + except requests.exceptions.RequestException as error: + log.error("unknown error connecting to: %s", url) return default_link - def _requests(self, action, session=requests, **kwargs): + def _ensure_server(self, server_id=None): + + if server_id is None and self.session_requests is None: + if not self.session: + server = self._get_session_info() + self.session = server + + elif server_id and server_id not in self.servers: + if server_id not in self.servers: + server = self._get_session_info(server_id) + self.servers[server_id] = server + + return True + + @classmethod + def _get_session_info(cls, server_id=None): + + info = { + 'UserId': "", + 'Server': "", + 'Token': "", + 'SSL': False + } + + if server_id is None: # Main server + server = window('emby_server.json') + else: # Other connect servers + server = window('emby_server%s.json' % server_id) + + if server: + info.update(server) + + return info + + @classmethod + def _requests(cls, action, session, **kwargs): if action == "GET": - r = session.get(**kwargs) + response = session.get(**kwargs) elif action == "POST": - r = session.post(**kwargs) + response = session.post(**kwargs) elif action == "DELETE": - r = session.delete(**kwargs) + response = session.delete(**kwargs) - return r \ No newline at end of file + return response diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py index 9220fa5d..f3d32aaa 100644 --- a/resources/lib/embydb_functions.py +++ b/resources/lib/embydb_functions.py @@ -20,6 +20,22 @@ class Embydb_Functions(): self.embycursor = embycursor + def get_version(self, version=None): + + if version is not None: + self.embycursor.execute("DELETE FROM version") + query = "INSERT INTO version(idVersion) VALUES (?)" + self.embycursor.execute(query, (version,)) + else: + query = "SELECT idVersion FROM version" + self.embycursor.execute(query) + try: + version = self.embycursor.fetchone()[0] + except TypeError: + pass + + return version + def getViews(self): views = [] @@ -155,11 +171,22 @@ class Embydb_Functions(): self.embycursor.execute(query, (mediafolderid,)) return self.embycursor.fetchall() + def get_item_by_view(self, view_id): + + query = ' '.join(( + + "SELECT emby_id", + "FROM emby", + "WHERE media_folder = ?" + )) + self.embycursor.execute(query, (view_id,)) + return self.embycursor.fetchall() + def getItem_byKodiId(self, kodiid, mediatype): query = ' '.join(( - "SELECT emby_id, parent_id", + "SELECT emby_id, parent_id, media_folder", "FROM emby", "WHERE kodi_id = ?", "AND media_type = ?" @@ -191,7 +218,7 @@ class Embydb_Functions(): self.embycursor.execute(query, (parentid, mediatype,)) return self.embycursor.fetchall() - def getChecksum(self, mediatype): + def get_checksum(self, mediatype): query = ' '.join(( @@ -202,6 +229,18 @@ class Embydb_Functions(): self.embycursor.execute(query, (mediatype,)) return self.embycursor.fetchall() + def get_checksum_by_view(self, media_type, view_id): + + query = ' '.join(( + + "SELECT emby_id, checksum", + "FROM emby", + "WHERE emby_type = ?", + "AND media_folder = ?" + )) + self.embycursor.execute(query, (media_type, view_id,)) + return self.embycursor.fetchall() + def getMediaType_byId(self, embyid): query = ' '.join(( diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index e532e5e2..9cc3ce0e 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -5,6 +5,7 @@ import json import logging import os +import shutil import sys import urlparse @@ -17,6 +18,7 @@ import xbmcplugin import artwork import utils import clientinfo +import connectmanager import downloadutils import librarysync import read_embyserver as embyserver @@ -25,7 +27,7 @@ import playlist import playbackutils as pbutils import playutils import api -from utils import window, settings, language as lang +from utils import window, settings, dialog, language as lang ################################################################################################# @@ -76,23 +78,15 @@ def doMainListing(): ''' because we do not use seperate entrypoints for each content type, we need to figure out which items to show in each listing. for now we just only show picture nodes in the picture library - video nodes in the video library and all nodes in any other window ''' - - '''if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and node == "photos": - addDirectoryItem(label, path) - elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") - and node != "photos": - addDirectoryItem(label, path) - elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | - Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"): - addDirectoryItem(label, path)''' + video nodes in the video library and all nodes in any other window + ''' if path: if xbmc.getCondVisibility("Window.IsActive(Pictures)") and node == "photos": addDirectoryItem(label, path) - elif xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and node != "photos": + elif xbmc.getCondVisibility("Window.IsActive(Videos)") and node != "photos": addDirectoryItem(label, path) - elif not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"): + elif not xbmc.getCondVisibility("Window.IsActive(Videos) | Window.IsActive(Pictures) | Window.IsActive(Music)"): addDirectoryItem(label, path) # experimental live tv nodes @@ -102,7 +96,14 @@ def doMainListing(): addDirectoryItem(lang(33052), "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root") - # some extra entries for settings and stuff. TODO --> localize the labels + ''' + TODO: Create plugin listing for servers + servers = window('emby_servers.json') + if servers: + for server in servers: + log.info(window('emby_server%s.name' % server)) + addDirectoryItem(window('emby_server%s.name' % server), "plugin://plugin.video.emby/?mode=%s" % server)''' + addDirectoryItem(lang(30517), "plugin://plugin.video.emby/?mode=passwords") addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings") addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser") @@ -112,9 +113,82 @@ def doMainListing(): addDirectoryItem(lang(33058), "plugin://plugin.video.emby/?mode=reset") addDirectoryItem(lang(33059), "plugin://plugin.video.emby/?mode=texturecache") addDirectoryItem(lang(33060), "plugin://plugin.video.emby/?mode=thememedia") + + if settings('backupPath'): + addDirectoryItem(lang(33092), "plugin://plugin.video.emby/?mode=backup") xbmcplugin.endOfDirectory(int(sys.argv[1])) +def emby_connect(): + + # Login user to emby connect + connect = connectmanager.ConnectManager() + try: + connectUser = connect.login_connect() + except RuntimeError: + return + else: + user = connectUser['User'] + token = connectUser['AccessToken'] + username = user['Name'] + dialog(type_="notification", + heading="{emby}", + message="%s %s" % (lang(33000), username.decode('utf-8')), + icon=user.get('ImageUrl') or "{emby}", + time=2000, + sound=False) + + settings('connectUsername', value=username) + +def emby_backup(): + # Create a backup at specified location + path = settings('backupPath') + + # filename + default_value = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], + xbmc.getInfoLabel('System.Date(dd-mm-yy)')) + filename = dialog(type_="input", + heading=lang(33089), + defaultt=default_value) + if not filename: + return + + backup = os.path.join(path, filename) + log.info("Backup: %s", backup) + + # Create directory + if xbmcvfs.exists(backup+"\\"): + log.info("Existing directory!") + if not dialog(type_="yesno", + heading="{emby}", + line1=lang(33090)): + return emby_backup() + shutil.rmtree(backup) + + # Addon_data + shutil.copytree(src=xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby").decode('utf-8'), + dst=os.path.join(backup, "addon_data", "plugin.video.emby")) + + # Database files + database = os.path.join(backup, "Database") + xbmcvfs.mkdir(database) + + # Emby database + shutil.copy(src=xbmc.translatePath("special://database/emby.db").decode('utf-8'), + dst=database) + # Videos database + shutil.copy(src=utils.getKodiVideoDBPath(), + dst=database) + # Music database + if settings('enableMusic') == "true": + shutil.copy(src=utils.getKodiMusicDBPath(), + dst=database) + + dialog(type_="ok", + heading="{emby}", + line1="%s: %s" % (lang(33091), backup)) + ##### Generate a new deviceId def resetDeviceId(): @@ -123,7 +197,7 @@ def resetDeviceId(): deviceId_old = window('emby_deviceId') try: window('emby_deviceId', clear=True) - deviceId = clientinfo.ClientInfo().getDeviceId(reset=True) + deviceId = clientinfo.ClientInfo().get_device_id(reset=True) except Exception as e: log.error("Failed to generate a new device Id: %s" % e) dialog.ok( @@ -185,11 +259,15 @@ def deleteItem(): ##### ADD ADDITIONAL USERS ##### def addUser(): + if window('emby_online') != "true": + log.info("server is offline") + return + doUtils = downloadutils.DownloadUtils() art = artwork.Artwork() clientInfo = clientinfo.ClientInfo() - deviceId = clientInfo.getDeviceId() - deviceName = clientInfo.getDeviceName() + deviceId = clientInfo.get_device_id() + deviceName = clientInfo.get_device_name() userid = window('emby_currUser') dialog = xbmcgui.Dialog() @@ -290,16 +368,21 @@ def addUser(): break window('EmbyAdditionalUserImage.%s' % i, clear=True) - url = "{server}/emby/Sessions?DeviceId=%s" % deviceId + url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId result = doUtils.downloadUrl(url) - additionalUsers = result[0]['AdditionalUsers'] + try: + additionalUsers = result[0]['AdditionalUsers'] + except (KeyError, TypeError) as error: + log.error(error) + additionaluser = [] + count = 0 for additionaluser in additionalUsers: userid = additionaluser['UserId'] url = "{server}/emby/Users/%s?format=json" % userid result = doUtils.downloadUrl(url) window('EmbyAdditionalUserImage.%s' % count, - value=art.getUserArtwork(result['Id'], 'Primary')) + value=art.get_user_artwork(result['Id'], 'Primary')) window('EmbyAdditionalUserPosition.%s' % userid, value=str(count)) count +=1 @@ -326,9 +409,7 @@ def getThemeMedia(): xbmcvfs.mkdir(library) # Set custom path for user - tvtunes_path = xbmc.translatePath( - "special://profile/addon_data/script.tvtunes/").decode('utf-8') - if xbmcvfs.exists(tvtunes_path): + if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): tvtunes = xbmcaddon.Addon(id="script.tvtunes") tvtunes.setSetting('custom_path_enable', "true") tvtunes.setSetting('custom_path', library) @@ -461,6 +542,10 @@ def getThemeMedia(): ##### REFRESH EMBY PLAYLISTS ##### def refreshPlaylist(): + if window('emby_online') != "true": + log.info("server is offline") + return + lib = librarysync.LibrarySync() dialog = xbmcgui.Dialog() try: @@ -478,7 +563,7 @@ def refreshPlaylist(): sound=False) except Exception as e: - log.error("Refresh playlists/nodes failed: %s" % e) + log.exception("Refresh playlists/nodes failed: %s" % e) dialog.notification( heading=lang(29999), message=lang(33070), @@ -544,6 +629,9 @@ def BrowseContent(viewname, browse_type="", folderid=""): listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending") elif filter_type == "recommended": listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") + elif folderid == "favepisodes": + xbmcplugin.setContent(int(sys.argv[1]), 'episodes') + listing = emby.getFilteredSection(None, itemtype="Episode", sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") elif filter_type == "sets": listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite") else: @@ -588,7 +676,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils. li.setProperty("embyid",itemid) - allart = art.getAllArtwork(item) + allart = art.get_all_artwork(item) if item["Type"] == "Photo": #listitem setup for pictures... @@ -601,16 +689,16 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils. li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title}) li.setThumbnailImage(img_path) - li.setProperty("plot",API.getOverview()) + li.setProperty("plot",API.get_overview()) li.setIconImage('DefaultPicture.png') else: #normal video items li.setProperty('IsPlayable', 'true') path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id")) li.setProperty("path",path) - genre = API.getGenres() + genre = API.get_genres() overlay = 0 - userdata = API.getUserData() + userdata = API.get_userdata() runtime = item.get("RunTimeTicks",0)/ 10000000.0 seektime = userdata['Resume'] if seektime: @@ -625,7 +713,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils. playcount = 0 rating = item.get('CommunityRating') - if not rating: rating = userdata['UserRating'] + if not rating: rating = 0 # Populate the extradata list and artwork extradata = { @@ -635,7 +723,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils. 'genre': genre, 'playcount': str(playcount), 'title': title, - 'plot': API.getOverview(), + 'plot': API.get_overview(), 'Overlay': str(overlay), 'duration': runtime } @@ -652,7 +740,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils. else: pbutils.PlaybackUtils(item).setArtwork(li) - mediastreams = API.getMediaStreams() + mediastreams = API.get_media_streams() videostreamFound = False if mediastreams: for key, value in mediastreams.iteritems(): @@ -722,6 +810,12 @@ def createListItem(item): 'Playcount': item['playcount'] } + if "episodeid" in item: + # Listitem of episode + metadata['mediatype'] = "episode" + metadata['dbid'] = item['episodeid'] + + # TODO: Review once Krypton is RC - probably no longer needed if there's dbid if "episode" in item: episode = item['episode'] metadata['Episode'] = episode @@ -769,7 +863,7 @@ def createListItem(item): for key, value in item['streamdetails'].iteritems(): for stream in value: li.addStreamInfo(key, stream) - + return li ##### GET NEXTUP EPISODES FOR TAGNAME ##### @@ -1064,7 +1158,7 @@ def getExtraFanArt(embyId,embyPath): xbmcvfs.mkdirs(fanartDir) item = emby.getItem(embyId) if item: - backdrops = art.getAllArtwork(item)['Backdrop'] + backdrops = art.get_all_artwork(item)['Backdrop'] tags = item['BackdropImageTags'] count = 0 for backdrop in backdrops: diff --git a/resources/lib/ga_client.py b/resources/lib/ga_client.py new file mode 100644 index 00000000..d4b476d6 --- /dev/null +++ b/resources/lib/ga_client.py @@ -0,0 +1,164 @@ +import sys +import os +import traceback +import requests +import logging +import clientinfo +import md5 +import xbmc +import platform +import xbmcgui +from utils import window, settings, language as lang + +log = logging.getLogger("EMBY."+__name__) + +# for info on the metrics that can be sent to Google Analytics +# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#events + +class GoogleAnalytics(): + + testing = False + + def __init__(self): + + client_info = clientinfo.ClientInfo() + self.version = client_info.get_version() + self.device_id = client_info.get_device_id() + + # user agent string, used for OS and Kodi version identification + kodi_ver = xbmc.getInfoLabel("System.BuildVersion") + if(not kodi_ver): + kodi_ver = "na" + kodi_ver = kodi_ver.strip() + if(kodi_ver.find(" ") > 0): + kodi_ver = kodi_ver[0:kodi_ver.find(" ")] + self.userAgent = "Kodi/" + kodi_ver + " (" + self.getUserAgentOS() + ")" + + # Use set user name + self.user_name = settings('username') or settings('connectUsername') or 'None' + + # use md5 for client and user for analytics + self.device_id = md5.new(self.device_id).hexdigest() + self.user_name = md5.new(self.user_name).hexdigest() + + # resolution + self.screen_mode = xbmc.getInfoLabel("System.ScreenMode") + self.screen_height = xbmc.getInfoLabel("System.ScreenHeight") + self.screen_width = xbmc.getInfoLabel("System.ScreenWidth") + + self.lang = xbmc.getInfoLabel("System.Language") + + def getUserAgentOS(self): + + if xbmc.getCondVisibility('system.platform.osx'): + return "Mac OS X" + elif xbmc.getCondVisibility('system.platform.ios'): + return "iOS" + elif xbmc.getCondVisibility('system.platform.windows'): + return "Windows NT" + elif xbmc.getCondVisibility('system.platform.android'): + return "Android" + elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): + return "Linux Rpi" + elif xbmc.getCondVisibility('system.platform.linux'): + return "Linux" + else: + return "Other" + + def formatException(self): + exc_type, exc_obj, exc_tb = sys.exc_info() + + latestStackFrame = None + allStackFrames = traceback.extract_tb(exc_tb) + if(len(allStackFrames) > 0): + latestStackFrame = allStackFrames[-1] + log.error(str(latestStackFrame)) + + errorType = "NA" + errorFile = "NA" + + if(latestStackFrame != None): + fileName = os.path.split(latestStackFrame[0])[1] + + codeLine = "NA" + if(len(latestStackFrame) > 3 and latestStackFrame[3] != None): + codeLine = latestStackFrame[3].strip() + + errorFile = "%s:%s(%s)(%s)" % (fileName, latestStackFrame[1], exc_obj.message, codeLine) + errorFile = errorFile[0:499] + errorType = "%s" % (exc_type.__name__) + log.error(errorType + " - " + errorFile) + + del(exc_type, exc_obj, exc_tb) + + return errorType, errorFile + + def getBaseData(self): + + # all the data we can send to Google Analytics + data = {} + data['v'] = '1' + data['tid'] = 'UA-85356267-1' # tracking id, this is the account ID + + data['ds'] = 'plugin' # data source + + data['an'] = 'Kodi4Emby' # App Name + data['aid'] = '1' # App ID + data['av'] = self.version # App Version + #data['aiid'] = '1.1' # App installer ID + + data['cid'] = self.device_id # Client ID + #data['uid'] = self.user_name # User ID + + data['ua'] = self.userAgent # user agent string + + # add width and height, only add if full screen + if(self.screen_mode.lower().find("window") == -1): + data['sr'] = str(self.screen_width) + "x" + str(self.screen_height) + + data["ul"] = self.lang + + return data + + def sendScreenView(self, name): + + data = self.getBaseData() + data['t'] = 'screenview' # action type + data['cd'] = name + + self.sendData(data) + + def sendEventData(self, eventCategory, eventAction, eventLabel=None): + + data = self.getBaseData() + data['t'] = 'event' # action type + data['ec'] = eventCategory # Event Category + data['ea'] = eventAction # Event Action + + if(eventLabel != None): + data['el'] = eventLabel # Event Label + + self.sendData(data) + + def sendData(self, data): + + log.info("GA: " + str(data)) + + if(settings('metricLogging') == "false"): + return + + if(self.testing): + url = "https://www.google-analytics.com/debug/collect" # test URL + else: + url = "https://www.google-analytics.com/collect" # prod URL + + try: + r = requests.post(url, data) + except Exception as error: + log.error(error) + + if(self.testing): + log.info("GA: " + r.text.encode('utf-8')) + + + \ No newline at end of file diff --git a/resources/lib/image_cache_thread.py b/resources/lib/image_cache_thread.py index ffed967b..dbaa06e0 100644 --- a/resources/lib/image_cache_thread.py +++ b/resources/lib/image_cache_thread.py @@ -3,8 +3,8 @@ ################################################################################################# import logging -import requests import threading +import requests ################################################################################################# @@ -12,49 +12,49 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################# -class image_cache_thread(threading.Thread): +class ImageCacheThread(threading.Thread): + + url_to_process = None + is_finished = False - urlToProcess = None - isFinished = False - xbmc_host = "" xbmc_port = "" xbmc_username = "" xbmc_password = "" - + def __init__(self): threading.Thread.__init__(self) - - def setUrl(self, url): - self.urlToProcess = url - - def setHost(self, host, port): + def set_url(self, url): + + self.url_to_process = url + + def set_host(self, host, port): self.xbmc_host = host self.xbmc_port = port - - def setAuth(self, user, pwd): - self.xbmc_username = user - self.xbmc_password = pwd - + def set_auth(self, username, password): + + self.xbmc_username = username + self.xbmc_password = password + def run(self): - - log.debug("Image Caching Thread Processing: %s" % self.urlToProcess) - + + log.debug("Image Caching Thread Processing: %s", self.url_to_process) + try: - response = requests.head( - url=( - "http://%s:%s/image/image://%s" - % (self.xbmc_host, self.xbmc_port, self.urlToProcess)), - auth=(self.xbmc_username, self.xbmc_password), - timeout=(35.1, 35.1)) + requests.head( + url=("http://%s:%s/image/image://%s" + % (self.xbmc_host, self.xbmc_port, self.url_to_process)), + auth=(self.xbmc_username, self.xbmc_password), + timeout=(35.1, 35.1)) # We don't need the result - except: pass - + except Exception: + pass + log.debug("Image Caching Thread Exited") - self.isFinished = True \ No newline at end of file + self.is_finished = True diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 032affb1..fccdb93c 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -2,174 +2,153 @@ ################################################################################################# -import json import logging -import socket import xbmc import xbmcgui -import xbmcaddon import clientinfo -import downloadutils +import connectmanager +import connect.connectionmanager as connectionmanager import userclient from utils import settings, language as lang, passwordsXML ################################################################################################# log = logging.getLogger("EMBY."+__name__) +STATE = connectionmanager.ConnectionState ################################################################################################# -class InitialSetup(): +class InitialSetup(object): def __init__(self): - self.addonId = clientinfo.ClientInfo().getAddonId() - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.userClient = userclient.UserClient() + self.addon_id = clientinfo.ClientInfo().get_addon_id() + self.user_client = userclient.UserClient() + self.connectmanager = connectmanager.ConnectManager() def setup(self): # Check server, user, direct paths, music, direct stream if not direct path. - addonId = self.addonId dialog = xbmcgui.Dialog() - ##### SERVER INFO ##### - - log.debug("Initial setup called.") - server = self.userClient.getServer() + log.debug("Initial setup called") - if server: - log.debug("Server is already set.") - return - - log.debug("Looking for server...") - server = self.getServerDetails() - log.debug("Found: %s" % server) - try: - prefix, ip, port = server.replace("/", "").split(":") - except Exception: # Failed to retrieve server information - log.error("getServerDetails failed.") - xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId) - return - else: - server_confirm = dialog.yesno( - heading=lang(29999), - line1=lang(33034), - line2="%s %s" % (lang(30169), server)) - if server_confirm: - # Correct server found - log.info("Server is selected. Saving the information.") - settings('ipaddress', value=ip) - settings('port', value=port) - - if prefix == "https": - settings('https', value="true") - else: - # User selected no or cancelled the dialog - log.info("No server selected.") - xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId) - return - - ##### USER INFO ##### - - log.info("Getting user list.") - - result = self.doUtils("%s/emby/Users/Public?format=json" % server, authenticate=False) - if result == "": - log.info("Unable to connect to %s" % server) + if self._server_verification() and settings('userId'): + # Setup is already completed return - log.debug("Response: %s" % result) - # Process the list of users - usernames = [] - users_hasPassword = [] - - for user in result: - # Username - name = user['Name'] - usernames.append(name) - # Password - if user['HasPassword']: - name = "%s (secure)" % name - users_hasPassword.append(name) - - log.info("Presenting user list: %s" % users_hasPassword) - user_select = dialog.select(lang(30200), users_hasPassword) - if user_select > -1: - selected_user = usernames[user_select] - log.info("Selected user: %s" % selected_user) - settings('username', value=selected_user) - else: - log.info("No user selected.") - xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId) + if not self._user_identification(): + # User failed to identify return ##### ADDITIONAL PROMPTS ##### - directPaths = dialog.yesno( - heading=lang(30511), - line1=lang(33035), - nolabel=lang(33036), - yeslabel=lang(33037)) - if directPaths: - log.info("User opted to use direct paths.") + direct_paths = dialog.yesno(heading=lang(30511), + line1=lang(33035), + nolabel=lang(33036), + yeslabel=lang(33037)) + if direct_paths: + log.info("User opted to use direct paths") settings('useDirectPaths', value="1") # ask for credentials - credentials = dialog.yesno( - heading=lang(30517), - line1= lang(33038)) + credentials = dialog.yesno(heading=lang(30517), line1=lang(33038)) if credentials: - log.info("Presenting network credentials dialog.") + log.info("Presenting network credentials dialog") passwordsXML() - - musicDisabled = dialog.yesno( - heading=lang(29999), - line1=lang(33039)) - if musicDisabled: - log.info("User opted to disable Emby music library.") + + music_disabled = dialog.yesno(heading=lang(29999), line1=lang(33039)) + if music_disabled: + log.info("User opted to disable Emby music library") settings('enableMusic', value="false") else: # Only prompt if the user didn't select direct paths for videos - if not directPaths: - musicAccess = dialog.yesno( - heading=lang(29999), - line1=lang(33040)) - if musicAccess: - log.info("User opted to direct stream music.") + if not direct_paths: + music_access = dialog.yesno(heading=lang(29999), line1=lang(33040)) + if music_access: + log.info("User opted to direct stream music") settings('streamMusic', value="true") - - def getServerDetails(self): - log.info("Getting Server Details from Network") - - MULTI_GROUP = ("", 7359) - MESSAGE = "who is EmbyServer?" - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(6.0) + def _server_verification(self): + + ###$ Begin migration $### + if settings('server') == "": + self.user_client.get_server() + log.info("server migration completed") + + self.user_client.get_userid() + self.user_client.get_token() + ###$ End migration $### + + current_server = self.user_client.get_server() + if current_server and not settings('serverId'): + server = self.connectmanager.get_server(current_server, + {'ssl': self.user_client.get_ssl()}) + log.info("Detected: %s", server) + try: + server_id = server['Servers'][0]['Id'] + settings('serverId', value=server_id) + except Exception as error: + log.error(error) + + if current_server: + current_state = self.connectmanager.get_state() + try: + for server in current_state['Servers']: + if server['Id'] == settings('serverId'): + # Update token + server['UserId'] = settings('userId') or None + server['AccessToken'] = settings('token') or None + self.connectmanager.update_token(server) + + server_address = self.connectmanager.get_address(server) + self._set_server(server_address, server) + log.info("Found server!") + except Exception as error: + log.error(error) + + return True + + return False + + def _user_identification(self): - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) - - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) - sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - - log.debug("MultiGroup : %s" % str(MULTI_GROUP)) - log.debug("Sending UDP Data: %s" % MESSAGE) - sock.sendto(MESSAGE, MULTI_GROUP) - try: - data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes - log.info("Received Response: %s" % data) - except Exception: - log.error("No UDP Response") - return None - else: - # Get the address - data = json.loads(data) - return data['Address'] \ No newline at end of file + server = self.connectmanager.select_servers() + log.info("Server: %s", server) + server_address = self.connectmanager.get_address(server) + self._set_server(server_address, server) + + if not server.get('AccessToken') and not server.get('UserId'): + user = self.connectmanager.login(server) + log.info("User authenticated: %s", user) + settings('username', value=user['User']['Name']) + self._set_user(user['User']['Id'], user['AccessToken']) + else: # Logged with Emby Connect + user = self.connectmanager.get_state() + settings('connectUsername', value=user['ConnectUser']['Name']) + self._set_user(server['UserId'], server['AccessToken']) + + return True + + except RuntimeError as error: + log.exception(error) + xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addon_id) + return False + + @classmethod + def _set_server(cls, server_address, server): + + settings('serverName', value=server['Name']) + settings('serverId', value=server['Id']) + settings('server', value=server_address) + + @classmethod + def _set_user(cls, user_id, token): + + settings('userId', value=user_id) + settings('token', value=token) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index ad408b0f..6dda8b38 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -3,23 +3,10 @@ ################################################################################################## import logging -import urllib -from ntpath import dirname -from datetime import datetime -import xbmc -import xbmcgui -import xbmcvfs - -import api -import artwork -import clientinfo -import downloadutils -import embydb_functions as embydb -import kodidb_functions as kodidb import read_embyserver as embyserver -import musicutils -from utils import window, settings, language as lang, kodiSQL +from objects import Movies, MusicVideos, TVShows, Music +from utils import settings, kodiSQL ################################################################################################# @@ -36,34 +23,20 @@ class Items(object): self.embycursor = embycursor self.kodicursor = kodicursor - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils() - - self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) - self.directpath = settings('useDirectPaths') == "1" - self.music_enabled = settings('enableMusic') == "true" - self.contentmsg = settings('newContent') == "true" - self.newvideo_time = int(settings('newvideotime'))*1000 - self.newmusic_time = int(settings('newmusictime'))*1000 - - self.artwork = artwork.Artwork() self.emby = embyserver.Read_EmbyServer() - self.emby_db = embydb.Embydb_Functions(embycursor) - self.kodi_db = kodidb.Kodidb_Functions(kodicursor) + self.music_enabled = settings('enableMusic') == "true" def itemsbyId(self, items, process, pdialog=None): # Process items by itemid. Process can be added, update, userdata, remove - emby = self.emby embycursor = self.embycursor kodicursor = self.kodicursor - music_enabled = self.music_enabled - + itemtypes = { 'Movie': Movies, 'BoxSet': Movies, + 'MusicVideo': MusicVideos, 'Series': TVShows, 'Season': TVShows, 'Episode': TVShows, @@ -81,11 +54,10 @@ class Items(object): if total == 0: return False - log.info("Processing %s: %s" % (process, items)) + #log.info("Processing %s: %s", process, items) if pdialog: pdialog.update(heading="Processing %s: %s items" % (process, total)) - count = 0 for itemtype in items: # Safety check @@ -101,2307 +73,31 @@ class Items(object): musicconn = None if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): - if music_enabled: + if self.music_enabled: musicconn = kodiSQL('music') musiccursor = musicconn.cursor() - items_process = itemtypes[itemtype](embycursor, musiccursor) + items_process = itemtypes[itemtype](embycursor, musiccursor, pdialog) else: # Music is not enabled, do not proceed with itemtype continue else: update_videolibrary = True - items_process = itemtypes[itemtype](embycursor, kodicursor) + items_process = itemtypes[itemtype](embycursor, kodicursor, pdialog) - if itemtype == "Movie": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "BoxSet": - actions = { - 'added': items_process.added_boxset, - 'update': items_process.add_updateBoxset, - 'remove': items_process.remove - } - elif itemtype == "MusicVideo": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Series": - actions = { - 'added': items_process.added, - 'update': items_process.add_update, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "Season": - actions = { - 'added': items_process.added_season, - 'update': items_process.add_updateSeason, - 'remove': items_process.remove - } - elif itemtype == "Episode": - actions = { - 'added': items_process.added_episode, - 'update': items_process.add_updateEpisode, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype == "MusicAlbum": - actions = { - 'added': items_process.added_album, - 'update': items_process.add_updateAlbum, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } - elif itemtype in ("MusicArtist", "AlbumArtist"): - actions = { - 'added': items_process.added, - 'update': items_process.add_updateArtist, - 'remove': items_process.remove - } - elif itemtype == "Audio": - actions = { - 'added': items_process.added_song, - 'update': items_process.add_updateSong, - 'userdata': items_process.updateUserdata, - 'remove': items_process.remove - } + + if process == "added": + items_process.add_all(itemtype, itemlist) + elif process == "remove": + items_process.remove_all(itemtype, itemlist) else: - log.info("Unsupported itemtype: %s." % itemtype) - actions = {} - - if actions.get(process): - - if process == "remove": - for item in itemlist: - actions[process](item) - - elif process == "added": - actions[process](itemlist, pdialog) - - else: - processItems = emby.getFullItems(itemlist) - for item in processItems: - - title = item['Name'] - - if itemtype == "Episode": - title = "%s - %s" % (item.get('SeriesName', "Unknown"), title) - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - - actions[process](item) + process_items = self.emby.getFullItems(itemlist) + items_process.process_all(itemtype, process, process_items, total) if musicconn is not None: # close connection for special types - log.info("Updating music database.") + log.info("updating music database") musicconn.commit() musiccursor.close() return (True, update_videolibrary) - - def pathValidation(self, path): - # Verify if direct path is accessible or not - if window('emby_pathverified') != "true" and not xbmcvfs.exists(path): - resp = xbmcgui.Dialog().yesno( - heading=lang(29999), - line1="%s %s. %s" % (lang(33047), path, lang(33048))) - if resp: - window('emby_shouldStop', value="true") - return False - - return True - - def contentPop(self, name, time=5000): - - if time: - # It's possible for the time to be 0. It should be considered disabled in this case. - xbmcgui.Dialog().notification( - heading=lang(29999), - message="%s %s" % (lang(33049), name), - icon="special://home/addons/plugin.video.emby/icon.png", - time=time, - sound=False) - - -class Movies(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for movie in items: - - title = movie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(movie) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - def added_boxset(self, items, pdialog): - - total = len(items) - count = 0 - for boxset in items: - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=boxset['Name']) - count += 1 - self.add_updateBoxset(boxset) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single movie - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid)) - - except TypeError: - update_item = False - log.info("movieid: %s not found." % itemid) - # movieid - kodicursor.execute("select coalesce(max(idMovie),0) from movie") - movieid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM movie WHERE idMovie = ?" - kodicursor.execute(query, (movieid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - log.info("movieid: %s missing from Kodi, repairing the entry." % movieid) - - if not viewtag or not viewid: - # Get view tag from emby - viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) - log.debug("View tag found: %s" % viewtag) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - people = API.getPeople() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - genres = item['Genres'] - title = item['Name'] - plot = API.getOverview() - shortplot = item.get('ShortOverview') - tagline = API.getTagline() - votecount = item.get('VoteCount') - rating = item.get('CommunityRating') - year = item.get('ProductionYear') - imdb = API.getProvider('Imdb') - sorttitle = item['SortName'] - runtime = API.getRuntime() - mpaa = API.getMpaa() - genre = " / ".join(genres) - country = API.getCountry() - studios = API.getStudios() - try: - studio = studios[0] - except IndexError: - studio = None - - if item.get('LocalTrailerCount'): - # There's a local trailer - url = ( - "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json" - % itemid - ) - result = self.doUtils.downloadUrl(url) - try: - trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id'] - except IndexError: - log.info("Failed to process local trailer.") - trailer = None - else: - # Try to get the youtube trailer - try: - trailer = item['RemoteTrailers'][0]['Url'] - except (KeyError, IndexError): - trailer = None - else: - try: - trailerId = trailer.rsplit('=', 1)[1] - except IndexError: - log.info("Failed to process trailer: %s" % trailer) - trailer = None - else: - trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if not self.pathValidation(playurl): - return False - - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.movies/" - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': movieid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE MOVIE ##### - if update_item: - log.info("UPDATE movie itemid: %s - Title: %s" % (itemid, title)) - - # Update the movie entry - query = ' '.join(( - - "UPDATE movie", - "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", - "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", - "c16 = ?, c18 = ?, c19 = ?, c21 = ?", - "WHERE idMovie = ?" - )) - kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating, writer, - year, imdb, sorttitle, runtime, mpaa, genre, director, title, studio, trailer, - country, movieid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MOVIE ##### - else: - log.info("ADD movie itemid: %s - Title: %s" % (itemid, title)) - - # Add path - pathid = self.kodi_db.addPath(path) - # Add the file - fileid = self.kodi_db.addFile(filename, pathid) - - # Create the movie entry - query = ( - ''' - INSERT INTO movie( - idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, - c09, c10, c11, c12, c14, c15, c16, c18, c19, c21) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount, - rating, writer, year, imdb, sorttitle, runtime, mpaa, genre, director, title, - studio, trailer, country)) - - # Create the reference in emby table - emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid)) - - # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - - # Process countries - self.kodi_db.addCountries(movieid, item['ProductionLocations'], "movie") - # Process cast - people = artwork.getPeopleArtwork(item['People']) - self.kodi_db.addPeople(movieid, people, "movie") - # Process genres - self.kodi_db.addGenres(movieid, genres, "movie") - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), movieid, "movie", kodicursor) - # Process stream details - streams = API.getMediaStreams() - self.kodi_db.addStreams(fileid, streams, runtime) - # Process studios - self.kodi_db.addStudios(movieid, studios, "movie") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite movies") - self.kodi_db.addTags(movieid, tags, "movie") - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - - def add_updateBoxset(self, boxset): - - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - - boxsetid = boxset['Id'] - title = boxset['Name'] - checksum = boxset['Etag'] - emby_dbitem = emby_db.getItem_byId(boxsetid) - try: - setid = emby_dbitem[0] - - except TypeError: - setid = self.kodi_db.createBoxset(title) - - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(boxset), setid, "set", self.kodicursor) - - # Process movies inside boxset - current_movies = emby_db.getItemId_byParentId(setid, "movie") - process = [] - try: - # Try to convert tuple to dictionary - current = dict(current_movies) - except ValueError: - current = {} - - # Sort current titles - for current_movie in current: - process.append(current_movie) - - # New list to compare - for movie in emby.getMovies_byBoxset(boxsetid)['Items']: - - itemid = movie['Id'] - - if not current.get(itemid): - # Assign boxset to movie - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - except TypeError: - log.info("Failed to add: %s to boxset." % movie['Name']) - continue - - log.info("New addition to boxset %s: %s" % (title, movie['Name'])) - self.kodi_db.assignBoxset(setid, movieid) - # Update emby reference - emby_db.updateParentId(itemid, setid) - else: - # Remove from process, because the item still belongs - process.remove(itemid) - - # Process removals from boxset - for movie in process: - movieid = current[movie] - log.info("Remove from boxset %s: %s" % (title, movieid)) - self.kodi_db.removefromBoxset(movieid) - # Update emby reference - emby_db.updateParentId(movie, None) - - # Update the reference in the emby table - emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum) - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - movieid = emby_dbitem[0] - fileid = emby_dbitem[1] - log.info("Update playstate for movie: %s fileid: %s" % (item['Name'], fileid)) - except TypeError: - return - - # Process favorite tags - if userdata['Favorite']: - self.kodi_db.addTag(movieid, "Favorite movies", "movie") - else: - self.kodi_db.removeTag(movieid, "Favorite movies", "movie") - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - log.debug("%s New resume point: %s" % (itemid, resume)) - - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove movieid, fileid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - log.info("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid)) - except TypeError: - return - - # Remove the emby reference - emby_db.removeItem(itemid) - # Remove artwork - artwork.deleteArtwork(kodiid, mediatype, kodicursor) - - if mediatype == "movie": - # Delete kodi movie and file - kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodiid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - - elif mediatype == "set": - # Delete kodi boxset - boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") - for movie in boxset_movies: - embyid = movie[0] - movieid = movie[1] - self.kodi_db.removefromBoxset(movieid) - # Update emby reference - emby_db.updateParentId(embyid, None) - - kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,)) - - log.info("Deleted %s %s from kodi database" % (mediatype, itemid)) - -class MusicVideos(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for mvideo in items: - - title = mvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(mvideo) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single music video - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("mvideoid: %s fileid: %s pathid: %s" % (mvideoid, fileid, pathid)) - - except TypeError: - update_item = False - log.info("mvideoid: %s not found." % itemid) - # mvideoid - kodicursor.execute("select coalesce(max(idMVideo),0) from musicvideo") - mvideoid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM musicvideo WHERE idMVideo = ?" - kodicursor.execute(query, (mvideoid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - log.info("mvideoid: %s missing from Kodi, repairing the entry." % mvideoid) - - if not viewtag or not viewid: - # Get view tag from emby - viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) - log.debug("View tag found: %s" % viewtag) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - runtime = API.getRuntime() - plot = API.getOverview() - title = item['Name'] - year = item.get('ProductionYear') - genres = item['Genres'] - genre = " / ".join(genres) - studios = API.getStudios() - studio = " / ".join(studios) - artist = " / ".join(item.get('Artists')) - album = item.get('Album') - track = item.get('Track') - people = API.getPeople() - director = " / ".join(people['Director']) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if not self.pathValidation(playurl): - return False - - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.musicvideos/" - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': mvideoid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE MUSIC VIDEO ##### - if update_item: - log.info("UPDATE mvideo itemid: %s - Title: %s" % (itemid, title)) - - # Update path - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - kodicursor.execute(query, (path, pathid)) - - # Update the filename - query = "UPDATE files SET strFilename = ?, dateAdded = ? WHERE idFile = ?" - kodicursor.execute(query, (filename, dateadded, fileid)) - - # Update the music video entry - query = ' '.join(( - - "UPDATE musicvideo", - "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,", - "c11 = ?, c12 = ?" - "WHERE idMVideo = ?" - )) - kodicursor.execute(query, (title, runtime, director, studio, year, plot, album, - artist, genre, track, mvideoid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE MUSIC VIDEO ##### - else: - log.info("ADD mvideo itemid: %s - Title: %s" % (itemid, title)) - - # Add path - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - kodicursor.execute(query, (path,)) - try: - pathid = kodicursor.fetchone()[0] - except TypeError: - kodicursor.execute("select coalesce(max(idPath),0) from path") - pathid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT OR REPLACE INTO path( - idPath, strPath, strContent, strScraper, noUpdate) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (pathid, path, "musicvideos", "metadata.local", 1)) - - # Add the file - kodicursor.execute("select coalesce(max(idFile),0) from files") - fileid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO files( - idFile, idPath, strFilename, dateAdded) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (fileid, pathid, filename, dateadded)) - - # Create the musicvideo entry - query = ( - ''' - INSERT INTO musicvideo( - idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (mvideoid, fileid, title, runtime, director, studio, - year, plot, album, artist, genre, track)) - - # Create the reference in emby table - emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid, - checksum=checksum, mediafolderid=viewid) - - - # Process cast - people = item['People'] - artists = item['ArtistItems'] - for artist in artists: - artist['Type'] = "Artist" - people.extend(artists) - people = artwork.getPeopleArtwork(people) - self.kodi_db.addPeople(mvideoid, people, "musicvideo") - # Process genres - self.kodi_db.addGenres(mvideoid, genres, "musicvideo") - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor) - # Process stream details - streams = API.getMediaStreams() - self.kodi_db.addStreams(fileid, streams, runtime) - # Process studios - self.kodi_db.addStudios(mvideoid, studios, "musicvideo") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite musicvideos") - self.kodi_db.addTags(mvideoid, tags, "musicvideo") - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - log.info( - "Update playstate for musicvideo: %s fileid: %s" - % (item['Name'], fileid)) - except TypeError: - return - - # Process favorite tags - if userdata['Favorite']: - self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") - else: - self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove mvideoid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - mvideoid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("Removing mvideoid: %s fileid: %s" % (mvideoid, fileid, pathid)) - except TypeError: - return - - # Remove artwork - query = ' '.join(( - - "SELECT url, type", - "FROM art", - "WHERE media_id = ?", - "AND media_type = 'musicvideo'" - )) - kodicursor.execute(query, (mvideoid,)) - for row in kodicursor.fetchall(): - - url = row[0] - imagetype = row[1] - if imagetype in ("poster", "fanart"): - artwork.deleteCachedArtwork(url) - - kodicursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (mvideoid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - if self.directpath: - kodicursor.execute("DELETE FROM path WHERE idPath = ?", (pathid,)) - self.embycursor.execute("DELETE FROM emby WHERE emby_id = ?", (itemid,)) - - log.info("Deleted musicvideo %s from kodi database" % itemid) - -class TVShows(Items): - - - def __init__(self, embycursor, kodicursor): - Items.__init__(self, embycursor, kodicursor) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for tvshow in items: - - title = tvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_update(tvshow) - # Add episodes - all_episodes = self.emby.getEpisodesbyShow(tvshow['Id']) - self.added_episode(all_episodes['Items'], pdialog) - - def added_season(self, items, pdialog): - - total = len(items) - count = 0 - for season in items: - - title = "%s - %s" % (season.get('SeriesName', "Unknown"), season['Name']) - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateSeason(season) - # Add episodes - all_episodes = self.emby.getEpisodesbySeason(season['Id']) - self.added_episode(all_episodes['Items'], pdialog) - - def added_episode(self, items, pdialog): - - total = len(items) - count = 0 - for episode in items: - title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - self.add_updateEpisode(episode) - if not pdialog and self.contentmsg: - self.contentPop(title, self.newvideo_time) - - - def add_update(self, item, viewtag=None, viewid=None): - # Process single tvshow - kodicursor = self.kodicursor - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): - log.info("Skipping empty show: %s" % item['Name']) - return - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - force_episodes = False - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - showid = emby_dbitem[0] - pathid = emby_dbitem[2] - log.info("showid: %s pathid: %s" % (showid, pathid)) - - except TypeError: - update_item = False - log.info("showid: %s not found." % itemid) - kodicursor.execute("select coalesce(max(idShow),0) from tvshow") - showid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM tvshow WHERE idShow = ?" - kodicursor.execute(query, (showid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - log.info("showid: %s missing from Kodi, repairing the entry." % showid) - # Force re-add episodes after the show is re-created. - force_episodes = True - - - if viewtag is None or viewid is None: - # Get view tag from emby - viewtag, viewid, mediatype = emby.getView_embyId(itemid) - log.debug("View tag found: %s" % viewtag) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - genres = item['Genres'] - title = item['Name'] - plot = API.getOverview() - rating = item.get('CommunityRating') - premieredate = API.getPremiereDate() - tvdb = API.getProvider('Tvdb') - sorttitle = item['SortName'] - mpaa = API.getMpaa() - genre = " / ".join(genres) - studios = API.getStudios() - studio = " / ".join(studios) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if self.directpath: - # Direct paths is set the Kodi way - if "\\" in playurl: - # Local path - path = "%s\\" % playurl - toplevelpath = "%s\\" % dirname(dirname(path)) - else: - # Network path - path = "%s/" % playurl - toplevelpath = "%s/" % dirname(dirname(path)) - - if not self.pathValidation(playurl): - return False - - window('emby_pathverified', value="true") - else: - # Set plugin path - toplevelpath = "plugin://plugin.video.emby.tvshows/" - path = "%s%s/" % (toplevelpath, itemid) - - - ##### UPDATE THE TVSHOW ##### - if update_item: - log.info("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title)) - - # Update the tvshow entry - query = ' '.join(( - - "UPDATE tvshow", - "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (title, plot, rating, premieredate, genre, title, - tvdb, mpaa, studio, sorttitle, showid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE TVSHOW ##### - else: - log.info("ADD tvshow itemid: %s - Title: %s" % (itemid, title)) - - # Add top path - toppathid = self.kodi_db.addPath(toplevelpath) - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid)) - - # Add path - pathid = self.kodi_db.addPath(path) - - # Create the tvshow entry - query = ( - ''' - INSERT INTO tvshow( - idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre, - title, tvdb, mpaa, studio, sorttitle)) - - # Link the path - query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)" - kodicursor.execute(query, (showid, pathid)) - - # Create the reference in emby table - emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, - checksum=checksum, mediafolderid=viewid) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, None, None, 1, pathid)) - - # Process cast - people = artwork.getPeopleArtwork(item['People']) - self.kodi_db.addPeople(showid, people, "tvshow") - # Process genres - self.kodi_db.addGenres(showid, genres, "tvshow") - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), showid, "tvshow", kodicursor) - # Process studios - self.kodi_db.addStudios(showid, studios, "tvshow") - # Process tags: view, emby tags - tags = [viewtag] - tags.extend(item['Tags']) - if userdata['Favorite']: - tags.append("Favorite tvshows") - self.kodi_db.addTags(showid, tags, "tvshow") - # Process seasons - all_seasons = emby.getSeasons(itemid) - for season in all_seasons['Items']: - self.add_updateSeason(season, showid=showid) - else: - # Finally, refresh the all season entry - seasonid = self.kodi_db.addSeason(showid, -1) - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) - - if force_episodes: - # We needed to recreate the show entry. Re-add episodes now. - log.info("Repairing episodes for showid: %s %s" % (showid, title)) - all_episodes = emby.getEpisodesbyShow(itemid) - self.added_episode(all_episodes['Items'], None) - - def add_updateSeason(self, item, showid=None): - - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - - seasonnum = item.get('IndexNumber', 1) - - if showid is None: - try: - seriesId = item['SeriesId'] - showid = emby_db.getItem_byId(seriesId)[0] - except KeyError: - return - except TypeError: - # Show is missing, update show instead. - show = self.emby.getItem(seriesId) - self.add_update(show) - return - - seasonid = self.kodi_db.addSeason(showid, seasonnum) - - if item['LocationType'] != "Virtual": - # Create the reference in emby table - emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid) - - # Process artwork - artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor) - - def add_updateEpisode(self, item): - # Process single episode - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - # If the item already exist in the local Kodi DB we'll perform a full item update - # If the item doesn't exist, we'll add it to the database - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - episodeid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - log.info("episodeid: %s fileid: %s pathid: %s" % (episodeid, fileid, pathid)) - - except TypeError: - update_item = False - log.info("episodeid: %s not found." % itemid) - # episodeid - kodicursor.execute("select coalesce(max(idEpisode),0) from episode") - episodeid = kodicursor.fetchone()[0] + 1 - - else: - # Verification the item is still in Kodi - query = "SELECT * FROM episode WHERE idEpisode = ?" - kodicursor.execute(query, (episodeid,)) - try: - kodicursor.fetchone()[0] - except TypeError: - # item is not found, let's recreate it. - update_item = False - log.info("episodeid: %s missing from Kodi, repairing the entry." % episodeid) - - # fileId information - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - people = API.getPeople() - writer = " / ".join(people['Writer']) - director = " / ".join(people['Director']) - title = item['Name'] - plot = API.getOverview() - rating = item.get('CommunityRating') - runtime = API.getRuntime() - premieredate = API.getPremiereDate() - - # episode details - try: - seriesId = item['SeriesId'] - except KeyError: - # Missing seriesId, skip - log.error("Skipping: %s. SeriesId is missing." % itemid) - return False - - season = item.get('ParentIndexNumber') - episode = item.get('IndexNumber', -1) - - if season is None: - if item.get('AbsoluteEpisodeNumber'): - # Anime scenario - season = 1 - episode = item['AbsoluteEpisodeNumber'] - else: - season = -1 - - # Specials ordering within season - if item.get('AirsAfterSeasonNumber'): - airsBeforeSeason = item['AirsAfterSeasonNumber'] - airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering - else: - airsBeforeSeason = item.get('AirsBeforeSeasonNumber') - airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') - - # Append multi episodes to title - if item.get('IndexNumberEnd'): - title = "| %02d | %s" % (item['IndexNumberEnd'], title) - - # Get season id - show = emby_db.getItem_byId(seriesId) - try: - showid = show[0] - except TypeError: - # Show is missing from database - show = self.emby.getItem(seriesId) - self.add_update(show) - show = emby_db.getItem_byId(seriesId) - try: - showid = show[0] - except TypeError: - log.error("Skipping: %s. Unable to add series: %s." % (itemid, seriesId)) - return False - - seasonid = self.kodi_db.addSeason(showid, season) - - - ##### GET THE FILE AND PATH ##### - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - if self.directpath: - # Direct paths is set the Kodi way - if not self.pathValidation(playurl): - return False - - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") - else: - # Set plugin path and media flags using real filename - path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId - params = { - - 'filename': filename.encode('utf-8'), - 'id': itemid, - 'dbid': episodeid, - 'mode': "play" - } - filename = "%s?%s" % (path, urllib.urlencode(params)) - - - ##### UPDATE THE EPISODE ##### - if update_item: - log.info("UPDATE episode itemid: %s - Title: %s" % (itemid, title)) - - # Update the movie entry - if self.kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?", - "WHERE idEpisode = ?" - )) - kodicursor.execute(query, (title, plot, rating, writer, premieredate, - runtime, director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, seasonid, episodeid)) - else: - query = ' '.join(( - - "UPDATE episode", - "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", - "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?", - "WHERE idEpisode = ?" - )) - kodicursor.execute(query, (title, plot, rating, writer, premieredate, - runtime, director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, episodeid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - # Update parentid reference - emby_db.updateParentId(itemid, seasonid) - - ##### OR ADD THE EPISODE ##### - else: - log.info("ADD episode itemid: %s - Title: %s" % (itemid, title)) - - # Add path - pathid = self.kodi_db.addPath(path) - # Add the file - fileid = self.kodi_db.addFile(filename, pathid) - - # Create the episode entry - if self.kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16, idSeason) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, showid, - airsBeforeSeason, airsBeforeEpisode, seasonid)) - else: - query = ( - ''' - INSERT INTO episode( - idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, - idShow, c15, c16) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer, - premieredate, runtime, director, season, episode, title, showid, - airsBeforeSeason, airsBeforeEpisode)) - - # Create the reference in emby table - emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, - seasonid, checksum) - - # Update the path - query = ' '.join(( - - "UPDATE path", - "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", - "WHERE idPath = ?" - )) - kodicursor.execute(query, (path, None, None, 1, pathid)) - - # Update the file - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (pathid, filename, dateadded, fileid)) - - # Process cast - people = artwork.getPeopleArtwork(item['People']) - self.kodi_db.addPeople(episodeid, people, "episode") - # Process artwork - artworks = artwork.getAllArtwork(item) - artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor) - # Process stream details - streams = API.getMediaStreams() - self.kodi_db.addStreams(fileid, streams, runtime) - # Process playstates - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - if not self.directpath and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/") - tempfileid = self.kodi_db.addFile(filename, temppathid) - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) - self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed) - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - dateadded = API.getDateCreated() - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - mediatype = emby_dbitem[4] - log.info( - "Update playstate for %s: %s fileid: %s" - % (mediatype, item['Name'], fileid)) - except TypeError: - return - - # Process favorite tags - if mediatype == "tvshow": - if userdata['Favorite']: - self.kodi_db.addTag(kodiid, "Favorite tvshows", "tvshow") - else: - self.kodi_db.removeTag(kodiid, "Favorite tvshows", "tvshow") - elif mediatype == "episode": - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - resume = API.adjustResume(userdata['Resume']) - total = round(float(runtime), 6) - - log.debug("%s New resume point: %s" % (itemid, resume)) - - self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed) - if not self.directpath and not resume: - # Make sure there's no other bookmarks created by widget. - filename = self.kodi_db.getFile(fileid) - self.kodi_db.removeFile("plugin://plugin.video.emby.tvshows/", filename) - - if not self.directpath and resume: - # Create additional entry for widgets. This is only required for plugin/episode. - filename = self.kodi_db.getFile(fileid) - temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/") - tempfileid = self.kodi_db.addFile(filename, temppathid) - query = ' '.join(( - - "UPDATE files", - "SET idPath = ?, strFilename = ?, dateAdded = ?", - "WHERE idFile = ?" - )) - self.kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) - self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed) - - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove showid, fileid, pathid, emby reference - emby_db = self.emby_db - embycursor = self.embycursor - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - fileid = emby_dbitem[1] - pathid = emby_dbitem[2] - parentid = emby_dbitem[3] - mediatype = emby_dbitem[4] - log.info("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid)) - except TypeError: - return - - ##### PROCESS ITEM ##### - - # Remove the emby reference - emby_db.removeItem(itemid) - - - ##### IF EPISODE ##### - - if mediatype == "episode": - # Delete kodi episode and file, verify season and tvshow - self.removeEpisode(kodiid, fileid) - - # Season verification - season = emby_db.getItem_byKodiId(parentid, "season") - try: - showid = season[1] - except TypeError: - return - - season_episodes = emby_db.getItem_byParentId(parentid, "episode") - if not season_episodes: - self.removeSeason(parentid) - emby_db.removeItem(season[0]) - - # Show verification - show = emby_db.getItem_byKodiId(showid, "tvshow") - query = ' '.join(( - - "SELECT totalCount", - "FROM tvshowcounts", - "WHERE idShow = ?" - )) - kodicursor.execute(query, (showid,)) - result = kodicursor.fetchone() - if result and result[0] is None: - # There's no episodes left, delete show and any possible remaining seasons - seasons = emby_db.getItem_byParentId(showid, "season") - for season in seasons: - self.removeSeason(season[1]) - else: - # Delete emby season entries - emby_db.removeItems_byParentId(showid, "season") - self.removeShow(showid) - emby_db.removeItem(show[0]) - - ##### IF TVSHOW ##### - - elif mediatype == "tvshow": - # Remove episodes, seasons, tvshow - seasons = emby_db.getItem_byParentId(kodiid, "season") - for season in seasons: - seasonid = season[1] - season_episodes = emby_db.getItem_byParentId(seasonid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - else: - # Remove emby episodes - emby_db.removeItems_byParentId(seasonid, "episode") - else: - # Remove emby seasons - emby_db.removeItems_byParentId(kodiid, "season") - - # Remove tvshow - self.removeShow(kodiid) - - ##### IF SEASON ##### - - elif mediatype == "season": - # Remove episodes, season, verify tvshow - season_episodes = emby_db.getItem_byParentId(kodiid, "episode") - for episode in season_episodes: - self.removeEpisode(episode[1], episode[2]) - else: - # Remove emby episodes - emby_db.removeItems_byParentId(kodiid, "episode") - - # Remove season - self.removeSeason(kodiid) - - # Show verification - seasons = emby_db.getItem_byParentId(parentid, "season") - if not seasons: - # There's no seasons, delete the show - self.removeShow(parentid) - emby_db.removeItem_byKodiId(parentid, "tvshow") - - log.info("Deleted %s: %s from kodi database" % (mediatype, itemid)) - - def removeShow(self, kodiid): - - kodicursor = self.kodicursor - self.artwork.deleteArtwork(kodiid, "tvshow", kodicursor) - kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,)) - log.debug("Removed tvshow: %s." % kodiid) - - def removeSeason(self, kodiid): - - kodicursor = self.kodicursor - - self.artwork.deleteArtwork(kodiid, "season", kodicursor) - kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,)) - log.debug("Removed season: %s." % kodiid) - - def removeEpisode(self, kodiid, fileid): - - kodicursor = self.kodicursor - - self.artwork.deleteArtwork(kodiid, "episode", kodicursor) - kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,)) - kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) - log.debug("Removed episode: %s." % kodiid) - -class Music(Items): - - - def __init__(self, embycursor, musiccursor): - - Items.__init__(self, embycursor, musiccursor) - - self.directstream = settings('streamMusic') == "true" - self.enableimportsongrating = settings('enableImportSongRating') == "true" - self.enableexportsongrating = settings('enableExportSongRating') == "true" - self.enableupdatesongrating = settings('enableUpdateSongRating') == "true" - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) - - def added(self, items, pdialog): - - total = len(items) - count = 0 - for artist in items: - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=artist['Name']) - count += 1 - self.add_updateArtist(artist) - # Add albums - all_albums = self.emby.getAlbumsbyArtist(artist['Id']) - self.added_album(all_albums['Items'], pdialog) - - def added_album(self, items, pdialog): - - total = len(items) - count = 0 - for album in items: - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=album['Name']) - count += 1 - self.add_updateAlbum(album) - # Add songs - all_songs = self.emby.getSongsbyAlbum(album['Id']) - self.added_song(all_songs['Items'], pdialog) - - def added_song(self, items, pdialog): - - total = len(items) - count = 0 - for song in items: - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=song['Name']) - count += 1 - self.add_updateSong(song) - if not pdialog and self.contentmsg: - self.contentPop(song['Name'], self.newmusic_time) - - def add_updateArtist(self, item, artisttype="MusicArtist"): - # Process a single artist - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - artistid = emby_dbitem[0] - except TypeError: - update_item = False - log.debug("artistid: %s not found." % itemid) - - ##### The artist details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - checksum = API.getChecksum() - - name = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzArtist') - genres = " / ".join(item.get('Genres')) - bio = API.getOverview() - - # Associate artwork - artworks = artwork.getAllArtwork(item, parentInfo=True) - thumb = artworks['Primary'] - backdrops = artworks['Backdrop'] # List - - if thumb: - thumb = "%s" % thumb - if backdrops: - fanart = "%s" % backdrops[0] - else: - fanart = "" - - - ##### UPDATE THE ARTIST ##### - if update_item: - log.info("UPDATE artist itemid: %s - Name: %s" % (itemid, name)) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE ARTIST ##### - else: - log.info("ADD artist itemid: %s - Name: %s" % (itemid, name)) - # 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. - artistid = self.kodi_db.addArtist(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum) - - - # Process the artist - if self.kodiversion in (16, 17): - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?", - "WHERE idArtist = ?" - )) - kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid)) - else: - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?, dateAdded = ?", - "WHERE idArtist = ?" - )) - kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, - dateadded, artistid)) - - - # Update artwork - artwork.addArtwork(artworks, artistid, "artist", kodicursor) - - def add_updateAlbum(self, item): - # Process a single artist - emby = self.emby - kodicursor = self.kodicursor - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - albumid = emby_dbitem[0] - except TypeError: - update_item = False - log.debug("albumid: %s not found." % itemid) - - ##### The album details ##### - lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - dateadded = API.getDateCreated() - userdata = API.getUserData() - checksum = API.getChecksum() - - name = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzAlbum') - year = item.get('ProductionYear') - genres = item.get('Genres') - genre = " / ".join(genres) - bio = API.getOverview() - rating = userdata['UserRating'] - artists = item['AlbumArtists'] - if not artists: - artists = item['ArtistItems'] - artistname = [] - for artist in artists: - artistname.append(artist['Name']) - artistname = " / ".join(artistname) - - # Associate artwork - artworks = artwork.getAllArtwork(item, parentInfo=True) - thumb = artworks['Primary'] - if thumb: - thumb = "%s" % thumb - - ##### UPDATE THE ALBUM ##### - if update_item: - log.info("UPDATE album itemid: %s - Name: %s" % (itemid, name)) - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE ALBUM ##### - else: - log.info("ADD album itemid: %s - Name: %s" % (itemid, name)) - # 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. - albumid = self.kodi_db.addAlbum(name, musicBrainzId) - # Create the reference in emby table - emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum) - - - # Process the album info - if self.kodiversion == 17: - # Kodi Krypton - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iUserrating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid)) - elif self.kodiversion == 16: - # Kodi Jarvis - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - "album", albumid)) - elif self.kodiversion == 15: - # Kodi Isengard - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, "album", albumid)) - else: - # Kodi Helix - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped, - dateadded, albumid)) - - # Associate the parentid for emby reference - parentId = item.get('ParentId') - if parentId is not None: - emby_dbartist = emby_db.getItem_byId(parentId) - try: - artistid = emby_dbartist[0] - except TypeError: - # Artist does not exist in emby database. - artist = emby.getItem(parentId) - # Item may not be an artist, verification necessary. - if artist['Type'] == "MusicArtist": - # Update with the parentId, for remove reference - emby_db.addReference(parentId, parentId, "MusicArtist", "artist") - emby_db.updateParentId(itemid, parentId) - else: - # Update emby reference with the artistid - emby_db.updateParentId(itemid, artistid) - - # Assign main artists to album - for artist in artists: - artistname = artist['Name'] - artistId = artist['Id'] - emby_dbartist = emby_db.getItem_byId(artistId) - try: - artistid = emby_dbartist[0] - except TypeError: - # Artist does not exist in emby database, create the reference - artist = emby.getItem(artistId) - self.add_updateArtist(artist, artisttype="AlbumArtist") - emby_dbartist = emby_db.getItem_byId(artistId) - artistid = emby_dbartist[0] - else: - # Best take this name over anything else. - query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - kodicursor.execute(query, (artistname, artistid,)) - - # Add artist to album - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, albumid, artistname)) - # Update discography - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, name, year)) - # Update emby reference with parentid - emby_db.updateParentId(artistId, albumid) - - # Add genres - self.kodi_db.addMusicGenres(albumid, genres, "album") - # Update artwork - artwork.addArtwork(artworks, albumid, "album", kodicursor) - - def add_updateSong(self, item): - # Process single song - kodicursor = self.kodicursor - emby = self.emby - emby_db = self.emby_db - artwork = self.artwork - API = api.API(item) - - update_item = True - itemid = item['Id'] - emby_dbitem = emby_db.getItem_byId(itemid) - try: - songid = emby_dbitem[0] - pathid = emby_dbitem[2] - albumid = emby_dbitem[3] - except TypeError: - update_item = False - log.debug("songid: %s not found." % itemid) - - ##### The song details ##### - checksum = API.getChecksum() - dateadded = API.getDateCreated() - userdata = API.getUserData() - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - # item details - title = item['Name'] - musicBrainzId = API.getProvider('MusicBrainzTrackId') - genres = item.get('Genres') - genre = " / ".join(genres) - artists = " / ".join(item['Artists']) - tracknumber = item.get('IndexNumber', 0) - disc = item.get('ParentIndexNumber', 1) - if disc == 1: - track = tracknumber - else: - track = disc*2**16 + tracknumber - year = item.get('ProductionYear') - duration = API.getRuntime() - rating = userdata['UserRating'] - - #if enabled, try to get the rating from file and/or emby - if not self.directstream: - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - else: - hasEmbeddedCover = False - comment = API.getOverview() - - - ##### GET THE FILE AND PATH ##### - if self.directstream: - path = "%s/emby/Audio/%s/" % (self.server, itemid) - filename = "stream.mp3" - else: - playurl = API.getFilePath() - - if "\\" in playurl: - # Local path - filename = playurl.rsplit("\\", 1)[1] - else: # Network share - filename = playurl.rsplit("/", 1)[1] - - # Direct paths is set the Kodi way - if not self.pathValidation(playurl): - return False - - path = playurl.replace(filename, "") - window('emby_pathverified', value="true") - - ##### UPDATE THE SONG ##### - if update_item: - log.info("UPDATE song itemid: %s - Title: %s" % (itemid, title)) - - # Update path - query = "UPDATE path SET strPath = ? WHERE idPath = ?" - kodicursor.execute(query, (path, pathid)) - - # Update the song entry - query = ' '.join(( - - "UPDATE song", - "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", - "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", - "rating = ?, comment = ?", - "WHERE idSong = ?" - )) - kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year, - filename, playcount, dateplayed, rating, comment, songid)) - - # Update the checksum in emby table - emby_db.updateReference(itemid, checksum) - - ##### OR ADD THE SONG ##### - else: - log.info("ADD song itemid: %s - Title: %s" % (itemid, title)) - - # Add path - pathid = self.kodi_db.addPath(path) - - try: - # Get the album - emby_dbalbum = emby_db.getItem_byId(item['AlbumId']) - albumid = emby_dbalbum[0] - except KeyError: - # Verify if there's an album associated. - album_name = item.get('Album') - if album_name: - log.info("Creating virtual music album for song: %s." % itemid) - albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) - emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album") - else: - # No album Id associated to the song. - log.error("Song itemid: %s has no albumId associated." % itemid) - return False - - except TypeError: - # No album found. Let's create it - log.info("Album database entry missing.") - emby_albumId = item['AlbumId'] - album = emby.getItem(emby_albumId) - self.add_updateAlbum(album) - emby_dbalbum = emby_db.getItem_byId(emby_albumId) - try: - albumid = emby_dbalbum[0] - log.info("Found albumid: %s" % albumid) - except TypeError: - # No album found, create a single's album - log.info("Failed to add album. Creating singles.") - kodicursor.execute("select coalesce(max(idAlbum),0) from album") - albumid = kodicursor.fetchone()[0] + 1 - if self.kodiversion == 16: - # Kodi Jarvis - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, "single")) - elif self.kodiversion == 15: - # Kodi Isengard - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded, "single")) - else: - # Kodi Helix - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded)) - - # Create the song entry - kodicursor.execute("select coalesce(max(idSong),0) from song") - songid = kodicursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO song( - idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, - iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, - rating) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (songid, albumid, pathid, artists, genre, title, track, - duration, year, filename, musicBrainzId, playcount, dateplayed, rating)) - - # Create the reference in emby table - emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, - checksum=checksum) - - - # Link song to album - query = ( - ''' - INSERT OR REPLACE INTO albuminfosong( - idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (songid, albumid, track, title, duration)) - - # Link song to artists - for index, artist in enumerate(item['ArtistItems']): - - artist_name = artist['Name'] - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - if self.kodiversion >= 17: - # Kodi Krypton - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, songid, 1, index, artist_name)) - - # May want to look into only doing this once? - query = ( - ''' - INSERT OR REPLACE INTO role(idRole, strRole) - - VALUES (?, ?) - ''' - ) - kodicursor.execute(query, (1, 'Composer')) - else: - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) - - VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, songid, index, artist_name)) - - # Verify if album artist exists - album_artists = [] - for artist in item['AlbumArtists']: - - artist_name = artist['Name'] - album_artists.append(artist_name) - artist_eid = artist['Id'] - artist_edb = emby_db.getItem_byId(artist_eid) - try: - artistid = artist_edb[0] - except TypeError: - # Artist is missing from emby database, add it. - artist_full = emby.getItem(artist_eid) - self.add_updateArtist(artist_full) - artist_edb = emby_db.getItem_byId(artist_eid) - artistid = artist_edb[0] - finally: - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, albumid, artist_name)) - # Update discography - if item.get('Album'): - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - - VALUES (?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, item['Album'], 0)) - else: - album_artists = " / ".join(album_artists) - query = ' '.join(( - - "SELECT strArtists", - "FROM album", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (albumid,)) - result = kodicursor.fetchone() - if result and result[0] != album_artists: - # Field is empty - if self.kodiversion in (16, 17): - # Kodi Jarvis, Krypton - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - elif self.kodiversion == 15: - # Kodi Isengard - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - else: - # Kodi Helix - query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" - kodicursor.execute(query, (album_artists, albumid)) - - # Add genres - self.kodi_db.addMusicGenres(songid, genres, "song") - - # Update artwork - allart = artwork.getAllArtwork(item, parentInfo=True) - if hasEmbeddedCover: - allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl ) - artwork.addArtwork(allart, songid, "song", kodicursor) - - if item.get('AlbumId') is None: - # Update album artwork - artwork.addArtwork(allart, albumid, "album", kodicursor) - - def updateUserdata(self, item): - # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks - # Poster with progress bar - kodicursor = self.kodicursor - emby_db = self.emby_db - API = api.API(item) - - # Get emby information - itemid = item['Id'] - checksum = API.getChecksum() - userdata = API.getUserData() - runtime = API.getRuntime() - rating = userdata['UserRating'] - - # Get Kodi information - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - log.info("Update playstate for %s: %s" % (mediatype, item['Name'])) - except TypeError: - return - - if mediatype == "song": - - #should we ignore this item ? - #happens when userdata updated by ratings method - if window("ignore-update-%s" %itemid): - window("ignore-update-%s" %itemid,clear=True) - return - - # Process playstates - playcount = userdata['PlayCount'] - dateplayed = userdata['LastPlayedDate'] - - #process item ratings - rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) - - query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?" - kodicursor.execute(query, (playcount, dateplayed, rating, kodiid)) - - elif mediatype == "album": - # Process playstates - query = "UPDATE album SET iRating = ? WHERE idAlbum = ?" - kodicursor.execute(query, (rating, kodiid)) - - emby_db.updateReference(itemid, checksum) - - def remove(self, itemid): - # Remove kodiid, fileid, pathid, emby reference - emby_db = self.emby_db - kodicursor = self.kodicursor - artwork = self.artwork - - emby_dbitem = emby_db.getItem_byId(itemid) - try: - kodiid = emby_dbitem[0] - mediatype = emby_dbitem[4] - log.info("Removing %s kodiid: %s" % (mediatype, kodiid)) - except TypeError: - return - - ##### PROCESS ITEM ##### - - # Remove the emby reference - emby_db.removeItem(itemid) - - - ##### IF SONG ##### - - if mediatype == "song": - # Delete song - self.removeSong(kodiid) - # This should only address single song scenario, where server doesn't actually - # create an album for the song. - emby_db.removeWildItem(itemid) - - for item in emby_db.getItem_byWildId(itemid): - - item_kid = item[0] - item_mediatype = item[1] - - if item_mediatype == "album": - childs = emby_db.getItem_byParentId(item_kid, "song") - if not childs: - # Delete album - self.removeAlbum(item_kid) - - ##### IF ALBUM ##### - - elif mediatype == "album": - # Delete songs, album - album_songs = emby_db.getItem_byParentId(kodiid, "song") - for song in album_songs: - self.removeSong(song[1]) - else: - # Remove emby songs - emby_db.removeItems_byParentId(kodiid, "song") - - # Remove the album - self.removeAlbum(kodiid) - - ##### IF ARTIST ##### - - elif mediatype == "artist": - # Delete songs, album, artist - albums = emby_db.getItem_byParentId(kodiid, "album") - for album in albums: - albumid = album[1] - album_songs = emby_db.getItem_byParentId(albumid, "song") - for song in album_songs: - self.removeSong(song[1]) - else: - # Remove emby song - emby_db.removeItems_byParentId(albumid, "song") - # Remove emby artist - emby_db.removeItems_byParentId(albumid, "artist") - # Remove kodi album - self.removeAlbum(albumid) - else: - # Remove emby albums - emby_db.removeItems_byParentId(kodiid, "album") - - # Remove artist - self.removeArtist(kodiid) - - log.info("Deleted %s: %s from kodi database" % (mediatype, itemid)) - - def removeSong(self, kodiId): - - kodicursor = self.kodicursor - - self.artwork.deleteArtwork(kodiId, "song", self.kodicursor) - self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiId,)) - - def removeAlbum(self, kodiId): - - self.artwork.deleteArtwork(kodiId, "album", self.kodicursor) - self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiId,)) - - def removeArtist(self, kodiId): - - self.artwork.deleteArtwork(kodiId, "artist", self.kodicursor) - self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiId,)) \ No newline at end of file diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 9b7aca14..e486250b 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -8,7 +8,6 @@ import xbmc import api import artwork -import clientinfo ################################################################################################# @@ -16,784 +15,17 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################# -class Kodidb_Functions(): +class Kodidb_Functions(object): kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) def __init__(self, cursor): - + self.cursor = cursor - - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() self.artwork = artwork.Artwork() - - - def addPath(self, path): - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - self.cursor.execute(query, (path,)) - try: - pathid = self.cursor.fetchone()[0] - except TypeError: - self.cursor.execute("select coalesce(max(idPath),0) from path") - pathid = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO path( - idPath, strPath) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (pathid, path)) - - return pathid - - def getPath(self, path): - - query = ' '.join(( - - "SELECT idPath", - "FROM path", - "WHERE strPath = ?" - )) - self.cursor.execute(query, (path,)) - try: - pathid = self.cursor.fetchone()[0] - except TypeError: - pathid = None - - return pathid - - def addFile(self, filename, pathid): - - query = ' '.join(( - - "SELECT idFile", - "FROM files", - "WHERE strFilename = ?", - "AND idPath = ?" - )) - self.cursor.execute(query, (filename, pathid,)) - try: - fileid = self.cursor.fetchone()[0] - except TypeError: - self.cursor.execute("select coalesce(max(idFile),0) from files") - fileid = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO files( - idFile, strFilename) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (fileid, filename)) - - return fileid - - def getFile(self, fileid): - - query = ' '.join(( - - "SELECT strFilename", - "FROM files", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (fileid,)) - try: - filename = self.cursor.fetchone()[0] - except TypeError: - filename = "" - - return filename - - def removeFile(self, path, filename): - - pathid = self.getPath(path) - - if pathid is not None: - query = ' '.join(( - - "DELETE FROM files", - "WHERE idPath = ?", - "AND strFilename = ?" - )) - self.cursor.execute(query, (pathid, filename,)) - - def addCountries(self, kodiid, countries, mediatype): - - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - for country in countries: - query = ' '.join(( - - "SELECT country_id", - "FROM country", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) - - try: - country_id = self.cursor.fetchone()[0] - - except TypeError: - # Country entry does not exists - self.cursor.execute("select coalesce(max(country_id),0) from country") - country_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(country_id, name) values(?, ?)" - self.cursor.execute(query, (country_id, country)) - log.debug("Add country to media, processing: %s" % country) - - finally: # Assign country to content - query = ( - ''' - INSERT OR REPLACE INTO country_link( - country_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (country_id, kodiid, mediatype)) - else: - # Kodi Helix - for country in countries: - query = ' '.join(( - - "SELECT idCountry", - "FROM country", - "WHERE strCountry = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (country,)) - - try: - idCountry = self.cursor.fetchone()[0] - - except TypeError: - # Country entry does not exists - self.cursor.execute("select coalesce(max(idCountry),0) from country") - idCountry = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" - self.cursor.execute(query, (idCountry, country)) - log.debug("Add country to media, processing: %s" % country) - - finally: - # Only movies have a country field - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO countrylinkmovie( - idCountry, idMovie) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (idCountry, kodiid)) - - def addPeople(self, kodiid, people, mediatype): - - castorder = 1 - for person in people: - - name = person['Name'] - person_type = person['Type'] - thumb = person['imageurl'] - - # Kodi Isengard, Jarvis, Krypton - if self.kodiversion in (15, 16, 17): - query = ' '.join(( - - "SELECT actor_id", - "FROM actor", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - - try: - actorid = self.cursor.fetchone()[0] - - except TypeError: - # Cast entry does not exists - self.cursor.execute("select coalesce(max(actor_id),0) from actor") - actorid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO actor(actor_id, name) values(?, ?)" - self.cursor.execute(query, (actorid, name)) - log.debug("Add people to media, processing: %s" % name) - - finally: - # Link person to content - if "Actor" in person_type: - role = person.get('Role') - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type, role, cast_order) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype, role, castorder)) - castorder += 1 - - elif "Director" in person_type: - query = ( - ''' - INSERT OR REPLACE INTO director_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) - - elif person_type in ("Writing", "Writer"): - query = ( - ''' - INSERT OR REPLACE INTO writer_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) - - elif "Artist" in person_type: - query = ( - ''' - INSERT OR REPLACE INTO actor_link( - actor_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid, mediatype)) - # Kodi Helix - else: - query = ' '.join(( - - "SELECT idActor", - "FROM actors", - "WHERE strActor = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - - try: - actorid = self.cursor.fetchone()[0] - - except TypeError: - # Cast entry does not exists - self.cursor.execute("select coalesce(max(idActor),0) from actors") - actorid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO actors(idActor, strActor) values(?, ?)" - self.cursor.execute(query, (actorid, name)) - log.debug("Add people to media, processing: %s" % name) - - finally: - # Link person to content - if "Actor" in person_type: - role = person.get('Role') - - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkmovie( - idActor, idMovie, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinktvshow( - idActor, idShow, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO actorlinkepisode( - idActor, idEpisode, strRole, iOrder) - - VALUES (?, ?, ?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (actorid, kodiid, role, castorder)) - castorder += 1 - - elif "Director" in person_type: - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmovie( - idDirector, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinktvshow( - idDirector, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkmusicvideo( - idDirector, idMVideo) - - VALUES (?, ?) - ''' - ) - - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO directorlinkepisode( - idDirector, idEpisode) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (actorid, kodiid)) - - elif person_type in ("Writing", "Writer"): - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkmovie( - idWriter, idMovie) - - VALUES (?, ?) - ''' - ) - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO writerlinkepisode( - idWriter, idEpisode) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (actorid, kodiid)) - - elif "Artist" in person_type: - query = ( - ''' - INSERT OR REPLACE INTO artistlinkmusicvideo( - idArtist, idMVideo) - - VALUES (?, ?) - ''' - ) - self.cursor.execute(query, (actorid, kodiid)) - - # Add person image to art table - if thumb: - arttype = person_type.lower() - - if "writing" in arttype: - arttype = "writer" - - self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor) - - def addGenres(self, kodiid, genres, mediatype): - - - # Kodi Isengard, Jarvis, Krypton - if self.kodiversion in (15, 16, 17): - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM genre_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT genre_id", - "FROM genre", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - genre_id = self.cursor.fetchone()[0] - - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(genre_id),0) from genre") - genre_id = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(genre_id, name) values(?, ?)" - self.cursor.execute(query, (genre_id, genre)) - log.debug("Add Genres to media, processing: %s" % genre) - - finally: - # Assign genre to item - query = ( - ''' - INSERT OR REPLACE INTO genre_link( - genre_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (genre_id, kodiid, mediatype)) - else: - # Kodi Helix - # Delete current genres for clean slate - if "movie" in mediatype: - self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,)) - elif "tvshow" in mediatype: - self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,)) - elif "musicvideo" in mediatype: - self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,)) - - # Add genres - for genre in genres: - - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - - try: - idGenre = self.cursor.fetchone()[0] - - except TypeError: - # Create genre in database - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - idGenre = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (idGenre, genre)) - log.debug("Add Genres to media, processing: %s" % genre) - - finally: - # Assign genre to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmovie( - idGenre, idMovie) - - VALUES (?, ?) - ''' - ) - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinktvshow( - idGenre, idShow) - - VALUES (?, ?) - ''' - ) - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE into genrelinkmusicvideo( - idGenre, idMVideo) - - VALUES (?, ?) - ''' - ) - else: return # Item is invalid - - self.cursor.execute(query, (idGenre, kodiid)) - - def addStudios(self, kodiid, studios, mediatype): - - for studio in studios: - - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "SELECT studio_id", - "FROM studio", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studioid = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(studio_id),0) from studio") - studioid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(studio_id, name) values(?, ?)" - self.cursor.execute(query, (studioid, studio)) - log.debug("Add Studios to media, processing: %s" % studio) - - finally: # Assign studio to item - query = ( - ''' - INSERT OR REPLACE INTO studio_link( - studio_id, media_id, media_type) - - VALUES (?, ?, ?) - ''') - self.cursor.execute(query, (studioid, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idstudio", - "FROM studio", - "WHERE strstudio = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (studio,)) - try: - studioid = self.cursor.fetchone()[0] - - except TypeError: - # Studio does not exists. - self.cursor.execute("select coalesce(max(idstudio),0) from studio") - studioid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" - self.cursor.execute(query, (studioid, studio)) - log.debug("Add Studios to media, processing: %s" % studio) - - finally: # Assign studio to item - if "movie" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) - VALUES (?, ?) - ''') - elif "musicvideo" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) - VALUES (?, ?) - ''') - elif "tvshow" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) - VALUES (?, ?) - ''') - elif "episode" in mediatype: - query = ( - ''' - INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) - VALUES (?, ?) - ''') - self.cursor.execute(query, (studioid, kodiid)) - - def addStreams(self, fileid, streamdetails, runtime): - - # First remove any existing entries - self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,)) - if streamdetails: - # Video details - for videotrack in streamdetails['video']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strVideoCodec, fVideoAspect, - iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 0, videotrack['codec'], - videotrack['aspect'], videotrack['width'], videotrack['height'], - runtime ,videotrack['video3DFormat'])) - - # Audio details - for audiotrack in streamdetails['audio']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) - - VALUES (?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 1, audiotrack['codec'], - audiotrack['channels'], audiotrack['language'])) - - # Subtitles details - for subtitletrack in streamdetails['subtitle']: - query = ( - ''' - INSERT INTO streamdetails( - idFile, iStreamType, strSubtitleLanguage) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (fileid, 2, subtitletrack)) - - def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed): - - # Delete existing resume point - query = ' '.join(( - - "DELETE FROM bookmark", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (fileid,)) - - # Set watched count - query = ' '.join(( - - "UPDATE files", - "SET playCount = ?, lastPlayed = ?", - "WHERE idFile = ?" - )) - self.cursor.execute(query, (playcount, dateplayed, fileid)) - - # Set the resume bookmark - if resume_seconds: - self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") - bookmarkId = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO bookmark( - idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) - - VALUES (?, ?, ?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds, - "DVDPlayer", 1)) - - def addTags(self, kodiid, tags, mediatype): - - # First, delete any existing tags associated to the id - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?" - )) - self.cursor.execute(query, (kodiid, mediatype)) - - # Add tags - log.debug("Adding Tags: %s" % tags) - for tag in tags: - self.addTag(kodiid, tag, mediatype) - - def addTag(self, kodiid, tag, mediatype): - - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag, because it does not exist - tag_id = self.createTag(tag) - log.debug("Adding tag: %s" % tag) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO tag_link( - tag_id, media_id, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodiid, mediatype)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tag,)) - try: - tag_id = self.cursor.fetchone()[0] - - except TypeError: - # Create the tag - tag_id = self.createTag(tag) - log.debug("Adding tag: %s" % tag) - - finally: - # Assign tag to item - query = ( - ''' - INSERT OR REPLACE INTO taglinks( - idTag, idMedia, media_type) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (tag_id, kodiid, mediatype)) def createTag(self, name): - # This will create and return the tag_id if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton @@ -807,14 +39,14 @@ class Kodidb_Functions(): self.cursor.execute(query, (name,)) try: tag_id = self.cursor.fetchone()[0] - + except TypeError: self.cursor.execute("select coalesce(max(tag_id),0) from tag") tag_id = self.cursor.fetchone()[0] + 1 query = "INSERT INTO tag(tag_id, name) values(?, ?)" self.cursor.execute(query, (tag_id, name)) - log.debug("Create tag_id: %s name: %s" % (tag_id, name)) + log.debug("Create tag_id: %s name: %s", tag_id, name) else: # Kodi Helix query = ' '.join(( @@ -834,17 +66,17 @@ class Kodidb_Functions(): query = "INSERT INTO tag(idTag, strTag) values(?, ?)" self.cursor.execute(query, (tag_id, name)) - log.debug("Create idTag: %s name: %s" % (tag_id, name)) + log.debug("Create idTag: %s name: %s", tag_id, name) return tag_id def updateTag(self, oldtag, newtag, kodiid, mediatype): + # TODO: Move to video nodes eventually + log.debug("Updating: %s with %s for %s: %s", oldtag, newtag, mediatype, kodiid) - log.debug("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid)) - if self.kodiversion in (15, 16, 17): # Kodi Isengard, Jarvis, Krypton - try: + try: query = ' '.join(( "UPDATE tag_link", @@ -854,7 +86,7 @@ class Kodidb_Functions(): "AND tag_id = ?" )) self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) - except Exception as e: + except Exception: # The new tag we are going to apply already exists for this item # delete current tag instead query = ' '.join(( @@ -877,7 +109,7 @@ class Kodidb_Functions(): "AND idTag = ?" )) self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,)) - except Exception as e: + except Exception: # The new tag we are going to apply already exists for this item # delete current tag instead query = ' '.join(( @@ -888,259 +120,3 @@ class Kodidb_Functions(): "AND idTag = ?" )) self.cursor.execute(query, (kodiid, mediatype, oldtag,)) - - def removeTag(self, kodiid, tagname, mediatype): - - if self.kodiversion in (15, 16, 17): - # Kodi Isengard, Jarvis, Krypton - query = ' '.join(( - - "SELECT tag_id", - "FROM tag", - "WHERE name = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tagname,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM tag_link", - "WHERE media_id = ?", - "AND media_type = ?", - "AND tag_id = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, tag_id,)) - else: - # Kodi Helix - query = ' '.join(( - - "SELECT idTag", - "FROM tag", - "WHERE strTag = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (tagname,)) - try: - tag_id = self.cursor.fetchone()[0] - except TypeError: - return - else: - query = ' '.join(( - - "DELETE FROM taglinks", - "WHERE idMedia = ?", - "AND media_type = ?", - "AND idTag = ?" - )) - self.cursor.execute(query, (kodiid, mediatype, tag_id,)) - - def createBoxset(self, boxsetname): - - log.debug("Adding boxset: %s" % boxsetname) - query = ' '.join(( - - "SELECT idSet", - "FROM sets", - "WHERE strSet = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (boxsetname,)) - try: - setid = self.cursor.fetchone()[0] - - except TypeError: - self.cursor.execute("select coalesce(max(idSet),0) from sets") - setid = self.cursor.fetchone()[0] + 1 - - query = "INSERT INTO sets(idSet, strSet) values(?, ?)" - self.cursor.execute(query, (setid, boxsetname)) - - return setid - - def assignBoxset(self, setid, movieid): - - query = ' '.join(( - - "UPDATE movie", - "SET idSet = ?", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (setid, movieid,)) - - def removefromBoxset(self, movieid): - - query = ' '.join(( - - "UPDATE movie", - "SET idSet = null", - "WHERE idMovie = ?" - )) - self.cursor.execute(query, (movieid,)) - - def addSeason(self, showid, seasonnumber): - - query = ' '.join(( - - "SELECT idSeason", - "FROM seasons", - "WHERE idShow = ?", - "AND season = ?" - )) - self.cursor.execute(query, (showid, seasonnumber,)) - try: - seasonid = self.cursor.fetchone()[0] - except TypeError: - self.cursor.execute("select coalesce(max(idSeason),0) from seasons") - seasonid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" - self.cursor.execute(query, (seasonid, showid, seasonnumber)) - - return seasonid - - def addArtist(self, name, musicbrainz): - - query = ' '.join(( - - "SELECT idArtist, strArtist", - "FROM artist", - "WHERE strMusicBrainzArtistID = ?" - )) - self.cursor.execute(query, (musicbrainz,)) - try: - result = self.cursor.fetchone() - artistid = result[0] - artistname = result[1] - - except TypeError: - - query = ' '.join(( - - "SELECT idArtist", - "FROM artist", - "WHERE strArtist = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (name,)) - try: - artistid = self.cursor.fetchone()[0] - except TypeError: - self.cursor.execute("select coalesce(max(idArtist),0) from artist") - artistid = self.cursor.fetchone()[0] + 1 - query = ( - ''' - INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (artistid, name, musicbrainz)) - else: - if artistname != name: - query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" - self.cursor.execute(query, (name, artistid,)) - - return artistid - - def addAlbum(self, name, musicbrainz): - - query = ' '.join(( - - "SELECT idAlbum", - "FROM album", - "WHERE strMusicBrainzAlbumID = ?" - )) - self.cursor.execute(query, (musicbrainz,)) - try: - albumid = self.cursor.fetchone()[0] - except TypeError: - # Create the album - self.cursor.execute("select coalesce(max(idAlbum),0) from album") - albumid = self.cursor.fetchone()[0] + 1 - if self.kodiversion in (15, 16, 17): - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) - - VALUES (?, ?, ?, ?) - ''' - ) - self.cursor.execute(query, (albumid, name, musicbrainz, "album")) - else: # Helix - query = ( - ''' - INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) - - VALUES (?, ?, ?) - ''' - ) - self.cursor.execute(query, (albumid, name, musicbrainz)) - - return albumid - - def addMusicGenres(self, kodiid, genres, mediatype): - - if mediatype == "album": - - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM album_genre", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (kodiid,)) - - for genre in genres: - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - try: - genreid = self.cursor.fetchone()[0] - except TypeError: - # Create the genre - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (genreid, genre)) - - query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" - self.cursor.execute(query, (genreid, kodiid)) - - elif mediatype == "song": - - # Delete current genres for clean slate - query = ' '.join(( - - "DELETE FROM song_genre", - "WHERE idSong = ?" - )) - self.cursor.execute(query, (kodiid,)) - - for genre in genres: - query = ' '.join(( - - "SELECT idGenre", - "FROM genre", - "WHERE strGenre = ?", - "COLLATE NOCASE" - )) - self.cursor.execute(query, (genre,)) - try: - genreid = self.cursor.fetchone()[0] - except TypeError: - # Create the genre - self.cursor.execute("select coalesce(max(idGenre),0) from genre") - genreid = self.cursor.fetchone()[0] + 1 - query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" - self.cursor.execute(query, (genreid, genre)) - - query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" - self.cursor.execute(query, (genreid, kodiid)) \ No newline at end of file diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 66e0b661..6247a82e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -8,7 +8,6 @@ import logging import xbmc import xbmcgui -import clientinfo import downloadutils import embydb_functions as embydb import playbackutils as pbutils @@ -26,194 +25,153 @@ class KodiMonitor(xbmc.Monitor): def __init__(self): - self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() - self.doUtils = downloadutils.DownloadUtils() + xbmc.Monitor.__init__(self) - log.info("Kodi monitor started.") + self.download = downloadutils.DownloadUtils().downloadUrl + log.info("Kodi monitor started") def onScanStarted(self, library): - log.debug("Kodi library scan %s running." % library) + + log.debug("Kodi library scan %s running", library) if library == "video": window('emby_kodiScan', value="true") - + def onScanFinished(self, library): - log.debug("Kodi library scan %s finished." % library) + + log.debug("Kodi library scan %s finished", library) if library == "video": window('emby_kodiScan', clear=True) def onSettingsChanged(self): # Monitor emby settings - # Review reset setting at a later time, need to be adjusted to account for initial setup - # changes. - '''currentPath = utils.settings('useDirectPaths') - if utils.window('emby_pluginpath') != currentPath: - # Plugin path value changed. Offer to reset - log("Changed to playback mode detected", 1) - utils.window('emby_pluginpath', value=currentPath) - resp = xbmcgui.Dialog().yesno( - heading="Playback mode change detected", - line1=( - "Detected the playback mode has changed. The database " - "needs to be recreated for the change to be applied. " - "Proceed?")) - if resp: - utils.reset()''' - - currentLog = settings('logLevel') - if window('emby_logLevel') != currentLog: + current_log_level = settings('logLevel') + if window('emby_logLevel') != current_log_level: # The log level changed, set new prop - log.info("New log level: %s" % currentLog) - window('emby_logLevel', value=currentLog) + log.info("New log level: %s", current_log_level) + window('emby_logLevel', value=current_log_level) + + current_context = "true" if settings('enableContext') == "true" else "" + if window('emby_context') != current_context: + log.info("New context setting: %s", current_context) + window('emby_context', value=current_context) def onNotification(self, sender, method, data): - doUtils = self.doUtils - if method not in ("Playlist.OnAdd"): - log.info("Method: %s Data: %s" % (method, data)) - + if method not in ('Playlist.OnAdd', 'Player.OnStop', 'Player.OnClear'): + log.info("Method: %s Data: %s", method, data) + if data: - data = json.loads(data,'utf-8') + data = json.loads(data, 'utf-8') + if method == 'Player.OnPlay': + self._on_play_(data) - if method == "Player.OnPlay": - # Set up report progress for emby playback - item = data.get('item') - try: - kodiid = item['id'] - item_type = item['type'] - except (KeyError, TypeError): - log.info("Item is invalid for playstate update.") - else: - if ((settings('useDirectPaths') == "1" and not item_type == "song") or - (item_type == "song" and settings('enableMusic') == "true")): - # Set up properties for player - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) - try: - itemid = emby_dbitem[0] - except TypeError: - log.info("No kodiId returned.") - else: - url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid - result = doUtils.downloadUrl(url) - log.debug("Item: %s" % result) + elif method == 'VideoLibrary.OnUpdate': + self._video_update(data) - playurl = None - count = 0 - while not playurl and count < 2: - try: - playurl = xbmc.Player().getPlayingFile() - except RuntimeError: - count += 1 - xbmc.sleep(200) - else: - listItem = xbmcgui.ListItem() - playback = pbutils.PlaybackUtils(result) - - if item_type == "song" and settings('streamMusic') == "true": - window('emby_%s.playmethod' % playurl, value="DirectStream") - else: - window('emby_%s.playmethod' % playurl, value="DirectPlay") - # Set properties for player.py - playback.setProperties(playurl, listItem) - finally: - embycursor.close() - - - elif method == "VideoLibrary.OnUpdate": - # Manually marking as watched/unwatched - playcount = data.get('playcount') - item = data.get('item') - try: - kodiid = item['id'] - item_type = item['type'] - except (KeyError, TypeError): - log.info("Item is invalid for playstate update.") - else: - # Send notification to the server. - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type) - try: - itemid = emby_dbitem[0] - except TypeError: - log.info("Could not find itemid in emby database.") - else: - # Stop from manually marking as watched unwatched, with actual playback. - if window('emby_skipWatched%s' % itemid) == "true": - # property is set in player.py - window('emby_skipWatched%s' % itemid, clear=True) - else: - # notify the server - url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid - if playcount != 0: - doUtils.downloadUrl(url, action_type="POST") - log.info("Mark as watched for itemid: %s" % itemid) - else: - doUtils.downloadUrl(url, action_type="DELETE") - log.info("Mark as unwatched for itemid: %s" % itemid) - finally: - embycursor.close() - - - elif method == "VideoLibrary.OnRemove": - # Removed function, because with plugin paths + clean library, it will wipe - # entire library if user has permissions. Instead, use the emby context menu available - # in Isengard and higher version - pass - '''try: - kodiid = data['id'] - type = data['type'] - except (KeyError, TypeError): - log("Item is invalid for emby deletion.", 1) - else: - # Send the delete action to the server. - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) - try: - itemid = emby_dbitem[0] - except TypeError: - log("Could not find itemid in emby database.", 1) - else: - if utils.settings('skipContextMenu') != "true": - resp = xbmcgui.Dialog().yesno( - heading="Confirm delete", - line1="Delete file on Emby Server?") - if not resp: - log("User skipped deletion.", 1) - embycursor.close() - return - - url = "{server}/emby/Items/%s?format=json" % itemid - log("Deleting request: %s" % itemid) - doUtils.downloadUrl(url, action_type="DELETE") - finally: - embycursor.close()''' - - elif method == "System.OnSleep": + elif method == 'System.OnSleep': # Connection is going to sleep log.info("Marking the server as offline. System.OnSleep activated.") window('emby_online', value="sleep") - elif method == "System.OnWake": - # Allow network to wake up - xbmc.sleep(10000) - window('emby_online', value="false") + elif method == 'System.OnWake': + self._system_wake() + + elif method == 'GUI.OnScreensaverDeactivated': + self._screensaver_deactivated() + + def _on_play_(self, data): + # Set up report progress for emby playback + try: + item = data['item'] + kodi_id = item['id'] + item_type = item['type'] + except (KeyError, TypeError): + log.info("Item is invalid for playstate update") + else: + if ((settings('useDirectPaths') == "1" and not item_type == "song") or + (item_type == "song" and settings('enableMusic') == "true")): + # Set up properties for player + item_id = self._get_item_id(kodi_id, item_type) + if item_id: + url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % item_id + result = self.download(url) + log.debug("Item: %s", result) + + playurl = None + count = 0 + while not playurl and count < 2: + try: + playurl = xbmc.Player().getPlayingFile() + except RuntimeError: + count += 1 + xbmc.sleep(200) + else: + listitem = xbmcgui.ListItem() + playback = pbutils.PlaybackUtils(result) + + if item_type == "song" and settings('streamMusic') == "true": + window('emby_%s.playmethod' % playurl, value="DirectStream") + else: + window('emby_%s.playmethod' % playurl, value="DirectPlay") + # Set properties for player.py + playback.setProperties(playurl, listitem) + + def _video_update(self, data): + # Manually marking as watched/unwatched + try: + item = data['item'] + kodi_id = item['id'] + item_type = item['type'] + except (KeyError, TypeError): + log.info("Item is invalid for playstate update") + else: + # Send notification to the server. + item_id = self._get_item_id(kodi_id, item_type) + if item_id: + # Stop from manually marking as watched unwatched, with actual playback. + if window('emby_skipWatched%s' % item_id) == "true": + # property is set in player.py + window('emby_skipWatched%s' % item_id, clear=True) + else: + # notify the server + url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % item_id + if data.get('playcount') != 0: + self.download(url, action_type="POST") + log.info("Mark as watched for itemid: %s", item_id) + else: + self.download(url, action_type="DELETE") + log.info("Mark as unwatched for itemid: %s", item_id) + + @classmethod + def _system_wake(cls): + # Allow network to wake up + xbmc.sleep(10000) + window('emby_online', value="false") + window('emby_onWake', value="true") + + @classmethod + def _screensaver_deactivated(cls): + + if settings('dbSyncScreensaver') == "true": + xbmc.sleep(5000) window('emby_onWake', value="true") + @classmethod + def _get_item_id(cls, kodi_id, item_type): - elif method == "GUI.OnScreensaverDeactivated": - if settings('dbSyncScreensaver') == "true": - xbmc.sleep(5000); - window('emby_onWake', value="true") + item_id = None + conn = kodiSQL('emby') + cursor = conn.cursor() + emby_db = embydb.Embydb_Functions(cursor) + db_item = emby_db.getItem_byKodiId(kodi_id, item_type) + cursor.close() - elif method == "Playlist.OnClear": - pass \ No newline at end of file + try: + item_id = db_item[0] + except TypeError: + log.info("Could not retrieve item Id") + + return item_id diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 46e5c4b1..9a6c75de 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -21,7 +21,9 @@ import kodidb_functions as kodidb import read_embyserver as embyserver import userclient import videonodes -from utils import window, settings, language as lang +from objects import Movies, MusicVideos, TVShows, Music +from utils import window, settings, language as lang, should_stop +from ga_client import GoogleAnalytics ################################################################################################## @@ -33,6 +35,8 @@ class LibrarySync(threading.Thread): _shared_state = {} + isFastSync = False + stop_thread = False suspend_thread = False @@ -51,12 +55,13 @@ class LibrarySync(threading.Thread): self.monitor = xbmc.Monitor() self.clientInfo = clientinfo.ClientInfo() - self.addonName = self.clientInfo.getAddonName() self.doUtils = downloadutils.DownloadUtils().downloadUrl self.user = userclient.UserClient() self.emby = embyserver.Read_EmbyServer() self.vnodes = videonodes.VideoNodes() + self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + threading.Thread.__init__(self) @@ -72,9 +77,10 @@ class LibrarySync(threading.Thread): def startSync(self): + ga = GoogleAnalytics() + # Run at start up - optional to use the server plugin if settings('SyncInstallRunDone') == "true": - # Validate views self.refreshViews() completed = False @@ -87,14 +93,18 @@ class LibrarySync(threading.Thread): for plugin in result: if plugin['Name'] == "Emby.Kodi Sync Queue": log.debug("Found server plugin.") + self.isFastSync = True + ga.sendEventData("SyncAction", "FastSync") completed = self.fastSync() break if not completed: # Fast sync failed or server plugin is not found + ga.sendEventData("SyncAction", "Sync") completed = ManualSync().sync() else: # Install sync is not completed + ga.sendEventData("SyncAction", "FullSync") completed = self.fullSync() return completed @@ -124,6 +134,8 @@ class LibrarySync(threading.Thread): return False params = {'LastUpdateDT': lastSync} + if settings('enableMusic') != "true": + params['filter'] = "music" url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json" result = self.doUtils(url, parameters=params) @@ -152,14 +164,17 @@ class LibrarySync(threading.Thread): # Save last sync time overlap = 2 - result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") try: # datetime fails when used more than once, TypeError - server_time = result['ServerDateTime'] - server_time = utils.convertDate(server_time) + if self.isFastSync: + result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json") + server_time = result['ServerDateTime'] + server_time = utils.convertDate(server_time) + else: + raise Exception("Fast sync server plugin is not enabled.") except Exception as e: # If the server plugin is not installed or an error happened. - log.error("An exception occurred: %s" % e) + log.debug("An exception occurred: %s" % e) time_now = datetime.utcnow()-timedelta(minutes=overlap) lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ') log.info("New sync time: client time -%s min: %s" % (overlap, lastSync)) @@ -171,25 +186,21 @@ class LibrarySync(threading.Thread): finally: settings('LastIncrementalSync', value=lastSync) - def shouldStop(self): - # Checkpoint during the syncing process - if self.monitor.abortRequested(): - return True - elif window('emby_shouldStop') == "true": - return True - else: # Keep going - return False - def dbCommit(self, connection): # Central commit, verifies if Kodi database update is running kodidb_scan = window('emby_kodiScan') == "true" + count = 0 while kodidb_scan: log.info("Kodi scan is running. Waiting...") kodidb_scan = window('emby_kodiScan') == "true" - if self.shouldStop(): + if count == 10: + log.info("Flag still active, but will try to commit") + window('emby_kodiScan', clear=True) + + if should_stop(): log.info("Commit unsuccessful. Sync terminated.") break @@ -197,9 +208,18 @@ class LibrarySync(threading.Thread): # Abort was requested while waiting. We should exit log.info("Commit unsuccessful.") break - else: + + count += 1 + + try: connection.commit() log.info("Commit successful.") + except sqlite3.OperationalError as error: + log.error(error) + if "database is locked" in error: + log.info("retrying...") + window('emby_kodiScan', value="true") + self.dbCommit(connection) def fullSync(self, manualrun=False, repair=False): # Only run once when first setting up. Can be run manually. @@ -214,18 +234,6 @@ class LibrarySync(threading.Thread): embyconn = utils.kodiSQL('emby') embycursor = embyconn.cursor() - # Create the tables for the emby database - # emby, view, version - embycursor.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)""") - embycursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") - embyconn.commit() - # content sync: movies, tvshows, musicvideos, music kodiconn = utils.kodiSQL('video') kodicursor = kodiconn.cursor() @@ -234,6 +242,31 @@ class LibrarySync(threading.Thread): message = "Manual sync" elif repair: message = "Repair sync" + repair_list = [] + choices = ['all', 'movies', 'musicvideos', 'tvshows'] + if music_enabled: + choices.append('music') + + if self.kodi_version > 15: + # Jarvis or higher + types = xbmcgui.Dialog().multiselect(lang(33094), choices) + if types is None: + pass + elif 0 in types: # all + choices.pop(0) + repair_list.extend(choices) + else: + for index in types: + repair_list.append(choices[index]) + else: + resp = xbmcgui.Dialog().select(lang(33094), choices) + if resp == 0: # all + choices.pop(resp) + repair_list.extend(choices) + else: + repair_list.append(choices[resp]) + + log.info("Repair queued for: %s", repair_list) else: message = "Initial sync" window('emby_initialScan', value="true") @@ -253,6 +286,10 @@ class LibrarySync(threading.Thread): 'tvshows': self.tvshows } for itemtype in process: + + if repair and itemtype not in repair_list: + continue + startTime = datetime.now() completed = process[itemtype](embycursor, kodicursor, pDialog) if not completed: @@ -278,36 +315,43 @@ class LibrarySync(threading.Thread): # sync music if music_enabled: - musicconn = utils.kodiSQL('music') - musiccursor = musicconn.cursor() - - startTime = datetime.now() - completed = self.music(embycursor, musiccursor, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - musiccursor.close() - return False + if repair and 'music' not in repair_list: + pass else: - musicconn.commit() - embyconn.commit() - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished music in: %s)" - % (str(elapsedTime).split('.')[0])) - musiccursor.close() + musicconn = utils.kodiSQL('music') + musiccursor = musicconn.cursor() + + startTime = datetime.now() + completed = self.music(embycursor, musiccursor, pDialog) + if not completed: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() + + embycursor.close() + musiccursor.close() + return False + else: + musicconn.commit() + embyconn.commit() + elapsedTime = datetime.now() - startTime + log.info("SyncDatabase (finished music in: %s)" + % (str(elapsedTime).split('.')[0])) + musiccursor.close() if pDialog: pDialog.close() + emby_db = embydb.Embydb_Functions(embycursor) + current_version = emby_db.get_version(self.clientInfo.get_version()) + window('emby_version', current_version) + embyconn.commit() embycursor.close() settings('SyncInstallRunDone', value="true") - settings("dbCreatedWithVersion", self.clientInfo.getVersion()) + self.saveLastSync() xbmc.executebuiltin('UpdateLibrary(video)') elapsedtotal = datetime.now() - starttotal @@ -352,7 +396,7 @@ class LibrarySync(threading.Thread): # Get views result = self.doUtils("{server}/emby/Users/{UserId}/Views?format=json") - grouped_views = result['Items'] + grouped_views = result['Items'] if 'Items' in result else [] ordered_views = self.emby.getViews(sortedlist=True) all_views = [] sorted_views = [] @@ -537,6 +581,8 @@ class LibrarySync(threading.Thread): totalnodes += 1 self.vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites") totalnodes += 1 + self.vnodes.singleNode(totalnodes, "Favorite episodes", "episodes", "favourites") + totalnodes += 1 self.vnodes.singleNode(totalnodes, "channels", "movies", "channels") totalnodes += 1 # Save total @@ -546,12 +592,17 @@ class LibrarySync(threading.Thread): log.info("Removing views: %s" % current_views) for view in current_views: emby_db.removeView(view) + # Remove any items that belongs to the old view + items = emby_db.get_item_by_view(view) + items = [i[0] for i in items] # Convert list of tuple to list + self.triage_items("remove", items) + def movies(self, embycursor, kodicursor, pdialog): # Get movies from emby emby_db = embydb.Embydb_Functions(embycursor) - movies = itemtypes.Movies(embycursor, kodicursor) + movies = Movies(embycursor, kodicursor, pdialog) views = emby_db.getView_byType('movies') views += emby_db.getView_byType('mixed') @@ -560,64 +611,27 @@ class LibrarySync(threading.Thread): ##### PROCESS MOVIES ##### for view in views: - if self.shouldStop(): - return False + log.info("Processing: %s", view) + view_name = view['name'] # Get items per view if pdialog: pdialog.update( heading=lang(29999), - message="%s %s..." % (lang(33017), view['name'])) + message="%s %s..." % (lang(33017), view_name)) - # Initial or repair sync - all_embymovies = self.emby.getMovies(view['id'], dialog=pdialog) - total = all_embymovies['TotalRecordCount'] - embymovies = all_embymovies['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) - - count = 0 - for embymovie in embymovies: - # Process individual movies - if self.shouldStop(): - return False - - title = embymovie['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_update(embymovie, view['name'], view['id']) - else: - log.debug("Movies finished.") + all_movies = self.emby.getMovies(view['id'], dialog=pdialog) + movies.add_all("Movie", all_movies, view) + log.debug("Movies finished.") ##### PROCESS BOXSETS ##### if pdialog: pdialog.update(heading=lang(29999), message=lang(33018)) boxsets = self.emby.getBoxset(dialog=pdialog) - total = boxsets['TotalRecordCount'] - embyboxsets = boxsets['Items'] - - if pdialog: - pdialog.update(heading="Processing Boxsets / %s items" % total) - - count = 0 - for boxset in embyboxsets: - # Process individual boxset - if self.shouldStop(): - return False - - title = boxset['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - movies.add_updateBoxset(boxset) - else: - log.debug("Boxsets finished.") + movies.add_all("BoxSet", boxsets) + log.debug("Boxsets finished.") return True @@ -625,15 +639,13 @@ class LibrarySync(threading.Thread): # Get musicvideos from emby emby_db = embydb.Embydb_Functions(embycursor) - mvideos = itemtypes.MusicVideos(embycursor, kodicursor) + mvideos = MusicVideos(embycursor, kodicursor, pdialog) views = emby_db.getView_byType('musicvideos') log.info("Media folders: %s" % views) for view in views: - - if self.shouldStop(): - return False + log.info("Processing: %s", view) # Get items per view viewId = view['id'] @@ -645,25 +657,9 @@ class LibrarySync(threading.Thread): message="%s %s..." % (lang(33019), viewName)) # Initial or repair sync - all_embymvideos = self.emby.getMusicVideos(viewId, dialog=pdialog) - total = all_embymvideos['TotalRecordCount'] - embymvideos = all_embymvideos['Items'] + all_mvideos = self.emby.getMusicVideos(viewId, dialog=pdialog) + mvideos.add_all("MusicVideo", all_mvideos, view) - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymvideo in embymvideos: - # Process individual musicvideo - if self.shouldStop(): - return False - - title = embymvideo['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - mvideos.add_update(embymvideo, viewName, viewId) else: log.debug("MusicVideos finished.") @@ -673,7 +669,7 @@ class LibrarySync(threading.Thread): # Get shows from emby emby_db = embydb.Embydb_Functions(embycursor) - tvshows = itemtypes.TVShows(embycursor, kodicursor) + tvshows = TVShows(embycursor, kodicursor, pdialog) views = emby_db.getView_byType('tvshows') views += emby_db.getView_byType('mixed') @@ -681,47 +677,15 @@ class LibrarySync(threading.Thread): for view in views: - if self.shouldStop(): - return False - # Get items per view if pdialog: pdialog.update( heading=lang(29999), message="%s %s..." % (lang(33020), view['name'])) - all_embytvshows = self.emby.getShows(view['id'], dialog=pdialog) - total = all_embytvshows['TotalRecordCount'] - embytvshows = all_embytvshows['Items'] + all_tvshows = self.emby.getShows(view['id'], dialog=pdialog) + tvshows.add_all("Series", all_tvshows, view) - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) - - count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.shouldStop(): - return False - - title = embytvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_update(embytvshow, view['name'], view['id']) - - # Process episodes - all_episodes = self.emby.getEpisodesbyShow(embytvshow['Id']) - for episode in all_episodes['Items']: - - # Process individual show - if self.shouldStop(): - return False - - episodetitle = episode['Name'] - if pdialog: - pdialog.update(percentage, message="%s - %s" % (title, episodetitle)) - tvshows.add_updateEpisode(episode) else: log.debug("TVShows finished.") @@ -730,41 +694,21 @@ class LibrarySync(threading.Thread): def music(self, embycursor, kodicursor, pdialog): # Get music from emby emby_db = embydb.Embydb_Functions(embycursor) - music = itemtypes.Music(embycursor, kodicursor) + music = Music(embycursor, kodicursor, pdialog) - process = { + views = emby_db.getView_byType('music') + log.info("Media folders: %s", views) - 'artists': [self.emby.getArtists, music.add_updateArtist], - 'albums': [self.emby.getAlbums, music.add_updateAlbum], - 'songs': [self.emby.getSongs, music.add_updateSong] - } - for itemtype in ['artists', 'albums', 'songs']: + # Add music artists and everything will fall into place + if pdialog: + pdialog.update(heading=lang(29999), + message="%s Music..." % lang(33021)) - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33021), itemtype)) + for view in views: + all_artists = self.emby.getArtists(view['id'], dialog=pdialog) + music.add_all("MusicArtist", all_artists) - all_embyitems = process[itemtype][0](dialog=pdialog) - total = all_embyitems['TotalRecordCount'] - embyitems = all_embyitems['Items'] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (itemtype, total)) - - count = 0 - for embyitem in embyitems: - # Process individual item - if self.shouldStop(): - return False - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=embyitem['Name']) - count += 1 - - process[itemtype][1](embyitem) - else: - log.debug("%s finished." % itemtype) + log.debug("Finished syncing music") return True @@ -805,7 +749,7 @@ class LibrarySync(threading.Thread): self.forceLibraryUpdate = True update_embydb = True - incSyncIndicator = int(settings('incSyncIndicator')) + incSyncIndicator = int(settings('incSyncIndicator') or 10) totalUpdates = len(self.addedItems) + len(self.updateItems) + len(self.userdataItems) + len(self.removeItems) if incSyncIndicator != -1 and totalUpdates > incSyncIndicator: @@ -883,8 +827,12 @@ class LibrarySync(threading.Thread): def compareDBVersion(self, current, minimum): # It returns True is database is up to date. False otherwise. log.info("current: %s minimum: %s" % (current, minimum)) - currMajor, currMinor, currPatch = current.split(".") - minMajor, minMinor, minPatch = minimum.split(".") + + try: + currMajor, currMinor, currPatch = current.split(".") + minMajor, minMinor, minPatch = minimum.split(".") + except ValueError as error: + raise ValueError("Unable to compare versions: %s, %s" % (current, minimum)) if currMajor > minMajor: return True @@ -895,20 +843,46 @@ class LibrarySync(threading.Thread): # Database out of date. return False + @classmethod + def _verify_emby_database(cls): + # Create the tables for the emby database + conn = utils.kodiSQL('emby') + cursor = conn.cursor() + # emby, view, version + 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)""") + cursor.execute( + """CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") + cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") + + conn.commit() + def run(self): try: self.run_internal() + except Warning as e: + if "restricted" in e: + pass + elif "401" in e: + pass except Exception as e: + ga = GoogleAnalytics() + errStrings = ga.formatException() + ga.sendEventData("Exception", errStrings[0], errStrings[1]) window('emby_dbScan', clear=True) + log.exception(e) xbmcgui.Dialog().ok( heading=lang(29999), line1=( "Library sync thread has exited! " "You should restart Kodi now. " - "Please report this on the forum.")) - log.exception(e) - raise + "Please report this on the forum."), + line2=(errStrings[0] + " (" + errStrings[1] + ")")) def run_internal(self): @@ -918,6 +892,9 @@ class LibrarySync(threading.Thread): log.warn("---===### Starting LibrarySync ###===---") + # Verify database structure, otherwise create it. + self._verify_emby_database() + while not self.monitor.abortRequested(): # In the event the server goes offline @@ -929,7 +906,20 @@ class LibrarySync(threading.Thread): if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): # Verify the validity of the database - currentVersion = settings('dbCreatedWithVersion') + + embyconn = utils.kodiSQL('emby') + embycursor = embyconn.cursor() + emby_db = embydb.Embydb_Functions(embycursor) + currentVersion = emby_db.get_version() + ###$ Begin migration $### + if not currentVersion: + currentVersion = emby_db.get_version(settings('dbCreatedWithVersion') or self.clientInfo.get_version()) + embyconn.commit() + log.info("Migration of database version completed") + ###$ End migration $### + embycursor.close() + window('emby_version', value=currentVersion) + minVersion = window('emby_minDBVersion') uptoDate = self.compareDBVersion(currentVersion, minVersion) @@ -963,22 +953,27 @@ class LibrarySync(threading.Thread): dialog.ok( heading=lang(29999), line1=lang(33024)) - break + break # Run start up sync - log.warn("Database version: %s" % settings('dbCreatedWithVersion')) + log.warn("Database version: %s", window('emby_version')) log.info("SyncDatabase (started)") startTime = datetime.now() librarySync = self.startSync() elapsedTime = datetime.now() - startTime log.info("SyncDatabase (finished in: %s) %s" % (str(elapsedTime).split('.')[0], librarySync)) + + # Add other servers at this point + # TODO: re-add once plugin listing is created + # self.user.load_connect_servers() + # Only try the initial sync once per kodi session regardless # This will prevent an infinite loop in case something goes wrong. startupComplete = True # Process updates - if window('emby_dbScan') != "true": + if window('emby_dbScan') != "true" and window('emby_shouldStop') != "true": self.incrementalSync() if window('emby_onWake') == "true" and window('emby_online') == "true": @@ -1018,449 +1013,19 @@ class ManualSync(LibrarySync): def __init__(self): - LibrarySync.__init__(self) def sync(self): - return self.fullSync(manualrun=True) - def movies(self, embycursor, kodicursor, pdialog): - - # Get movies from emby - emby_db = embydb.Embydb_Functions(embycursor) - movies = itemtypes.Movies(embycursor, kodicursor) - - views = emby_db.getView_byType('movies') - views += emby_db.getView_byType('mixed') - log.info("Media folders: %s" % views) - - # Pull the list of movies and boxsets in Kodi - try: - all_kodimovies = dict(emby_db.getChecksum('Movie')) - except ValueError: - all_kodimovies = {} - - try: - all_kodisets = dict(emby_db.getChecksum('BoxSet')) - except ValueError: - all_kodisets = {} - - all_embymoviesIds = set() - all_embyboxsetsIds = set() - updatelist = [] - - ##### PROCESS MOVIES ##### - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33026), viewName)) - - all_embymovies = self.emby.getMovies(viewId, basic=True, dialog=pdialog) - for embymovie in all_embymovies['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymovie) - itemid = embymovie['Id'] - all_embymoviesIds.add(itemid) - - - if all_kodimovies.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("Movies to update for %s: %s" % (viewName, updatelist)) - embymovies = self.emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymovie in embymovies: - # Process individual movies - if self.shouldStop(): - return False - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=embymovie['Name']) - count += 1 - movies.add_update(embymovie, viewName, viewId) - - ##### PROCESS BOXSETS ##### - - boxsets = self.emby.getBoxset(dialog=pdialog) - embyboxsets = [] - - if pdialog: - pdialog.update(heading=lang(29999), message=lang(33027)) - - for boxset in boxsets['Items']: - - if self.shouldStop(): - return False - - # Boxset has no real userdata, so using etag to compare - itemid = boxset['Id'] - all_embyboxsetsIds.add(itemid) - - if all_kodisets.get(itemid) != boxset['Etag']: - # Only update if boxset is not in Kodi or boxset['Etag'] is different - updatelist.append(itemid) - embyboxsets.append(boxset) - - log.info("Boxsets to update: %s" % updatelist) - total = len(updatelist) - - if pdialog: - pdialog.update(heading="Processing Boxsets / %s items" % total) - - count = 0 - for boxset in embyboxsets: - # Process individual boxset - if self.shouldStop(): - return False - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=boxset['Name']) - count += 1 - movies.add_updateBoxset(boxset) - - ##### PROCESS DELETES ##### - - for kodimovie in all_kodimovies: - if kodimovie not in all_embymoviesIds: - movies.remove(kodimovie) - else: - log.info("Movies compare finished.") - - for boxset in all_kodisets: - if boxset not in all_embyboxsetsIds: - movies.remove(boxset) - else: - log.info("Boxsets compare finished.") - - return True + return Movies(embycursor, kodicursor, pdialog).compare_all() def musicvideos(self, embycursor, kodicursor, pdialog): - - # Get musicvideos from emby - emby_db = embydb.Embydb_Functions(embycursor) - mvideos = itemtypes.MusicVideos(embycursor, kodicursor) - - views = emby_db.getView_byType('musicvideos') - log.info("Media folders: %s" % views) - - # Pull the list of musicvideos in Kodi - try: - all_kodimvideos = dict(emby_db.getChecksum('MusicVideo')) - except ValueError: - all_kodimvideos = {} - - all_embymvideosIds = set() - updatelist = [] - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33028), viewName)) - - all_embymvideos = self.emby.getMusicVideos(viewId, basic=True, dialog=pdialog) - for embymvideo in all_embymvideos['Items']: - - if self.shouldStop(): - return False - - API = api.API(embymvideo) - itemid = embymvideo['Id'] - all_embymvideosIds.add(itemid) - - - if all_kodimvideos.get(itemid) != API.getChecksum(): - # Only update if musicvideo is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("MusicVideos to update for %s: %s" % (viewName, updatelist)) - embymvideos = self.emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embymvideo in embymvideos: - # Process individual musicvideo - if self.shouldStop(): - return False - - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=embymvideo['Name']) - count += 1 - mvideos.add_update(embymvideo, viewName, viewId) - - ##### PROCESS DELETES ##### - - for kodimvideo in all_kodimvideos: - if kodimvideo not in all_embymvideosIds: - mvideos.remove(kodimvideo) - else: - log.info("MusicVideos compare finished.") - - return True + return MusicVideos(embycursor, kodicursor, pdialog).compare_all() def tvshows(self, embycursor, kodicursor, pdialog): - - # Get shows from emby - emby_db = embydb.Embydb_Functions(embycursor) - tvshows = itemtypes.TVShows(embycursor, kodicursor) - - views = emby_db.getView_byType('tvshows') - views += emby_db.getView_byType('mixed') - log.info("Media folders: %s" % views) - - # Pull the list of tvshows and episodes in Kodi - try: - all_koditvshows = dict(emby_db.getChecksum('Series')) - except ValueError: - all_koditvshows = {} - - try: - all_kodiepisodes = dict(emby_db.getChecksum('Episode')) - except ValueError: - all_kodiepisodes = {} - - all_embytvshowsIds = set() - all_embyepisodesIds = set() - updatelist = [] - - - for view in views: - - if self.shouldStop(): - return False - - # Get items per view - viewId = view['id'] - viewName = view['name'] - - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33029), viewName)) - - all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog) - for embytvshow in all_embytvshows['Items']: - - if self.shouldStop(): - return False - - API = api.API(embytvshow) - itemid = embytvshow['Id'] - all_embytvshowsIds.add(itemid) - - - if all_koditvshows.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("TVShows to update for %s: %s" % (viewName, updatelist)) - embytvshows = self.emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (viewName, total)) - - count = 0 - for embytvshow in embytvshows: - # Process individual show - if self.shouldStop(): - return False - - itemid = embytvshow['Id'] - title = embytvshow['Name'] - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_update(embytvshow, viewName, viewId) - - else: - # Get all episodes in view - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33030), viewName)) - - all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog) - for embyepisode in all_embyepisodes['Items']: - - if self.shouldStop(): - return False - - API = api.API(embyepisode) - itemid = embyepisode['Id'] - all_embyepisodesIds.add(itemid) - - if all_kodiepisodes.get(itemid) != API.getChecksum(): - # Only update if movie is not in Kodi or checksum is different - updatelist.append(itemid) - - log.info("Episodes to update for %s: %s" % (viewName, updatelist)) - embyepisodes = self.emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - - count = 0 - for episode in embyepisodes: - - # Process individual episode - if self.shouldStop(): - return False - - if pdialog: - percentage = int((float(count) / float(total))*100) - title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) - pdialog.update(percentage, message=title) - count += 1 - tvshows.add_updateEpisode(episode) - - ##### PROCESS DELETES ##### - - for koditvshow in all_koditvshows: - if koditvshow not in all_embytvshowsIds: - tvshows.remove(koditvshow) - else: - log.info("TVShows compare finished.") - - for kodiepisode in all_kodiepisodes: - if kodiepisode not in all_embyepisodesIds: - tvshows.remove(kodiepisode) - else: - log.info("Episodes compare finished.") - - return True + return TVShows(embycursor, kodicursor, pdialog).compare_all() def music(self, embycursor, kodicursor, pdialog): - - # Get music from emby - emby_db = embydb.Embydb_Functions(embycursor) - music = itemtypes.Music(embycursor, kodicursor) - - # Pull the list of artists, albums, songs - try: - all_kodiartists = dict(emby_db.getChecksum('MusicArtist')) - except ValueError: - all_kodiartists = {} - - try: - all_kodialbums = dict(emby_db.getChecksum('MusicAlbum')) - except ValueError: - all_kodialbums = {} - - try: - all_kodisongs = dict(emby_db.getChecksum('Audio')) - except ValueError: - all_kodisongs = {} - - all_embyartistsIds = set() - all_embyalbumsIds = set() - all_embysongsIds = set() - updatelist = [] - - process = { - - 'artists': [self.emby.getArtists, music.add_updateArtist], - 'albums': [self.emby.getAlbums, music.add_updateAlbum], - 'songs': [self.emby.getSongs, music.add_updateSong] - } - for data_type in ['artists', 'albums', 'songs']: - if pdialog: - pdialog.update( - heading=lang(29999), - message="%s %s..." % (lang(33031), data_type)) - if data_type != "artists": - all_embyitems = process[data_type][0](basic=True, dialog=pdialog) - else: - all_embyitems = process[data_type][0](dialog=pdialog) - for embyitem in all_embyitems['Items']: - if self.shouldStop(): - return False - API = api.API(embyitem) - itemid = embyitem['Id'] - if data_type == "artists": - all_embyartistsIds.add(itemid) - if all_kodiartists.get(itemid) != API.getChecksum(): - # Only update if artist is not in Kodi or checksum is different - updatelist.append(itemid) - elif data_type == "albums": - all_embyalbumsIds.add(itemid) - if all_kodialbums.get(itemid) != API.getChecksum(): - # Only update if album is not in Kodi or checksum is different - updatelist.append(itemid) - else: - all_embysongsIds.add(itemid) - if all_kodisongs.get(itemid) != API.getChecksum(): - # Only update if songs is not in Kodi or checksum is different - updatelist.append(itemid) - log.info("%s to update: %s" % (data_type, updatelist)) - embyitems = self.emby.getFullItems(updatelist) - total = len(updatelist) - del updatelist[:] - if pdialog: - pdialog.update(heading="Processing %s / %s items" % (data_type, total)) - count = 0 - for embyitem in embyitems: - # Process individual item - if self.shouldStop(): - return False - if pdialog: - percentage = int((float(count) / float(total))*100) - pdialog.update(percentage, message=embyitem['Name']) - count += 1 - process[data_type][1](embyitem) - ##### PROCESS DELETES ##### - for kodiartist in all_kodiartists: - if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None: - music.remove(kodiartist) - else: - log.info("Artist compare finished.") - for kodialbum in all_kodialbums: - if kodialbum not in all_embyalbumsIds: - music.remove(kodialbum) - else: - log.info("Albums compare finished.") - for kodisong in all_kodisongs: - if kodisong not in all_embysongsIds: - music.remove(kodisong) - else: - log.info("Songs compare finished.") - return True \ No newline at end of file + return Music(embycursor, kodicursor).compare_all() diff --git a/resources/lib/loghandler.py b/resources/lib/loghandler.py index b576e078..5169d361 100644 --- a/resources/lib/loghandler.py +++ b/resources/lib/loghandler.py @@ -11,7 +11,7 @@ from utils import window def config(): - + logger = logging.getLogger('EMBY') logger.addHandler(LogHandler()) logger.setLevel(logging.DEBUG) @@ -20,19 +20,20 @@ def config(): class LogHandler(logging.StreamHandler): def __init__(self): - + logging.StreamHandler.__init__(self) self.setFormatter(MyFormatter()) def emit(self, record): - if self._getLogLevel(record.levelno): + if self._get_log_level(record.levelno): try: - xbmc.log(self.format(record)) + xbmc.log(self.format(record), level=xbmc.LOGNOTICE) except UnicodeEncodeError: - xbmc.log(self.format(record).encode('utf-8')) + xbmc.log(self.format(record).encode('utf-8'), level=xbmc.LOGNOTICE) - def _getLogLevel(self, level): + @classmethod + def _get_log_level(cls, level): levels = { logging.ERROR: 0, @@ -41,17 +42,17 @@ class LogHandler(logging.StreamHandler): logging.DEBUG: 2 } try: - logLevel = int(window('emby_logLevel')) + log_level = int(window('emby_logLevel')) except ValueError: - logLevel = 0 + log_level = 0 - return logLevel >= levels[level] + 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): @@ -70,4 +71,4 @@ class MyFormatter(logging.Formatter): # Restore the original format configured by the user self._fmt = format_orig - return result \ No newline at end of file + return result diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py index 1f5bd9e9..2a02638d 100644 --- a/resources/lib/musicutils.py +++ b/resources/lib/musicutils.py @@ -74,7 +74,7 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei emby = embyserver.Read_EmbyServer() previous_values = None - filename = API.getFilePath() + filename = API.get_file_path() rating = 0 emby_rating = int(round(emby_rating, 0)) diff --git a/resources/lib/objects/__init__.py b/resources/lib/objects/__init__.py new file mode 100644 index 00000000..5219542a --- /dev/null +++ b/resources/lib/objects/__init__.py @@ -0,0 +1,5 @@ +# Dummy file to make this directory a package. +from movies import Movies +from musicvideos import MusicVideos +from tvshows import TVShows +from music import Music diff --git a/resources/lib/objects/_common.py b/resources/lib/objects/_common.py new file mode 100644 index 00000000..0f022e3d --- /dev/null +++ b/resources/lib/objects/_common.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmc +import xbmcvfs + +import api +import artwork +import downloadutils +import read_embyserver as embyserver +from ga_client import GoogleAnalytics +from utils import window, settings, dialog, language as lang, should_stop + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +ga = GoogleAnalytics() + +################################################################################################## + +def catch_except(errors=(Exception, ), default_value=False): +# Will wrap method with try/except and print parameters for easier debugging + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except errors as error: + errStrings = ga.formatException() + ga.sendEventData("Exception", errStrings[0], errStrings[1]) + log.exception(error) + log.error("function: %s \n args: %s \n kwargs: %s", + func.__name__, args, kwargs) + return default_value + + return wrapper + return decorator + + +class Items(object): + + pdialog = None + title = None + count = 0 + total = 0 + + + def __init__(self): + + self.artwork = artwork.Artwork() + self.emby = embyserver.Read_EmbyServer() + self.do_url = downloadutils.DownloadUtils().downloadUrl + self.should_stop = should_stop + + self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + self.direct_path = settings('useDirectPaths') == "1" + self.content_msg = settings('newContent') == "true" + + @classmethod + def path_validation(cls, path): + # Verify if direct path is accessible or not + if not os.path.supports_unicode_filenames: + path = path.encode('utf-8') + + if window('emby_pathverified') != "true" and not xbmcvfs.exists(path): + if dialog(type_="yesno", + heading="{emby}", + line1="%s %s. %s" % (lang(33047), path, lang(33048))): + + window('emby_shouldStop', value="true") + return False + + return True + + def content_pop(self, name): + # It's possible for the time to be 0. It should be considered disabled in this case. + if not self.pdialog and self.content_msg and self.new_time: + dialog(type_="notification", + heading="{emby}", + message="%s %s" % (lang(33049), name), + icon="{emby}", + time=self.new_time, + sound=False) + + def update_pdialog(self): + + if self.pdialog: + percentage = int((float(self.count) / float(self.total))*100) + self.pdialog.update(percentage, message=self.title) + + def add_all(self, item_type, items, view=None): + + if self.should_stop(): + return False + + total = items['TotalRecordCount'] if 'TotalRecordCount' in items else len(items) + items = items['Items'] if 'Items' in items else items + + if self.pdialog and view: + self.pdialog.update(heading="Processing %s / %s items" % (view['name'], total)) + + process = self._get_func(item_type, "added") + if view: + process(items, total, view) + else: + process(items, total) + + def process_all(self, item_type, action, items, total=None, view=None): + + log.debug("Processing %s: %s", action, items) + + process = self._get_func(item_type, action) + self.total = total or len(items) + self.count = 0 + + for item in items: + + if self.should_stop(): + return False + + if not process: + continue + + self.title = item.get('Name', "unknown") + self.update_pdialog() + + process(item) + self.count += 1 + + def remove_all(self, item_type, items): + + log.debug("Processing removal: %s", items) + + process = self._get_func(item_type, "remove") + for item in items: + process(item) + + def added(self, items, total=None, update=True): + # Generator for newly added content + if update: + self.total = total or len(items) + self.count = 0 + + for item in items: + + if self.should_stop(): + break + + self.title = item.get('Name', "unknown") + + yield item + self.update_pdialog() + + if update: + self.count += 1 + + def compare(self, item_type, items, compare_to, view=None): + + view_name = view['name'] if view else item_type + + update_list = self._compare_checksum(items, compare_to) + log.info("Update for %s: %s", view_name, update_list) + + if self.should_stop(): + return False + + emby_items = self.emby.getFullItems(update_list) + total = len(update_list) + + if self.pdialog: + self.pdialog.update(heading="Processing %s / %s items" % (view_name, total)) + + # Process additions and updates + if emby_items: + self.process_all(item_type, "update", emby_items, total, view) + # Process deletes + if compare_to: + self.remove_all(item_type, compare_to.items()) + + return True + + def _compare_checksum(self, items, compare_to): + + update_list = list() + + for item in items: + + if self.should_stop(): + break + + item_id = item['Id'] + + if compare_to.get(item_id) != api.API(item).get_checksum(): + # Only update if item is not in Kodi or checksum is different + update_list.append(item_id) + + compare_to.pop(item_id, None) + + return update_list diff --git a/resources/lib/objects/_kodi_common.py b/resources/lib/objects/_kodi_common.py new file mode 100644 index 00000000..bc703a53 --- /dev/null +++ b/resources/lib/objects/_kodi_common.py @@ -0,0 +1,813 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc + +import artwork + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class KodiItems(object): + + + def __init__(self): + + self.artwork = artwork.Artwork() + self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + + def create_entry_path(self): + self.cursor.execute("select coalesce(max(idPath),0) from path") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_file(self): + self.cursor.execute("select coalesce(max(idFile),0) from files") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_person(self): + self.cursor.execute("select coalesce(max(actor_id),0) from actor") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_genre(self): + self.cursor.execute("select coalesce(max(genre_id),0) from genre") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_studio(self): + self.cursor.execute("select coalesce(max(studio_id),0) from studio") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_bookmark(self): + self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_tag(self): + self.cursor.execute("select coalesce(max(tag_id),0) from tag") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def add_path(self, path): + + path_id = self.get_path(path) + if path_id is None: + # Create a new entry + path_id = self.create_entry_path() + query = ( + ''' + INSERT INTO path(idPath, strPath) + + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (path_id, path)) + + return path_id + + def get_path(self, path): + + query = ' '.join(( + + "SELECT idPath", + "FROM path", + "WHERE strPath = ?" + )) + self.cursor.execute(query, (path,)) + try: + path_id = self.cursor.fetchone()[0] + except TypeError: + path_id = None + + return path_id + + def update_path(self, path_id, path, media_type, scraper): + + query = ' '.join(( + + "UPDATE path", + "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?", + "WHERE idPath = ?" + )) + self.cursor.execute(query, (path, media_type, scraper, 1, path_id)) + + def remove_path(self, path_id): + self.cursor.execute("DELETE FROM path WHERE idPath = ?", (path_id,)) + + def add_file(self, filename, path_id): + + query = ' '.join(( + + "SELECT idFile", + "FROM files", + "WHERE strFilename = ?", + "AND idPath = ?" + )) + self.cursor.execute(query, (filename, path_id,)) + try: + file_id = self.cursor.fetchone()[0] + except TypeError: + # Create a new entry + file_id = self.create_entry_file() + query = ( + ''' + INSERT INTO files(idFile, idPath, strFilename) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (file_id, path_id, filename)) + + return file_id + + def update_file(self, file_id, filename, path_id, date_added): + + query = ' '.join(( + + "UPDATE files", + "SET idPath = ?, strFilename = ?, dateAdded = ?", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (path_id, filename, date_added, file_id)) + + def remove_file(self, path, filename): + + path_id = self.get_path(path) + if path_id is not None: + + query = ' '.join(( + + "DELETE FROM files", + "WHERE idPath = ?", + "AND strFilename = ?" + )) + self.cursor.execute(query, (path_id, filename,)) + + def get_filename(self, file_id): + + query = ' '.join(( + + "SELECT strFilename", + "FROM files", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (file_id,)) + try: + filename = self.cursor.fetchone()[0] + except TypeError: + filename = "" + + return filename + + def add_people(self, kodi_id, people, media_type): + + def add_thumbnail(person_id, person, type_): + + thumbnail = person['imageurl'] + if thumbnail: + + art = type_.lower() + if "writing" in art: + art = "writer" + + self.artwork.add_update_art(thumbnail, person_id, art, "thumb", self.cursor) + + def add_link(link_type, person_id, kodi_id, media_type): + + query = ( + "INSERT OR REPLACE INTO " + link_type + "(actor_id, media_id, media_type)" + "VALUES (?, ?, ?)" + ) + self.cursor.execute(query, (person_id, kodi_id, media_type)) + + cast_order = 1 + + if self.kodi_version > 14: + + for person in people: + + name = person['Name'] + type_ = person['Type'] + person_id = self._get_person(name) + + # Link person to content + if type_ == "Actor": + role = person.get('Role') + query = ( + ''' + INSERT OR REPLACE INTO actor_link( + actor_id, media_id, media_type, role, cast_order) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (person_id, kodi_id, media_type, role, cast_order)) + cast_order += 1 + + elif type_ == "Director": + add_link("director_link", person_id, kodi_id, media_type) + + elif type_ in ("Writing", "Writer"): + add_link("writer_link", person_id, kodi_id, media_type) + + elif type_ == "Artist": + add_link("actor_link", person_id, kodi_id, media_type) + + add_thumbnail(person_id, person, type_) + else: + # TODO: Remove Helix code when Krypton is RC + for person in people: + name = person['Name'] + type_ = person['Type'] + + query = ' '.join(( + + "SELECT idActor", + "FROM actors", + "WHERE strActor = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + + try: + person_id = self.cursor.fetchone()[0] + + except TypeError: + # Cast entry does not exists + self.cursor.execute("select coalesce(max(idActor),0) from actors") + person_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO actors(idActor, strActor) values(?, ?)" + self.cursor.execute(query, (person_id, name)) + log.debug("Add people to media, processing: %s", name) + + finally: + # Link person to content + if type_ == "Actor": + role = person.get('Role') + + if media_type == "movie": + query = ( + ''' + INSERT OR REPLACE INTO actorlinkmovie( + idActor, idMovie, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + elif media_type == "tvshow": + query = ( + ''' + INSERT OR REPLACE INTO actorlinktvshow( + idActor, idShow, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + elif media_type == "episode": + query = ( + ''' + INSERT OR REPLACE INTO actorlinkepisode( + idActor, idEpisode, strRole, iOrder) + + VALUES (?, ?, ?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (person_id, kodi_id, role, cast_order)) + cast_order += 1 + + elif type_ == "Director": + if media_type == "movie": + query = ( + ''' + INSERT OR REPLACE INTO directorlinkmovie(idDirector, idMovie) + VALUES (?, ?) + ''' + ) + elif media_type == "tvshow": + query = ( + ''' + INSERT OR REPLACE INTO directorlinktvshow(idDirector, idShow) + VALUES (?, ?) + ''' + ) + elif media_type == "musicvideo": + query = ( + ''' + INSERT OR REPLACE INTO directorlinkmusicvideo(idDirector, idMVideo) + VALUES (?, ?) + ''' + ) + elif media_type == "episode": + query = ( + ''' + INSERT OR REPLACE INTO directorlinkepisode(idDirector, idEpisode) + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (person_id, kodi_id)) + + elif type_ in ("Writing", "Writer"): + if media_type == "movie": + query = ( + ''' + INSERT OR REPLACE INTO writerlinkmovie(idWriter, idMovie) + VALUES (?, ?) + ''' + ) + elif media_type == "episode": + query = ( + ''' + INSERT OR REPLACE INTO writerlinkepisode(idWriter, idEpisode) + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (person_id, kodi_id)) + + elif type_ == "Artist": + query = ( + ''' + INSERT OR REPLACE INTO artistlinkmusicvideo(idArtist, idMVideo) + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (person_id, kodi_id)) + + add_thumbnail(person_id, person, type_) + + def _add_person(self, name): + + person_id = self.create_entry_person() + query = "INSERT INTO actor(actor_id, name) values(?, ?)" + self.cursor.execute(query, (person_id, name)) + log.debug("Add people to media, processing: %s", name) + + return person_id + + def _get_person(self, name): + + query = ' '.join(( + + "SELECT actor_id", + "FROM actor", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + + try: + person_id = self.cursor.fetchone()[0] + except TypeError: + person_id = self._add_person(name) + + return person_id + + def add_genres(self, kodi_id, genres, media_type): + + if self.kodi_version > 14: + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM genre_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodi_id, media_type,)) + + # Add genres + for genre in genres: + + genre_id = self._get_genre(genre) + query = ( + ''' + INSERT OR REPLACE INTO genre_link( + genre_id, media_id, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (genre_id, kodi_id, media_type)) + else: + # TODO: Remove Helix code when Krypton is RC + # Delete current genres for clean slate + if media_type == "movie": + self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodi_id,)) + elif media_type == "tvshow": + self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodi_id,)) + elif media_type == "musicvideo": + self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodi_id,)) + + # Add genres + for genre in genres: + + query = ' '.join(( + + "SELECT idGenre", + "FROM genre", + "WHERE strGenre = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + + try: + genre_id = self.cursor.fetchone()[0] + + except TypeError: + # Create genre in database + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + genre_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" + self.cursor.execute(query, (genre_id, genre)) + log.debug("Add Genres to media, processing: %s", genre) + + finally: + # Assign genre to item + if media_type == "movie": + query = ( + ''' + INSERT OR REPLACE into genrelinkmovie(idGenre, idMovie) + VALUES (?, ?) + ''' + ) + elif media_type == "tvshow": + query = ( + ''' + INSERT OR REPLACE into genrelinktvshow(idGenre, idShow) + VALUES (?, ?) + ''' + ) + elif media_type == "musicvideo": + query = ( + ''' + INSERT OR REPLACE into genrelinkmusicvideo(idGenre, idMVideo) + VALUES (?, ?) + ''' + ) + else: return # Item is invalid + + self.cursor.execute(query, (genre_id, kodi_id)) + + def _add_genre(self, genre): + + genre_id = self.create_entry_genre() + query = "INSERT INTO genre(genre_id, name) values(?, ?)" + self.cursor.execute(query, (genre_id, genre)) + log.debug("Add Genres to media, processing: %s", genre) + + return genre_id + + def _get_genre(self, genre): + + query = ' '.join(( + + "SELECT genre_id", + "FROM genre", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + + try: + genre_id = self.cursor.fetchone()[0] + except TypeError: + genre_id = self._add_genre(genre) + + return genre_id + + def add_studios(self, kodi_id, studios, media_type): + + if self.kodi_version > 14: + + for studio in studios: + + studio_id = self._get_studio(studio) + query = ( + ''' + INSERT OR REPLACE INTO studio_link(studio_id, media_id, media_type) + VALUES (?, ?, ?) + ''') + self.cursor.execute(query, (studio_id, kodi_id, media_type)) + else: + # TODO: Remove Helix code when Krypton is RC + for studio in studios: + + query = ' '.join(( + + "SELECT idstudio", + "FROM studio", + "WHERE strstudio = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (studio,)) + try: + studio_id = self.cursor.fetchone()[0] + + except TypeError: + # Studio does not exists. + self.cursor.execute("select coalesce(max(idstudio),0) from studio") + studio_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)" + self.cursor.execute(query, (studio_id, studio)) + log.debug("Add Studios to media, processing: %s", studio) + + finally: # Assign studio to item + if media_type == "movie": + query = ( + ''' + INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie) + VALUES (?, ?) + ''') + elif media_type == "musicvideo": + query = ( + ''' + INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo) + VALUES (?, ?) + ''') + elif media_type == "tvshow": + query = ( + ''' + INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow) + VALUES (?, ?) + ''') + elif media_type == "episode": + query = ( + ''' + INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode) + VALUES (?, ?) + ''') + self.cursor.execute(query, (studio_id, kodi_id)) + + def _add_studio(self, studio): + + studio_id = self.create_entry_studio() + query = "INSERT INTO studio(studio_id, name) values(?, ?)" + self.cursor.execute(query, (studio_id, studio)) + log.debug("Add Studios to media, processing: %s", studio) + + return studio_id + + def _get_studio(self, studio): + + query = ' '.join(( + + "SELECT studio_id", + "FROM studio", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (studio,)) + try: + studio_id = self.cursor.fetchone()[0] + except TypeError: + studio_id = self._add_studio(studio) + + return studio_id + + def add_streams(self, file_id, streams, runtime): + # First remove any existing entries + self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (file_id,)) + if streams: + # Video details + for track in streams['video']: + query = ( + ''' + INSERT INTO streamdetails( + idFile, iStreamType, strVideoCodec, fVideoAspect, + iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (file_id, 0, track['codec'], track['aspect'], + track['width'], track['height'], runtime, + track['video3DFormat'])) + # Audio details + for track in streams['audio']: + query = ( + ''' + INSERT INTO streamdetails( + idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (file_id, 1, track['codec'], track['channels'], + track['language'])) + # Subtitles details + for track in streams['subtitle']: + query = ( + ''' + INSERT INTO streamdetails(idFile, iStreamType, strSubtitleLanguage) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (file_id, 2, track)) + + def add_playstate(self, file_id, resume, total, playcount, date_played): + + # Delete existing resume point + self.cursor.execute("DELETE FROM bookmark WHERE idFile = ?", (file_id,)) + # Set watched count + self.set_playcount(file_id, playcount, date_played) + + if resume: + bookmark_id = self.create_entry_bookmark() + query = ( + ''' + INSERT INTO bookmark( + idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type) + + VALUES (?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (bookmark_id, file_id, resume, total, "DVDPlayer", 1)) + + def set_playcount(self, file_id, playcount, date_played): + + query = ' '.join(( + + "UPDATE files", + "SET playCount = ?, lastPlayed = ?", + "WHERE idFile = ?" + )) + self.cursor.execute(query, (playcount, date_played, file_id)) + + def add_tags(self, kodi_id, tags, media_type): + + if self.kodi_version > 14: + + query = ' '.join(( + + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodi_id, media_type)) + + # Add tags + log.debug("Adding Tags: %s", tags) + for tag in tags: + tag_id = self.get_tag(kodi_id, tag, media_type) + else: + # TODO: Remove Helix code when Krypton is RC + query = ' '.join(( + + "DELETE FROM taglinks", + "WHERE idMedia = ?", + "AND media_type = ?" + )) + self.cursor.execute(query, (kodi_id, media_type)) + + # Add tags + log.debug("Adding Tags: %s", tags) + for tag in tags: + tag_id = self.get_tag_old(kodi_id, tag, media_type) + + def _add_tag(self, tag): + + tag_id = self.create_entry_tag() + query = "INSERT INTO tag(tag_id, name) values(?, ?)" + self.cursor.execute(query, (tag_id, tag)) + log.debug("Create tag_id: %s name: %s", tag_id, tag) + + return tag_id + + def get_tag(self, kodi_id, tag, media_type): + + if self.kodi_version > 14: + + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + tag_id = self._add_tag(tag) + + query = ( + ''' + INSERT OR REPLACE INTO tag_link(tag_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (tag_id, kodi_id, media_type)) + else: + # TODO: Remove Helix code when Krypton is RC + tag_id = self.get_tag_old(kodi_id, tag, media_type) + + return tag_id + + def get_tag_old(self, kodi_id, tag, media_type): + # TODO: Remove Helix code when Krypton is RC + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + + except TypeError: + # Create the tag + self.cursor.execute("select coalesce(max(idTag),0) from tag") + tag_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO tag(idTag, strTag) values(?, ?)" + self.cursor.execute(query, (tag_id, name)) + log.debug("Create idTag: %s name: %s", tag_id, name) + + finally: + # Assign tag to item + query = ( + ''' + INSERT OR REPLACE INTO taglinks( + idTag, idMedia, media_type) + + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (tag_id, kodi_id, media_type)) + + return tag_id + + def remove_tag(self, kodi_id, tag, media_type): + + if self.kodi_version > 14: + + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + else: + query = ' '.join(( + + "DELETE FROM tag_link", + "WHERE media_id = ?", + "AND media_type = ?", + "AND tag_id = ?" + )) + self.cursor.execute(query, (kodi_id, media_type, tag_id,)) + else: + # TODO: Remove Helix code when Krypton is RC + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (tag,)) + try: + tag_id = self.cursor.fetchone()[0] + except TypeError: + return + else: + query = ' '.join(( + + "DELETE FROM taglinks", + "WHERE idMedia = ?", + "AND media_type = ?", + "AND idTag = ?" + )) + self.cursor.execute(query, (kodi_id, media_type, tag_id,)) diff --git a/resources/lib/objects/_kodi_movies.py b/resources/lib/objects/_kodi_movies.py new file mode 100644 index 00000000..46700b93 --- /dev/null +++ b/resources/lib/objects/_kodi_movies.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +from _kodi_common import KodiItems + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class KodiMovies(KodiItems): + + + def __init__(self, cursor): + self.cursor = cursor + + KodiItems.__init__(self) + + def create_entry(self): + self.cursor.execute("select coalesce(max(idMovie),0) from movie") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_set(self): + self.cursor.execute("select coalesce(max(idSet),0) from sets") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_country(self): + self.cursor.execute("select coalesce(max(country_id),0) from country") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def get_movie(self, kodi_id): + + query = "SELECT * FROM movie WHERE idMovie = ?" + self.cursor.execute(query, (kodi_id,)) + try: + kodi_id = self.cursor.fetchone()[0] + except TypeError: + kodi_id = None + + return kodi_id + + def add_movie(self, *args): + + query = ( + ''' + INSERT INTO movie( + idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, + c09, c10, c11, c12, c14, c15, c16, c18, c19, c21) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def add_movie_17(self, *args): + # Create the movie entry + query = ( + ''' + INSERT INTO movie( + idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07, + c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def update_movie(self, *args): + + query = ' '.join(( + + "UPDATE movie", + "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", + "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", + "c16 = ?, c18 = ?, c19 = ?, c21 = ?", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (args)) + + def update_movie_17(self, *args): + + query = ' '.join(( + + "UPDATE movie", + "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,", + "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,", + "c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ?", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (args)) + + def remove_movie(self, kodi_id, file_id): + self.cursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,)) + self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) + + def add_countries(self, kodi_id, countries): + + if self.kodi_version > 14: + + for country in countries: + country_id = self._get_country(country) + + query = ( + ''' + INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (country_id, kodi_id, "movie")) + else: + # TODO: Remove Helix code when Krypton is RC + for country in countries: + query = ' '.join(( + + "SELECT idCountry", + "FROM country", + "WHERE strCountry = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (country,)) + + try: + country_id = self.cursor.fetchone()[0] + except TypeError: + # Create a new entry + self.cursor.execute("select coalesce(max(idCountry),0) from country") + country_id = self.cursor.fetchone()[0] + 1 + + query = "INSERT INTO country(idCountry, strCountry) values(?, ?)" + self.cursor.execute(query, (country_id, country)) + log.debug("Add country to media, processing: %s", country) + + query = ( + ''' + INSERT OR REPLACE INTO countrylinkmovie(idCountry, idMovie) + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (country_id, kodi_id)) + + def _add_country(self, country): + + country_id = self.create_entry_country() + query = "INSERT INTO country(country_id, name) values(?, ?)" + self.cursor.execute(query, (country_id, country)) + log.debug("Add country to media, processing: %s", country) + + return country_id + + def _get_country(self, country): + + query = ' '.join(( + + "SELECT country_id", + "FROM country", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (country,)) + try: + country_id = self.cursor.fetchone()[0] + except TypeError: + country_id = self._add_country(country) + + return country_id + + def add_boxset(self, boxset): + + query = ' '.join(( + + "SELECT idSet", + "FROM sets", + "WHERE strSet = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (boxset,)) + try: + set_id = self.cursor.fetchone()[0] + except TypeError: + set_id = self._add_boxset(boxset) + + return set_id + + def _add_boxset(self, boxset): + + set_id = self.create_entry_set() + query = "INSERT INTO sets(idSet, strSet) values(?, ?)" + self.cursor.execute(query, (set_id, boxset)) + log.debug("Adding boxset: %s", boxset) + + return set_id + + def set_boxset(self, set_id, movie_id): + + query = ' '.join(( + + "UPDATE movie", + "SET idSet = ?", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (set_id, movie_id,)) + + def remove_from_boxset(self, movie_id): + + query = ' '.join(( + + "UPDATE movie", + "SET idSet = null", + "WHERE idMovie = ?" + )) + self.cursor.execute(query, (movie_id,)) + + def remove_boxset(self, kodi_id): + self.cursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,)) diff --git a/resources/lib/objects/_kodi_music.py b/resources/lib/objects/_kodi_music.py new file mode 100644 index 00000000..eeec63b5 --- /dev/null +++ b/resources/lib/objects/_kodi_music.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +from _kodi_common import KodiItems + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class KodiMusic(KodiItems): + + + def __init__(self, cursor): + self.cursor = cursor + + KodiItems.__init__(self) + + def create_entry(self): + self.cursor.execute("select coalesce(max(idArtist),0) from artist") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_album(self): + self.cursor.execute("select coalesce(max(idAlbum),0) from album") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_song(self): + self.cursor.execute("select coalesce(max(idSong),0) from song") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_genre(self): + self.cursor.execute("select coalesce(max(idGenre),0) from genre") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def update_path(self, path_id, path): + + query = "UPDATE path SET strPath = ? WHERE idPath = ?" + self.cursor.execute(query, (path, path_id)) + + def add_role(self): + query = ( + ''' + INSERT OR REPLACE INTO role(idRole, strRole) + VALUES (?, ?) + ''' + ) + self.cursor.execute(query, (1, 'Composer')) + + def get_artist(self, name, musicbrainz): + + query = ' '.join(( + + "SELECT idArtist, strArtist", + "FROM artist", + "WHERE strMusicBrainzArtistID = ?" + )) + self.cursor.execute(query, (musicbrainz,)) + try: + result = self.cursor.fetchone() + artist_id = result[0] + artist_name = result[1] + except TypeError: + artist_id = self._add_artist(name, musicbrainz) + else: + if artist_name != name: + self.update_artist_name(artist_id, name) + + return artist_id + + def _add_artist(self, name, musicbrainz): + + query = ' '.join(( + # Safety check, when musicbrainz does not exist + "SELECT idArtist", + "FROM artist", + "WHERE strArtist = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (name,)) + try: + artist_id = self.cursor.fetchone()[0] + except TypeError: + artist_id = self.create_entry() + query = ( + ''' + INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (artist_id, name, musicbrainz)) + + return artist_id + + def update_artist_name(self, kodi_id, name): + + query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?" + self.cursor.execute(query, (name, kodi_id,)) + + def update_artist_16(self, *args): + query = ' '.join(( + + "UPDATE artist", + "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", + "lastScraped = ?", + "WHERE idArtist = ?" + )) + self.cursor.execute(query, (args)) + + def update_artist(self, *args): + query = ' '.join(( + + "UPDATE artist", + "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", + "lastScraped = ?, dateAdded = ?", + "WHERE idArtist = ?" + )) + self.cursor.execute(query, (args)) + + def link_artist(self, kodi_id, album_id, name): + query = ( + ''' + INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (kodi_id, album_id, name)) + + def add_discography(self, kodi_id, album, year): + query = ( + ''' + INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (kodi_id, album, year)) + + def get_album(self, name, musicbrainz): + + query = ' '.join(( + + "SELECT idAlbum", + "FROM album", + "WHERE strMusicBrainzAlbumID = ?" + )) + self.cursor.execute(query, (musicbrainz,)) + try: + album_id = self.cursor.fetchone()[0] + except TypeError: + album_id = self._add_album(name, musicbrainz) + + return album_id + + def _add_album(self, name, musicbrainz): + + album_id = self.create_entry_album() + if self.kodi_version > 14: + query = ( + ''' + INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType) + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (album_id, name, musicbrainz, "album")) + else: + # TODO: Remove Helix code when Krypton is RC + query = ( + ''' + INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (album_id, name, musicbrainz)) + + return album_id + + def update_album(self, *args): + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (args)) + + def update_album_17(self, *args): + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iUserrating = ?, lastScraped = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (args)) + + def update_album_15(self, *args): + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (args)) + + def update_album_14(self, *args): + # TODO: Remove Helix code when Krypton is RC + query = ' '.join(( + + "UPDATE album", + "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", + "iRating = ?, lastScraped = ?, dateAdded = ?", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (args)) + + def get_album_artist(self, album_id, artists): + + query = ' '.join(( + + "SELECT strArtists", + "FROM album", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (album_id,)) + try: + curr_artists = self.cursor.fetchone()[0] + except TypeError: + return + + if curr_artists != artists: + self._update_album_artist(album_id, artists) + + def _update_album_artist(self, album_id, artists): + + query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" + self.cursor.execute(query, (artists, album_id)) + + def add_single(self, *args): + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) + + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def add_single_15(self, *args): + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def add_single_14(self, *args): + # TODO: Remove Helix code when Krypton is RC + query = ( + ''' + INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) + + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def add_song(self, *args): + query = ( + ''' + INSERT INTO song( + idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, + iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, + rating) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def update_song(self, *args): + query = ' '.join(( + + "UPDATE song", + "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", + "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", + "rating = ?, comment = ?", + "WHERE idSong = ?" + )) + self.cursor.execute(query, (args)) + + def link_song_artist(self, kodi_id, song_id, index, artist): + + if self.kodi_version > 16: + query = ( + ''' + INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (kodi_id, song_id, 1, index, artist)) + else: + query = ( + ''' + INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) + VALUES (?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (kodi_id, song_id, index, artist)) + + def link_song_album(self, song_id, album_id, track, title, duration): + query = ( + ''' + INSERT OR REPLACE INTO albuminfosong( + idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) + + VALUES (?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (song_id, album_id, track, title, duration)) + + def rate_song(self, kodi_id, playcount, rating, date_played): + + query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?" + self.cursor.execute(query, (playcount, date_played, rating, kodi_id)) + + def add_genres(self, kodi_id, genres, media_type): + + if media_type == "album": + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM album_genre", + "WHERE idAlbum = ?" + )) + self.cursor.execute(query, (kodi_id,)) + + for genre in genres: + + genre_id = self.get_genre(genre) + query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)" + self.cursor.execute(query, (genre_id, kodi_id)) + + elif media_type == "song": + # Delete current genres for clean slate + query = ' '.join(( + + "DELETE FROM song_genre", + "WHERE idSong = ?" + )) + self.cursor.execute(query, (kodi_id,)) + + for genre in genres: + + genre_id = self.get_genre(genre) + query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)" + self.cursor.execute(query, (genre_id, kodi_id)) + + def get_genre(self, genre): + + query = ' '.join(( + + "SELECT idGenre", + "FROM genre", + "WHERE strGenre = ?", + "COLLATE NOCASE" + )) + self.cursor.execute(query, (genre,)) + try: + genre_id = self.cursor.fetchone()[0] + except TypeError: + genre_id = self._add_genre(genre) + + return genre_id + + def _add_genre(self, genre): + + genre_id = self.create_entry_genre() + query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)" + self.cursor.execute(query, (genre_id, genre)) + + return genre_id + + def remove_artist(self, kodi_id): + self.cursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodi_id,)) + + def remove_album(self, kodi_id): + self.cursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodi_id,)) + + def remove_song(self, kodi_id): + self.cursor.execute("DELETE FROM song WHERE idSong = ?", (kodi_id,)) diff --git a/resources/lib/objects/_kodi_musicvideos.py b/resources/lib/objects/_kodi_musicvideos.py new file mode 100644 index 00000000..adf6d342 --- /dev/null +++ b/resources/lib/objects/_kodi_musicvideos.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +from _kodi_common import KodiItems + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class KodiMusicVideos(KodiItems): + + + def __init__(self, cursor): + self.cursor = cursor + + KodiItems.__init__(self) + + def create_entry(self): + self.cursor.execute("select coalesce(max(idMVideo),0) from musicvideo") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def get_musicvideo(self, kodi_id): + + query = "SELECT * FROM musicvideo WHERE idMVideo = ?" + self.cursor.execute(query, (kodi_id,)) + try: + kodi_id = self.cursor.fetchone()[0] + except TypeError: + kodi_id = None + + return kodi_id + + def add_musicvideo(self, *args): + + query = ( + ''' + INSERT INTO musicvideo( + idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def update_musicvideo(self, *args): + + query = ' '.join(( + + "UPDATE musicvideo", + "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,", + "c11 = ?, c12 = ?" + "WHERE idMVideo = ?" + )) + self.cursor.execute(query, (args)) + + def remove_musicvideo(self, kodi_id, file_id): + self.cursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (kodi_id,)) + self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) diff --git a/resources/lib/objects/_kodi_tvshows.py b/resources/lib/objects/_kodi_tvshows.py new file mode 100644 index 00000000..917cc25e --- /dev/null +++ b/resources/lib/objects/_kodi_tvshows.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +from _kodi_common import KodiItems + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class KodiTVShows(KodiItems): + + + def __init__(self, cursor): + self.cursor = cursor + + KodiItems.__init__(self) + + def create_entry(self): + self.cursor.execute("select coalesce(max(idShow),0) from tvshow") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_season(self): + self.cursor.execute("select coalesce(max(idSeason),0) from seasons") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def create_entry_episode(self): + self.cursor.execute("select coalesce(max(idEpisode),0) from episode") + kodi_id = self.cursor.fetchone()[0] + 1 + + return kodi_id + + def get_tvshow(self, kodi_id): + + query = "SELECT * FROM tvshow WHERE idShow = ?" + self.cursor.execute(query, (kodi_id,)) + try: + kodi_id = self.cursor.fetchone()[0] + except TypeError: + kodi_id = None + + return kodi_id + + def get_episode(self, kodi_id): + + query = "SELECT * FROM episode WHERE idEpisode = ?" + self.cursor.execute(query, (kodi_id,)) + try: + kodi_id = self.cursor.fetchone()[0] + except TypeError: + kodi_id = None + + return kodi_id + + def add_tvshow(self, *args): + + query = ( + ''' + INSERT INTO tvshow(idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def update_tvshow(self, *args): + + query = ' '.join(( + + "UPDATE tvshow", + "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?", + "WHERE idShow = ?" + )) + self.cursor.execute(query, (args)) + + def link_tvshow(self, show_id, path_id): + query = "INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) values(?, ?)" + self.cursor.execute(query, (show_id, path_id)) + + def get_season(self, show_id, number, name=None): + + query = ' '.join(( + + "SELECT idSeason", + "FROM seasons", + "WHERE idShow = ?", + "AND season = ?" + )) + self.cursor.execute(query, (show_id, number,)) + try: + season_id = self.cursor.fetchone()[0] + except TypeError: + season_id = self._add_season(show_id, number) + + if self.kodi_version > 15 and name is not None: + query = "UPDATE seasons SET name = ? WHERE idSeason = ?" + self.cursor.execute(query, (name, season_id)) + + return season_id + + def _add_season(self, show_id, number): + + season_id = self.create_entry_season() + query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)" + self.cursor.execute(query, (season_id, show_id, number)) + + return season_id + + def add_episode(self, *args): + query = ( + ''' + INSERT INTO episode( + idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, + idShow, c15, c16) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def add_episode_16(self, *args): + query = ( + ''' + INSERT INTO episode( + idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14, + idShow, c15, c16, idSeason) + + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''' + ) + self.cursor.execute(query, (args)) + + def update_episode(self, *args): + query = ' '.join(( + + "UPDATE episode", + "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idShow = ?", + "WHERE idEpisode = ?" + )) + self.cursor.execute(query, (args)) + + def update_episode_16(self, *args): + query = ' '.join(( + + "UPDATE episode", + "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,", + "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?, idShow = ?", + "WHERE idEpisode = ?" + )) + self.cursor.execute(query, (args)) + + def remove_tvshow(self, kodi_id): + self.cursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) + + def remove_season(self, kodi_id): + self.cursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,)) + + def remove_episode(self, kodi_id, file_id): + self.cursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) + self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,)) diff --git a/resources/lib/objects/movies.py b/resources/lib/objects/movies.py new file mode 100644 index 00000000..07690bcb --- /dev/null +++ b/resources/lib/objects/movies.py @@ -0,0 +1,453 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import urllib + +import api +import embydb_functions as embydb +import _kodi_movies +from _common import Items, catch_except +from utils import window, settings, language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Movies(Items): + + + def __init__(self, embycursor, kodicursor, pdialog=None): + + self.embycursor = embycursor + self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.kodicursor = kodicursor + self.kodi_db = _kodi_movies.KodiMovies(self.kodicursor) + self.pdialog = pdialog + + self.new_time = int(settings('newvideotime'))*1000 + + Items.__init__(self) + + def _get_func(self, item_type, action): + + if item_type == "Movie": + actions = { + 'added': self.added, + 'update': self.add_update, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + elif item_type == "BoxSet": + actions = { + 'added': self.added_boxset, + 'update': self.add_updateBoxset, + 'remove': self.remove + } + else: + log.info("Unsupported item_type: %s", item_type) + actions = {} + + return actions.get(action) + + def compare_all(self): + # Pull the list of movies and boxsets in Kodi + views = self.emby_db.getView_byType('movies') + views += self.emby_db.getView_byType('mixed') + log.info("Media folders: %s", views) + + # Process movies + for view in views: + + if self.should_stop(): + return False + + if not self.compare_movies(view): + return False + + # Process boxsets + if not self.compare_boxsets(): + return False + + return True + + def compare_movies(self, view): + + view_id = view['id'] + view_name = view['name'] + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33026), view_name)) + + movies = dict(self.emby_db.get_checksum_by_view("Movie", view_id)) + emby_movies = self.emby.getMovies(view_id, basic=True, dialog=self.pdialog) + + return self.compare("Movie", emby_movies['Items'], movies, view) + + def compare_boxsets(self): + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message=lang(33027)) + + boxsets = dict(self.emby_db.get_checksum('BoxSet')) + emby_boxsets = self.emby.getBoxset(dialog=self.pdialog) + + return self.compare("BoxSet", emby_boxsets['Items'], boxsets) + + def added(self, items, total=None, view=None): + + for item in super(Movies, self).added(items, total): + if self.add_update(item, view): + self.content_pop(item.get('Name', "unknown")) + + def added_boxset(self, items, total=None): + + for item in super(Movies, self).added(items, total): + self.add_updateBoxset(item) + + @catch_except() + def add_update(self, item, view=None): + # Process single movie + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + log.info("movieid: %s fileid: %s pathid: %s", movieid, fileid, pathid) + + except TypeError: + update_item = False + log.debug("movieid: %s not found", itemid) + # movieid + movieid = self.kodi_db.create_entry() + + else: + if self.kodi_db.get_movie(movieid) is None: + # item is not found, let's recreate it. + update_item = False + log.info("movieid: %s missing from Kodi, repairing the entry", movieid) + + if not view: + # Get view tag from emby + viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) + log.debug("View tag found: %s", viewtag) + else: + viewtag = view['name'] + viewid = view['id'] + + # fileId information + checksum = API.get_checksum() + dateadded = API.get_date_created() + userdata = API.get_userdata() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + people = API.get_people() + writer = " / ".join(people['Writer']) + director = " / ".join(people['Director']) + genres = item['Genres'] + title = item['Name'] + plot = API.get_overview() + shortplot = item.get('ShortOverview') + tagline = API.get_tagline() + votecount = item.get('VoteCount') + rating = item.get('CommunityRating') + year = item.get('ProductionYear') + imdb = API.get_provider('Imdb') + sorttitle = item['SortName'] + runtime = API.get_runtime() + mpaa = API.get_mpaa() + genre = " / ".join(genres) + country = API.get_country() + studios = API.get_studios() + try: + studio = studios[0] + except IndexError: + studio = None + + if item.get('LocalTrailerCount'): + # There's a local trailer + url = ( + "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json" + % itemid + ) + result = self.do_url(url) + try: + trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id'] + except IndexError: + log.info("Failed to process local trailer.") + trailer = None + else: + # Try to get the youtube trailer + try: + trailer = item['RemoteTrailers'][0]['Url'] + except (KeyError, IndexError): + trailer = None + else: + try: + trailer_id = trailer.rsplit('=', 1)[1] + except IndexError: + log.info("Failed to process trailer: %s", trailer) + trailer = None + else: + trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailer_id + + + ##### GET THE FILE AND PATH ##### + playurl = API.get_file_path() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.direct_path: + # Direct paths is set the Kodi way + if not self.path_validation(playurl): + return False + + path = playurl.replace(filename, "") + window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.movies/" + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': movieid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE MOVIE ##### + if update_item: + log.info("UPDATE movie itemid: %s - Title: %s", itemid, title) + + # Update the movie entry + if self.kodi_version > 16: + self.kodi_db.update_movie_17(title, plot, shortplot, tagline, votecount, rating, + writer, year, imdb, sorttitle, runtime, mpaa, genre, + director, title, studio, trailer, country, year, + movieid) + else: + self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, rating, + writer, year, imdb, sorttitle, runtime, mpaa, genre, + director, title, studio, trailer, country, movieid) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE MOVIE ##### + else: + log.info("ADD movie itemid: %s - Title: %s", itemid, title) + + # Add path + pathid = self.kodi_db.add_path(path) + # Add the file + fileid = self.kodi_db.add_file(filename, pathid) + + # Create the movie entry + if self.kodi_version > 16: + self.kodi_db.add_movie_17(movieid, fileid, title, plot, shortplot, tagline, + votecount, rating, writer, year, imdb, sorttitle, + runtime, mpaa, genre, director, title, studio, trailer, + country, year) + else: + self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline, + votecount, rating, writer, year, imdb, sorttitle, + runtime, mpaa, genre, director, title, studio, trailer, + country) + + # Create the reference in emby table + emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, + checksum, viewid) + + # Update the path + self.kodi_db.update_path(pathid, path, "movies", "metadata.local") + # Update the file + self.kodi_db.update_file(fileid, filename, pathid, dateadded) + + # Process countries + if 'ProductionLocations' in item: + self.kodi_db.add_countries(movieid, item['ProductionLocations']) + # Process cast + people = artwork.get_people_artwork(item['People']) + self.kodi_db.add_people(movieid, people, "movie") + # Process genres + self.kodi_db.add_genres(movieid, genres, "movie") + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(item), movieid, "movie", self.kodicursor) + # Process stream details + streams = API.get_media_streams() + self.kodi_db.add_streams(fileid, streams, runtime) + # Process studios + self.kodi_db.add_studios(movieid, studios, "movie") + # Process tags: view, emby tags + tags = [viewtag] + tags.extend(item['Tags']) + if userdata['Favorite']: + tags.append("Favorite movies") + log.info("Applied tags: %s", tags) + self.kodi_db.add_tags(movieid, tags, "movie") + # Process playstates + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + + return True + + def add_updateBoxset(self, boxset): + + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + API = api.API(boxset) + + boxsetid = boxset['Id'] + title = boxset['Name'] + checksum = API.get_checksum() + emby_dbitem = emby_db.getItem_byId(boxsetid) + try: + setid = emby_dbitem[0] + + except TypeError: + setid = self.kodi_db.add_boxset(title) + + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(boxset), setid, "set", self.kodicursor) + + # Process movies inside boxset + current_movies = emby_db.getItemId_byParentId(setid, "movie") + process = [] + try: + # Try to convert tuple to dictionary + current = dict(current_movies) + except ValueError: + current = {} + + # Sort current titles + for current_movie in current: + process.append(current_movie) + + # New list to compare + for movie in emby.getMovies_byBoxset(boxsetid)['Items']: + + itemid = movie['Id'] + + if not current.get(itemid): + # Assign boxset to movie + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + except TypeError: + log.info("Failed to add: %s to boxset", movie['Name']) + continue + + log.info("New addition to boxset %s: %s", title, movie['Name']) + self.kodi_db.set_boxset(setid, movieid) + # Update emby reference + emby_db.updateParentId(itemid, setid) + else: + # Remove from process, because the item still belongs + process.remove(itemid) + + # Process removals from boxset + for movie in process: + movieid = current[movie] + log.info("Remove from boxset %s: %s", title, movieid) + self.kodi_db.remove_from_boxset(movieid) + # Update emby reference + emby_db.updateParentId(movie, None) + + # Update the reference in the emby table + emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum) + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.get_checksum() + userdata = API.get_userdata() + runtime = API.get_runtime() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + movieid = emby_dbitem[0] + fileid = emby_dbitem[1] + log.info("Update playstate for movie: %s fileid: %s", item['Name'], fileid) + except TypeError: + return + + # Process favorite tags + if userdata['Favorite']: + self.kodi_db.get_tag(movieid, "Favorite movies", "movie") + else: + self.kodi_db.remove_tag(movieid, "Favorite movies", "movie") + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + + log.debug("%s New resume point: %s", itemid, resume) + + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove movieid, fileid, emby reference + emby_db = self.emby_db + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + mediatype = emby_dbitem[4] + log.info("Removing %sid: %s fileid: %s", mediatype, kodiid, fileid) + except TypeError: + return + + # Remove the emby reference + emby_db.removeItem(itemid) + # Remove artwork + artwork.delete_artwork(kodiid, mediatype, self.kodicursor) + + if mediatype == "movie": + self.kodi_db.remove_movie(kodiid, fileid) + + elif mediatype == "set": + # Delete kodi boxset + boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") + for movie in boxset_movies: + embyid = movie[0] + movieid = movie[1] + self.kodi_db.remove_from_boxset(movieid) + # Update emby reference + emby_db.updateParentId(embyid, None) + + self.kodi_db.remove_boxset(kodiid) + + log.info("Deleted %s %s from kodi database", mediatype, itemid) diff --git a/resources/lib/objects/music.py b/resources/lib/objects/music.py new file mode 100644 index 00000000..dc560481 --- /dev/null +++ b/resources/lib/objects/music.py @@ -0,0 +1,709 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +from datetime import datetime + +import api +import embydb_functions as embydb +import musicutils +import _kodi_music +from _common import Items, catch_except +from utils import window, settings, language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class Music(Items): + + + def __init__(self, embycursor, kodicursor, pdialog=None): + + self.embycursor = embycursor + self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.kodicursor = kodicursor + self.kodi_db = _kodi_music.KodiMusic(self.kodicursor) + self.pdialog = pdialog + + self.new_time = int(settings('newmusictime'))*1000 + self.directstream = settings('streamMusic') == "true" + self.enableimportsongrating = settings('enableImportSongRating') == "true" + self.enableexportsongrating = settings('enableExportSongRating') == "true" + self.enableupdatesongrating = settings('enableUpdateSongRating') == "true" + self.userid = window('emby_currUser') + self.server = window('emby_server%s' % self.userid) + + Items.__init__(self) + + def _get_func(self, item_type, action): + + if item_type == "MusicAlbum": + actions = { + 'added': self.added_album, + 'update': self.add_updateAlbum, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + elif item_type in ("MusicArtist", "AlbumArtist"): + actions = { + 'added': self.added, + 'update': self.add_updateArtist, + 'remove': self.remove + } + elif item_type == "Audio": + actions = { + 'added': self.added_song, + 'update': self.add_updateSong, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + else: + log.info("Unsupported item_type: %s", item_type) + actions = {} + + return actions.get(action) + + def compare_all(self): + # Pull the list of artists, albums, songs + views = self.emby_db.getView_byType('music') + + for view in views: + # Process artists + self.compare_artists(view) + # Process albums + self.compare_albums() + # Process songs + self.compare_songs() + + return True + + def compare_artists(self, view): + + all_embyartistsIds = set() + update_list = list() + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message="%s Artists..." % lang(33031)) + + artists = dict(self.emby_db.get_checksum('MusicArtist')) + album_artists = dict(self.emby_db.get_checksum('AlbumArtist')) + emby_artists = self.emby.getArtists(view['id'], dialog=self.pdialog) + + for item in emby_artists['Items']: + + if self.should_stop(): + return False + + item_id = item['Id'] + API = api.API(item) + + all_embyartistsIds.add(item_id) + if item_id in artists: + if artists[item_id] != API.get_checksum(): + # Only update if artist is not in Kodi or checksum is different + update_list.append(item_id) + elif album_artists.get(item_id) != API.get_checksum(): + # Only update if artist is not in Kodi or checksum is different + update_list.append(item_id) + + #compare_to.pop(item_id, None) + + log.info("Update for Artist: %s", update_list) + + emby_items = self.emby.getFullItems(update_list) + total = len(update_list) + + if self.pdialog: + self.pdialog.update(heading="Processing Artists / %s items" % total) + + # Process additions and updates + if emby_items: + self.process_all("MusicArtist", "update", emby_items, total) + # Process removals + for artist in artists: + if artist not in all_embyartistsIds and artists[artist] is not None: + self.remove(artist) + + def compare_albums(self): + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message="%s Albums..." % lang(33031)) + + albums = dict(self.emby_db.get_checksum('MusicAlbum')) + emby_albums = self.emby.getAlbums(basic=True, dialog=self.pdialog) + + return self.compare("MusicAlbum", emby_albums['Items'], albums) + + def compare_songs(self): + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message="%s Songs..." % lang(33031)) + + songs = dict(self.emby_db.get_checksum('Audio')) + emby_songs = self.emby.getSongs(basic=True, dialog=self.pdialog) + + return self.compare("Audio", emby_songs['Items'], songs) + + def added(self, items, total=None): + + for item in super(Music, self).added(items, total): + if self.add_updateArtist(item): + # Add albums + all_albums = self.emby.getAlbumsbyArtist(item['Id']) + self.added_album(all_albums['Items']) + + def added_album(self, items, total=None): + + update = True if not self.total else False + + for item in super(Music, self).added(items, total, update): + self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title) + + if self.add_updateAlbum(item): + # Add songs + all_songs = self.emby.getSongsbyAlbum(item['Id']) + self.added_song(all_songs['Items']) + + def added_song(self, items, total=None): + + update = True if not self.total else False + + for item in super(Music, self).added(items, total, update): + self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title) + + if self.add_updateSong(item): + self.content_pop(self.title) + + @catch_except() + def add_updateArtist(self, item, artisttype="MusicArtist"): + # Process a single artist + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + artistid = emby_dbitem[0] + except TypeError: + update_item = False + log.debug("artistid: %s not found", itemid) + else: + pass + + ##### The artist details ##### + lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + dateadded = API.get_date_created() + checksum = API.get_checksum() + + name = item['Name'] + musicBrainzId = API.get_provider('MusicBrainzArtist') + genres = " / ".join(item.get('Genres')) + bio = API.get_overview() + + # Associate artwork + artworks = artwork.get_all_artwork(item, parent_info=True) + thumb = artworks['Primary'] + backdrops = artworks['Backdrop'] # List + + if thumb: + thumb = "%s" % thumb + if backdrops: + fanart = "%s" % backdrops[0] + else: + fanart = "" + + + ##### UPDATE THE ARTIST ##### + if update_item: + log.info("UPDATE artist itemid: %s - Name: %s", itemid, name) + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE ARTIST ##### + else: + log.info("ADD artist itemid: %s - Name: %s", itemid, name) + # 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. + artistid = self.kodi_db.get_artist(name, musicBrainzId) + # Create the reference in emby table + emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum) + + # Process the artist + if self.kodi_version > 15: + self.kodi_db.update_artist_16(genres, bio, thumb, fanart, lastScraped, artistid) + else: + self.kodi_db.update_artist(genres, bio, thumb, fanart, lastScraped, dateadded, artistid) + + # Update artwork + artwork.add_artwork(artworks, artistid, "artist", kodicursor) + + return True + + @catch_except() + def add_updateAlbum(self, item): + # Process a single artist + emby = self.emby + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + albumid = emby_dbitem[0] + except TypeError: + update_item = False + log.debug("albumid: %s not found", itemid) + + ##### The album details ##### + lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + dateadded = API.get_date_created() + userdata = API.get_userdata() + checksum = API.get_checksum() + + name = item['Name'] + musicBrainzId = API.get_provider('MusicBrainzAlbum') + year = item.get('ProductionYear') + genres = item.get('Genres') + genre = " / ".join(genres) + bio = API.get_overview() + rating = 0 + artists = item['AlbumArtists'] + artistname = [] + for artist in artists: + artistname.append(artist['Name']) + artistname = " / ".join(artistname) + + # Associate artwork + artworks = artwork.get_all_artwork(item, parent_info=True) + thumb = artworks['Primary'] + if thumb: + thumb = "%s" % thumb + + ##### UPDATE THE ALBUM ##### + if update_item: + log.info("UPDATE album itemid: %s - Name: %s", itemid, name) + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE ALBUM ##### + else: + log.info("ADD album itemid: %s - Name: %s", itemid, name) + # 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. + albumid = self.kodi_db.get_album(name, musicBrainzId) + # Create the reference in emby table + emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum) + + # Process the album info + if self.kodi_version == 17: + # Kodi Krypton + self.kodi_db.update_album_17(artistname, year, genre, bio, thumb, rating, lastScraped, + "album", albumid) + elif self.kodi_version == 16: + # Kodi Jarvis + self.kodi_db.update_album(artistname, year, genre, bio, thumb, rating, lastScraped, + "album", albumid) + elif self.kodi_version == 15: + # Kodi Isengard + self.kodi_db.update_album_15(artistname, year, genre, bio, thumb, rating, lastScraped, + dateadded, "album", albumid) + else: + # TODO: Remove Helix code when Krypton is RC + self.kodi_db.update_album_14(artistname, year, genre, bio, thumb, rating, lastScraped, + dateadded, albumid) + + # Assign main artists to album + for artist in item['AlbumArtists']: + artistname = artist['Name'] + artistId = artist['Id'] + emby_dbartist = emby_db.getItem_byId(artistId) + try: + artistid = emby_dbartist[0] + except TypeError: + # Artist does not exist in emby database, create the reference + artist = emby.getItem(artistId) + self.add_updateArtist(artist, artisttype="AlbumArtist") + emby_dbartist = emby_db.getItem_byId(artistId) + artistid = emby_dbartist[0] + else: + # Best take this name over anything else. + self.kodi_db.update_artist_name(artistid, artistname) + + # Add artist to album + self.kodi_db.link_artist(artistid, albumid, artistname) + # Update emby reference with parentid + emby_db.updateParentId(artistId, albumid) + + for artist in item['ArtistItems']: + artistId = artist['Id'] + emby_dbartist = emby_db.getItem_byId(artistId) + try: + artistid = emby_dbartist[0] + except TypeError: + pass + else: + # Update discography + self.kodi_db.add_discography(artistid, name, year) + + # Add genres + self.kodi_db.add_genres(albumid, genres, "album") + # Update artwork + artwork.add_artwork(artworks, albumid, "album", kodicursor) + + return True + + @catch_except() + def add_updateSong(self, item): + # Process single song + kodicursor = self.kodicursor + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + songid = emby_dbitem[0] + pathid = emby_dbitem[2] + albumid = emby_dbitem[3] + except TypeError: + update_item = False + log.debug("songid: %s not found", itemid) + songid = self.kodi_db.create_entry_song() + + ##### The song details ##### + checksum = API.get_checksum() + dateadded = API.get_date_created() + userdata = API.get_userdata() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + title = item['Name'] + musicBrainzId = API.get_provider('MusicBrainzTrackId') + genres = item.get('Genres') + genre = " / ".join(genres) + artists = " / ".join(item['Artists']) + tracknumber = item.get('IndexNumber', 0) + disc = item.get('ParentIndexNumber', 1) + if disc == 1: + track = tracknumber + else: + track = disc*2**16 + tracknumber + year = item.get('ProductionYear') + duration = API.get_runtime() + rating = 0 + + #if enabled, try to get the rating from file and/or emby + if not self.directstream: + rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) + else: + hasEmbeddedCover = False + comment = API.get_overview() + + + ##### GET THE FILE AND PATH ##### + if self.directstream: + path = "%s/emby/Audio/%s/" % (self.server, itemid) + extensions = ['mp3', 'aac', 'ogg', 'oga', 'webma', 'wma', 'flac'] + + if 'Container' in item and item['Container'].lower() in extensions: + filename = "stream.%s?static=true" % item['Container'] + else: + filename = "stream.mp3?static=true" + else: + playurl = API.get_file_path() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + # Direct paths is set the Kodi way + if not self.path_validation(playurl): + return False + + path = playurl.replace(filename, "") + window('emby_pathverified', value="true") + + ##### UPDATE THE SONG ##### + if update_item: + log.info("UPDATE song itemid: %s - Title: %s", itemid, title) + + # Update path + self.kodi_db.update_path(pathid, path) + + # Update the song entry + self.kodi_db.update_song(albumid, artists, genre, title, track, duration, year, + filename, playcount, dateplayed, rating, comment, songid) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE SONG ##### + else: + log.info("ADD song itemid: %s - Title: %s", itemid, title) + + # Add path + pathid = self.kodi_db.add_path(path) + + try: + # Get the album + emby_dbalbum = emby_db.getItem_byId(item['AlbumId']) + albumid = emby_dbalbum[0] + except KeyError: + # Verify if there's an album associated. + album_name = item.get('Album') + if album_name: + log.info("Creating virtual music album for song: %s", itemid) + albumid = self.kodi_db.get_album(album_name, API.get_provider('MusicBrainzAlbum')) + emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album") + else: + # No album Id associated to the song. + log.error("Song itemid: %s has no albumId associated", itemid) + return False + + except TypeError: + # No album found. Let's create it + log.info("Album database entry missing.") + emby_albumId = item['AlbumId'] + album = emby.getItem(emby_albumId) + self.add_updateAlbum(album) + emby_dbalbum = emby_db.getItem_byId(emby_albumId) + try: + albumid = emby_dbalbum[0] + log.info("Found albumid: %s", albumid) + except TypeError: + # No album found, create a single's album + log.info("Failed to add album. Creating singles.") + album_id = self.kodi_db.create_entry_album() + if self.kodi_version == 16: + self.kodi_db.add_single(albumid, genre, year, "single") + + elif self.kodi_version == 15: + self.kodi_db.add_single_15(albumid, genre, year, dateadded, "single") + + else: + # TODO: Remove Helix code when Krypton is RC + self.kodi_db.add_single_14(albumid, genre, year, dateadded) + + # Create the song entry + self.kodi_db.add_song(songid, albumid, pathid, artists, genre, title, track, duration, + year, filename, musicBrainzId, playcount, dateplayed, rating) + + # Create the reference in emby table + emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid, + checksum=checksum) + + # Link song to album + self.kodi_db.link_song_album(songid, albumid, track, title, duration) + # Create default role + if self.kodi_version > 16: + self.kodi_db.add_role() + + # Link song to artists + for index, artist in enumerate(item['ArtistItems']): + + artist_name = artist['Name'] + artist_eid = artist['Id'] + artist_edb = emby_db.getItem_byId(artist_eid) + try: + artistid = artist_edb[0] + except TypeError: + # Artist is missing from emby database, add it. + artist_full = emby.getItem(artist_eid) + self.add_updateArtist(artist_full) + artist_edb = emby_db.getItem_byId(artist_eid) + artistid = artist_edb[0] + finally: + # Link song to artist + self.kodi_db.link_song_artist(artistid, songid, index, artist_name) + + # Verify if album artist exists + album_artists = [] + for artist in item['AlbumArtists']: + + artist_name = artist['Name'] + album_artists.append(artist_name) + artist_eid = artist['Id'] + artist_edb = emby_db.getItem_byId(artist_eid) + try: + artistid = artist_edb[0] + except TypeError: + # Artist is missing from emby database, add it. + artist_full = emby.getItem(artist_eid) + self.add_updateArtist(artist_full) + artist_edb = emby_db.getItem_byId(artist_eid) + artistid = artist_edb[0] + finally: + # Link artist to album + self.kodi_db.link_artist(artistid, albumid, artist_name) + # Update discography + if item.get('Album'): + self.kodi_db.add_discography(artistid, item['Album'], 0) + + # Artist names + album_artists = " / ".join(album_artists) + self.kodi_db.get_album_artist(albumid, album_artists) + + # Add genres + self.kodi_db.add_genres(songid, genres, "song") + + # Update artwork + allart = artwork.get_all_artwork(item, parent_info=True) + if hasEmbeddedCover: + allart["Primary"] = "image://music@" + artwork.single_urlencode(playurl) + artwork.add_artwork(allart, songid, "song", kodicursor) + + if item.get('AlbumId') is None: + # Update album artwork + artwork.add_artwork(allart, albumid, "album", kodicursor) + + return True + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + kodicursor = self.kodicursor + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.get_checksum() + userdata = API.get_userdata() + rating = 0 + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + mediatype = emby_dbitem[4] + log.info("Update playstate for %s: %s", mediatype, item['Name']) + except TypeError: + return + + if mediatype == "song": + + #should we ignore this item ? + #happens when userdata updated by ratings method + if window("ignore-update-%s" %itemid): + window("ignore-update-%s" %itemid,clear=True) + return + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + #process item ratings + rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating) + self.kodi_db.rate_song(playcount, dateplayed, rating, kodiid) + + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove kodiid, fileid, pathid, emby reference + emby_db = self.emby_db + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + mediatype = emby_dbitem[4] + log.info("Removing %s kodiid: %s", mediatype, kodiid) + except TypeError: + return + + ##### PROCESS ITEM ##### + + # Remove the emby reference + emby_db.removeItem(itemid) + + + ##### IF SONG ##### + + if mediatype == "song": + # Delete song + self.removeSong(kodiid) + # This should only address single song scenario, where server doesn't actually + # create an album for the song. + emby_db.removeWildItem(itemid) + + for item in emby_db.getItem_byWildId(itemid): + + item_kid = item[0] + item_mediatype = item[1] + + if item_mediatype == "album": + childs = emby_db.getItem_byParentId(item_kid, "song") + if not childs: + # Delete album + self.removeAlbum(item_kid) + + ##### IF ALBUM ##### + + elif mediatype == "album": + # Delete songs, album + album_songs = emby_db.getItem_byParentId(kodiid, "song") + for song in album_songs: + self.removeSong(song[1]) + else: + # Remove emby songs + emby_db.removeItems_byParentId(kodiid, "song") + + # Remove the album + self.removeAlbum(kodiid) + + ##### IF ARTIST ##### + + elif mediatype == "artist": + # Delete songs, album, artist + albums = emby_db.getItem_byParentId(kodiid, "album") + for album in albums: + albumid = album[1] + album_songs = emby_db.getItem_byParentId(albumid, "song") + for song in album_songs: + self.removeSong(song[1]) + else: + # Remove emby song + emby_db.removeItems_byParentId(albumid, "song") + # Remove emby artist + emby_db.removeItems_byParentId(albumid, "artist") + # Remove kodi album + self.removeAlbum(albumid) + else: + # Remove emby albums + emby_db.removeItems_byParentId(kodiid, "album") + + # Remove artist + self.removeArtist(kodiid) + + log.info("Deleted %s: %s from kodi database", mediatype, itemid) + + def removeSong(self, kodi_id): + + self.artwork.delete_artwork(kodi_id, "song", self.kodicursor) + self.kodi_db.remove_song(kodi_id) + + def removeAlbum(self, kodi_id): + + self.artwork.delete_artwork(kodi_id, "album", self.kodicursor) + self.kodi_db.remove_album(kodi_id) + + def removeArtist(self, kodi_id): + + self.artwork.delete_artwork(kodi_id, "artist", self.kodicursor) + self.kodi_db.remove_artist(kodi_id) diff --git a/resources/lib/objects/musicvideos.py b/resources/lib/objects/musicvideos.py new file mode 100644 index 00000000..12b69242 --- /dev/null +++ b/resources/lib/objects/musicvideos.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import urllib + +import api +import embydb_functions as embydb +import _kodi_musicvideos +from _common import Items, catch_except +from utils import window, settings, language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class MusicVideos(Items): + + + def __init__(self, embycursor, kodicursor, pdialog=None): + + self.embycursor = embycursor + self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.kodicursor = kodicursor + self.kodi_db = _kodi_musicvideos.KodiMusicVideos(self.kodicursor) + self.pdialog = pdialog + + self.new_time = int(settings('newvideotime'))*1000 + + Items.__init__(self) + + def _get_func(self, item_type, action): + + if item_type == "MusicVideo": + actions = { + 'added': self.added, + 'update': self.add_update, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + else: + log.info("Unsupported item_type: %s", item_type) + actions = {} + + return actions.get(action) + + def compare_all(self): + # Pull the list of musicvideos in Kodi + views = self.emby_db.getView_byType('musicvideos') + log.info("Media folders: %s", views) + + for view in views: + + if self.should_stop(): + return False + + if not self.compare_mvideos(view): + return False + + return True + + def compare_mvideos(self, view): + + view_id = view['id'] + view_name = view['name'] + + if self.pdialog: + self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33028), view_name)) + + mvideos = dict(self.emby_db.get_checksum_by_view('MusicVideo', view_id)) + emby_mvideos = self.emby.getMusicVideos(view_id, basic=True, dialog=self.pdialog) + + return self.compare("MusicVideo", emby_mvideos['Items'], mvideos, view) + + def added(self, items, total=None, view=None): + + for item in super(MusicVideos, self).added(items, total): + if self.add_update(item, view): + self.content_pop(item.get('Name', "unknown")) + + @catch_except() + def add_update(self, item, view=None): + # Process single music video + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + log.info("mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid) + + except TypeError: + update_item = False + log.debug("mvideoid: %s not found", itemid) + # mvideoid + mvideoid = self.kodi_db.create_entry() + + else: + if self.kodi_db.get_musicvideo(mvideoid) is None: + # item is not found, let's recreate it. + update_item = False + log.info("mvideoid: %s missing from Kodi, repairing the entry.", mvideoid) + + if not view: + # Get view tag from emby + viewtag, viewid, mediatype = self.emby.getView_embyId(itemid) + log.debug("View tag found: %s", viewtag) + else: + viewtag = view['name'] + viewid = view['id'] + + # fileId information + checksum = API.get_checksum() + dateadded = API.get_date_created() + userdata = API.get_userdata() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + runtime = API.get_runtime() + plot = API.get_overview() + title = item['Name'] + year = item.get('ProductionYear') + genres = item['Genres'] + genre = " / ".join(genres) + studios = API.get_studios() + studio = " / ".join(studios) + artist = " / ".join(item.get('Artists')) + album = item.get('Album') + track = item.get('Track') + people = API.get_people() + director = " / ".join(people['Director']) + + + ##### GET THE FILE AND PATH ##### + playurl = API.get_file_path() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.direct_path: + # Direct paths is set the Kodi way + if not self.path_validation(playurl): + return False + + path = playurl.replace(filename, "") + window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.musicvideos/" + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': mvideoid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE MUSIC VIDEO ##### + if update_item: + log.info("UPDATE mvideo itemid: %s - Title: %s", itemid, title) + + # Update the music video entry + self.kodi_db.update_musicvideo(title, runtime, director, studio, year, plot, album, + artist, genre, track, mvideoid) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE MUSIC VIDEO ##### + else: + log.info("ADD mvideo itemid: %s - Title: %s", itemid, title) + + # Add path + pathid = self.kodi_db.add_path(path) + # Add the file + fileid = self.kodi_db.add_file(filename, pathid) + + # Create the musicvideo entry + self.kodi_db.add_musicvideo(mvideoid, fileid, title, runtime, director, studio, + year, plot, album, artist, genre, track) + + # Create the reference in emby table + emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid, + checksum=checksum, mediafolderid=viewid) + + # Update the path + self.kodi_db.update_path(pathid, path, "musicvideos", "metadata.local") + # Update the file + self.kodi_db.update_file(fileid, filename, pathid, dateadded) + + # Process cast + people = item['People'] + artists = item['ArtistItems'] + for artist in artists: + artist['Type'] = "Artist" + people.extend(artists) + people = artwork.get_people_artwork(people) + self.kodi_db.add_people(mvideoid, people, "musicvideo") + # Process genres + self.kodi_db.add_genres(mvideoid, genres, "musicvideo") + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(item), mvideoid, "musicvideo", kodicursor) + # Process stream details + streams = API.get_media_streams() + self.kodi_db.add_streams(fileid, streams, runtime) + # Process studios + self.kodi_db.add_studios(mvideoid, studios, "musicvideo") + # Process tags: view, emby tags + tags = [viewtag] + tags.extend(item['Tags']) + if userdata['Favorite']: + tags.append("Favorite musicvideos") + self.kodi_db.add_tags(mvideoid, tags, "musicvideo") + # Process playstates + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + + return True + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.get_checksum() + userdata = API.get_userdata() + runtime = API.get_runtime() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + log.info("Update playstate for musicvideo: %s fileid: %s", item['Name'], fileid) + except TypeError: + return + + # Process favorite tags + if userdata['Favorite']: + self.kodi_db.get_tag(mvideoid, "Favorite musicvideos", "musicvideo") + else: + self.kodi_db.remove_tag(mvideoid, "Favorite musicvideos", "musicvideo") + + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove mvideoid, fileid, pathid, emby reference + emby_db = self.emby_db + kodicursor = self.kodicursor + artwork = self.artwork + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + mvideoid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + log.info("Removing mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid) + except TypeError: + return + + # Remove the emby reference + emby_db.removeItem(itemid) + # Remove artwork + artwork.delete_artwork(mvideoid, "musicvideo", self.kodicursor) + + self.kodi_db.remove_musicvideo(mvideoid, fileid) + if self.direct_path: + self.kodi_db.remove_path(pathid) + + log.info("Deleted musicvideo %s from kodi database", itemid) diff --git a/resources/lib/objects/tvshows.py b/resources/lib/objects/tvshows.py new file mode 100644 index 00000000..7153182f --- /dev/null +++ b/resources/lib/objects/tvshows.py @@ -0,0 +1,818 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import urllib +from ntpath import dirname + +import api +import embydb_functions as embydb +import _kodi_tvshows +from _common import Items, catch_except +from utils import window, settings, language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################## + + +class TVShows(Items): + + + def __init__(self, embycursor, kodicursor, pdialog=None): + + self.embycursor = embycursor + self.emby_db = embydb.Embydb_Functions(self.embycursor) + self.kodicursor = kodicursor + self.kodi_db = _kodi_tvshows.KodiTVShows(self.kodicursor) + self.pdialog = pdialog + + self.new_time = int(settings('newvideotime'))*1000 + + Items.__init__(self) + + def _get_func(self, item_type, action): + + if item_type == "Series": + actions = { + 'added': self.added, + 'update': self.add_update, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + elif item_type == "Season": + actions = { + 'added': self.added_season, + 'update': self.add_updateSeason, + 'remove': self.remove + } + elif item_type == "Episode": + actions = { + 'added': self.added_episode, + 'update': self.add_updateEpisode, + 'userdata': self.updateUserdata, + 'remove': self.remove + } + else: + log.info("Unsupported item_type: %s", item_type) + actions = {} + + return actions.get(action) + + def compare_all(self): + # Pull the list of movies and boxsets in Kodi + pdialog = self.pdialog + views = self.emby_db.getView_byType('tvshows') + views += self.emby_db.getView_byType('mixed') + log.info("Media folders: %s", views) + + # Pull the list of tvshows and episodes in Kodi + try: + all_koditvshows = dict(self.emby_db.get_checksum('Series')) + except ValueError: + all_koditvshows = {} + + log.info("all_koditvshows = %s", all_koditvshows) + + try: + all_kodiepisodes = dict(self.emby_db.get_checksum('Episode')) + except ValueError: + all_kodiepisodes = {} + + all_embytvshowsIds = set() + all_embyepisodesIds = set() + updatelist = [] + + # TODO: Review once series pooling is explicitely returned in api + for view in views: + + if self.should_stop(): + return False + + # Get items per view + viewId = view['id'] + viewName = view['name'] + + if pdialog: + pdialog.update( + heading=lang(29999), + message="%s %s..." % (lang(33029), viewName)) + + all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog) + for embytvshow in all_embytvshows['Items']: + + if self.should_stop(): + return False + + API = api.API(embytvshow) + itemid = embytvshow['Id'] + all_embytvshowsIds.add(itemid) + + + if all_koditvshows.get(itemid) != API.get_checksum(): + # Only update if movie is not in Kodi or checksum is different + updatelist.append(itemid) + + log.info("TVShows to update for %s: %s", viewName, updatelist) + embytvshows = self.emby.getFullItems(updatelist) + self.total = len(updatelist) + del updatelist[:] + + + if pdialog: + pdialog.update(heading="Processing %s / %s items" % (viewName, self.total)) + + self.count = 0 + for embytvshow in embytvshows: + # Process individual show + if self.should_stop(): + return False + + itemid = embytvshow['Id'] + title = embytvshow['Name'] + all_embytvshowsIds.add(itemid) + self.update_pdialog() + + self.add_update(embytvshow, view) + self.count += 1 + + else: + # Get all episodes in view + if pdialog: + pdialog.update( + heading=lang(29999), + message="%s %s..." % (lang(33030), viewName)) + + all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog) + for embyepisode in all_embyepisodes['Items']: + + if self.should_stop(): + return False + + API = api.API(embyepisode) + itemid = embyepisode['Id'] + all_embyepisodesIds.add(itemid) + if "SeriesId" in embyepisode: + all_embytvshowsIds.add(embyepisode['SeriesId']) + + if all_kodiepisodes.get(itemid) != API.get_checksum(): + # Only update if movie is not in Kodi or checksum is different + updatelist.append(itemid) + + log.info("Episodes to update for %s: %s", viewName, updatelist) + embyepisodes = self.emby.getFullItems(updatelist) + self.total = len(updatelist) + del updatelist[:] + + self.count = 0 + for episode in embyepisodes: + + # Process individual episode + if self.should_stop(): + return False + self.title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name']) + self.add_updateEpisode(episode) + self.count += 1 + + ##### PROCESS DELETES ##### + + log.info("all_embytvshowsIds = %s ", all_embytvshowsIds) + + for koditvshow in all_koditvshows: + if koditvshow not in all_embytvshowsIds: + self.remove(koditvshow) + + log.info("TVShows compare finished.") + + for kodiepisode in all_kodiepisodes: + if kodiepisode not in all_embyepisodesIds: + self.remove(kodiepisode) + + log.info("Episodes compare finished.") + + return True + + + def added(self, items, total=None, view=None): + + for item in super(TVShows, self).added(items, total): + if self.add_update(item, view): + # Add episodes + all_episodes = self.emby.getEpisodesbyShow(item['Id']) + self.added_episode(all_episodes['Items']) + + def added_season(self, items, total=None, view=None): + + update = True if not self.total else False + + for item in super(TVShows, self).added(items, total, update): + self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title) + + if self.add_updateSeason(item): + # Add episodes + all_episodes = self.emby.getEpisodesbySeason(item['Id']) + self.added_episode(all_episodes['Items']) + + def added_episode(self, items, total=None, view=None): + + update = True if not self.total else False + + for item in super(TVShows, self).added(items, total, update): + self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title) + + if self.add_updateEpisode(item): + self.content_pop(self.title) + + @catch_except() + def add_update(self, item, view=None): + # Process single tvshow + kodicursor = self.kodicursor + emby = self.emby + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): + log.info("Skipping empty show: %s", item['Name']) + return + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + force_episodes = False + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + showid = emby_dbitem[0] + pathid = emby_dbitem[2] + log.info("showid: %s pathid: %s", showid, pathid) + + except TypeError: + update_item = False + log.debug("showid: %s not found", itemid) + showid = self.kodi_db.create_entry() + + else: + # Verification the item is still in Kodi + if self.kodi_db.get_tvshow(showid) is None: + # item is not found, let's recreate it. + update_item = False + log.info("showid: %s missing from Kodi, repairing the entry", showid) + # Force re-add episodes after the show is re-created. + force_episodes = True + + + if view is None: + # Get view tag from emby + viewtag, viewid, mediatype = emby.getView_embyId(itemid) + log.debug("View tag found: %s", viewtag) + else: + viewtag = view['name'] + viewid = view['id'] + + # fileId information + checksum = API.get_checksum() + userdata = API.get_userdata() + + # item details + genres = item['Genres'] + title = item['Name'] + plot = API.get_overview() + rating = item.get('CommunityRating') + premieredate = API.get_premiere_date() + tvdb = API.get_provider('Tvdb') + sorttitle = item['SortName'] + mpaa = API.get_mpaa() + genre = " / ".join(genres) + studios = API.get_studios() + studio = " / ".join(studios) + + # Verify series pooling + if not update_item and tvdb: + query = "SELECT idShow FROM tvshow WHERE C12 = ?" + kodicursor.execute(query, (tvdb,)) + try: + temp_showid = kodicursor.fetchone()[0] + except TypeError: + pass + else: + emby_other = emby_db.getItem_byKodiId(temp_showid, "tvshow") + if emby_other and viewid == emby_other[2]: + log.info("Applying series pooling for %s", title) + emby_other_item = emby_db.getItem_byId(emby_other[0]) + showid = emby_other_item[0] + pathid = emby_other_item[2] + log.info("showid: %s pathid: %s", showid, pathid) + # Create the reference in emby table + emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, + checksum=checksum, mediafolderid=viewid) + update_item = True + + + ##### GET THE FILE AND PATH ##### + playurl = API.get_file_path() + + if self.direct_path: + # Direct paths is set the Kodi way + if "\\" in playurl: + # Local path + path = "%s\\" % playurl + toplevelpath = "%s\\" % dirname(dirname(path)) + else: + # Network path + path = "%s/" % playurl + toplevelpath = "%s/" % dirname(dirname(path)) + + if not self.path_validation(path): + return False + + window('emby_pathverified', value="true") + else: + # Set plugin path + toplevelpath = "plugin://plugin.video.emby.tvshows/" + path = "%s%s/" % (toplevelpath, itemid) + + + ##### UPDATE THE TVSHOW ##### + if update_item: + log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) + + # Update the tvshow entry + self.kodi_db.update_tvshow(title, plot, rating, premieredate, genre, title, + tvdb, mpaa, studio, sorttitle, showid) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + + ##### OR ADD THE TVSHOW ##### + else: + log.info("ADD tvshow itemid: %s - Title: %s", itemid, title) + + # Add top path + toppathid = self.kodi_db.add_path(toplevelpath) + self.kodi_db.update_path(toppathid, toplevelpath, "tvshows", "metadata.local") + + # Add path + pathid = self.kodi_db.add_path(path) + + # Create the tvshow entry + self.kodi_db.add_tvshow(showid, title, plot, rating, premieredate, genre, + title, tvdb, mpaa, studio, sorttitle) + + # Create the reference in emby table + emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, + checksum=checksum, mediafolderid=viewid) + + + # Link the path + self.kodi_db.link_tvshow(showid, pathid) + + # Update the path + self.kodi_db.update_path(pathid, path, None, None) + + # Process cast + people = artwork.get_people_artwork(item['People']) + self.kodi_db.add_people(showid, people, "tvshow") + # Process genres + self.kodi_db.add_genres(showid, genres, "tvshow") + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(item), showid, "tvshow", kodicursor) + # Process studios + self.kodi_db.add_studios(showid, studios, "tvshow") + # Process tags: view, emby tags + tags = [viewtag] + tags.extend(item['Tags']) + if userdata['Favorite']: + tags.append("Favorite tvshows") + self.kodi_db.add_tags(showid, tags, "tvshow") + # Process seasons + all_seasons = emby.getSeasons(itemid) + for season in all_seasons['Items']: + self.add_updateSeason(season, showid=showid) + else: + # Finally, refresh the all season entry + seasonid = self.kodi_db.get_season(showid, -1) + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor) + + if force_episodes: + # We needed to recreate the show entry. Re-add episodes now. + log.info("Repairing episodes for showid: %s %s", showid, title) + all_episodes = emby.getEpisodesbyShow(itemid) + self.added_episode(all_episodes['Items'], None) + + return True + + def add_updateSeason(self, item, showid=None): + + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + + seasonnum = item.get('IndexNumber', 1) + + if showid is None: + try: + seriesId = item['SeriesId'] + showid = emby_db.getItem_byId(seriesId)[0] + except KeyError: + return + except TypeError: + # Show is missing, update show instead. + show = self.emby.getItem(seriesId) + self.add_update(show) + return + + seasonid = self.kodi_db.get_season(showid, seasonnum, item['Name']) + + if item['LocationType'] != "Virtual": + # Create the reference in emby table + emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid) + + # Process artwork + artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor) + + return True + + @catch_except() + def add_updateEpisode(self, item): + # Process single episode + kodicursor = self.kodicursor + emby_db = self.emby_db + artwork = self.artwork + API = api.API(item) + + if item.get('LocationType') == "Virtual": # TODO: Filter via api instead + log.info("Skipping virtual episode: %s", item['Name']) + return + + # If the item already exist in the local Kodi DB we'll perform a full item update + # If the item doesn't exist, we'll add it to the database + update_item = True + itemid = item['Id'] + emby_dbitem = emby_db.getItem_byId(itemid) + try: + episodeid = emby_dbitem[0] + fileid = emby_dbitem[1] + pathid = emby_dbitem[2] + log.info("episodeid: %s fileid: %s pathid: %s", episodeid, fileid, pathid) + + except TypeError: + update_item = False + log.debug("episodeid: %s not found", itemid) + # episodeid + episodeid = self.kodi_db.create_entry_episode() + + else: + # Verification the item is still in Kodi + if self.kodi_db.get_episode(episodeid) is None: + # item is not found, let's recreate it. + update_item = False + log.info("episodeid: %s missing from Kodi, repairing the entry", episodeid) + + # fileId information + checksum = API.get_checksum() + dateadded = API.get_date_created() + userdata = API.get_userdata() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + + # item details + people = API.get_people() + writer = " / ".join(people['Writer']) + director = " / ".join(people['Director']) + title = item['Name'] + plot = API.get_overview() + rating = item.get('CommunityRating') + runtime = API.get_runtime() + premieredate = API.get_premiere_date() + + # episode details + try: + seriesId = item['SeriesId'] + except KeyError: + # Missing seriesId, skip + log.error("Skipping: %s. SeriesId is missing.", itemid) + return False + + season = item.get('ParentIndexNumber') + episode = item.get('IndexNumber', -1) + + if season is None: + if item.get('AbsoluteEpisodeNumber'): + # Anime scenario + season = 1 + episode = item['AbsoluteEpisodeNumber'] + else: + season = -1 if "Specials" not in item['Path'] else 0 + + # Specials ordering within season + if item.get('AirsAfterSeasonNumber'): + airsBeforeSeason = item['AirsAfterSeasonNumber'] + airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering + else: + airsBeforeSeason = item.get('AirsBeforeSeasonNumber') + airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber') + + # Append multi episodes to title + if item.get('IndexNumberEnd'): + title = "| %02d | %s" % (item['IndexNumberEnd'], title) + + # Get season id + show = emby_db.getItem_byId(seriesId) + try: + showid = show[0] + except TypeError: + # Show is missing from database + show = self.emby.getItem(seriesId) + self.add_update(show) + show = emby_db.getItem_byId(seriesId) + try: + showid = show[0] + except TypeError: + log.error("Skipping: %s. Unable to add series: %s", itemid, seriesId) + return False + + seasonid = self.kodi_db.get_season(showid, season) + + + ##### GET THE FILE AND PATH ##### + playurl = API.get_file_path() + + if "\\" in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: # Network share + filename = playurl.rsplit("/", 1)[1] + + if self.direct_path: + # Direct paths is set the Kodi way + if not self.path_validation(playurl): + return False + + path = playurl.replace(filename, "") + window('emby_pathverified', value="true") + else: + # Set plugin path and media flags using real filename + path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId + params = { + + 'filename': filename.encode('utf-8'), + 'id': itemid, + 'dbid': episodeid, + 'mode': "play" + } + filename = "%s?%s" % (path, urllib.urlencode(params)) + + + ##### UPDATE THE EPISODE ##### + if update_item: + log.info("UPDATE episode itemid: %s - Title: %s", itemid, title) + + # Update the movie entry + if self.kodi_version in (16, 17): + # Kodi Jarvis, Krypton + self.kodi_db.update_episode_16(title, plot, rating, writer, premieredate, runtime, + director, season, episode, title, airsBeforeSeason, + airsBeforeEpisode, seasonid, showid, episodeid) + else: + self.kodi_db.update_episode(title, plot, rating, writer, premieredate, runtime, + director, season, episode, title, airsBeforeSeason, + airsBeforeEpisode, showid, episodeid) + + # Update the checksum in emby table + emby_db.updateReference(itemid, checksum) + # Update parentid reference + emby_db.updateParentId(itemid, seasonid) + + ##### OR ADD THE EPISODE ##### + else: + log.info("ADD episode itemid: %s - Title: %s", itemid, title) + + # Add path + pathid = self.kodi_db.add_path(path) + # Add the file + fileid = self.kodi_db.add_file(filename, pathid) + + # Create the episode entry + if self.kodi_version in (16, 17): + # Kodi Jarvis, Krypton + self.kodi_db.add_episode_16(episodeid, fileid, title, plot, rating, writer, + premieredate, runtime, director, season, episode, title, + showid, airsBeforeSeason, airsBeforeEpisode, seasonid) + else: + self.kodi_db.add_episode(episodeid, fileid, title, plot, rating, writer, + premieredate, runtime, director, season, episode, title, + showid, airsBeforeSeason, airsBeforeEpisode) + + # Create the reference in emby table + emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, + seasonid, checksum) + + # Update the path + self.kodi_db.update_path(pathid, path, None, None) + # Update the file + self.kodi_db.update_file(fileid, filename, pathid, dateadded) + + # Process cast + people = artwork.get_people_artwork(item['People']) + self.kodi_db.add_people(episodeid, people, "episode") + # Process artwork + artworks = artwork.get_all_artwork(item) + artwork.add_update_art(artworks['Primary'], episodeid, "episode", "thumb", kodicursor) + # Process stream details + streams = API.get_media_streams() + self.kodi_db.add_streams(fileid, streams, runtime) + # Process playstates + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + if not self.direct_path and resume: + # Create additional entry for widgets. This is only required for plugin/episode. + temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/") + tempfileid = self.kodi_db.add_file(filename, temppathid) + self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded) + self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed) + + return True + + def updateUserdata(self, item): + # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks + # Poster with progress bar + emby_db = self.emby_db + API = api.API(item) + + # Get emby information + itemid = item['Id'] + checksum = API.get_checksum() + userdata = API.get_userdata() + runtime = API.get_runtime() + dateadded = API.get_date_created() + + # Get Kodi information + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + mediatype = emby_dbitem[4] + log.info("Update playstate for %s: %s fileid: %s", mediatype, item['Name'], fileid) + except TypeError: + return + + # Process favorite tags + if mediatype == "tvshow": + if userdata['Favorite']: + self.kodi_db.get_tag(kodiid, "Favorite tvshows", "tvshow") + else: + self.kodi_db.remove_tag(kodiid, "Favorite tvshows", "tvshow") + elif mediatype == "episode": + # Process playstates + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = API.adjust_resume(userdata['Resume']) + total = round(float(runtime), 6) + + log.debug("%s New resume point: %s", itemid, resume) + + self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed) + if not self.direct_path and not resume: + # Make sure there's no other bookmarks created by widget. + filename = self.kodi_db.get_filename(fileid) + self.kodi_db.remove_file("plugin://plugin.video.emby.tvshows/", filename) + + if not self.direct_path and resume: + # Create additional entry for widgets. This is only required for plugin/episode. + filename = self.kodi_db.get_filename(fileid) + temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/") + tempfileid = self.kodi_db.add_file(filename, temppathid) + self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded) + self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed) + + emby_db.updateReference(itemid, checksum) + + def remove(self, itemid): + # Remove showid, fileid, pathid, emby reference + emby_db = self.emby_db + kodicursor = self.kodicursor + + emby_dbitem = emby_db.getItem_byId(itemid) + try: + kodiid = emby_dbitem[0] + fileid = emby_dbitem[1] + parentid = emby_dbitem[3] + mediatype = emby_dbitem[4] + log.info("Removing %s kodiid: %s fileid: %s", mediatype, kodiid, fileid) + except TypeError: + return + + ##### PROCESS ITEM ##### + + # Remove the emby reference + emby_db.removeItem(itemid) + + ##### IF EPISODE ##### + + if mediatype == "episode": + # Delete kodi episode and file, verify season and tvshow + self.removeEpisode(kodiid, fileid) + + # Season verification + season = emby_db.getItem_byKodiId(parentid, "season") + try: + showid = season[1] + except TypeError: + return + + season_episodes = emby_db.getItem_byParentId(parentid, "episode") + if not season_episodes: + self.removeSeason(parentid) + emby_db.removeItem(season[0]) + + # Show verification + show = emby_db.getItem_byKodiId(showid, "tvshow") + query = ' '.join(( + + "SELECT totalCount", + "FROM tvshowcounts", + "WHERE idShow = ?" + )) + kodicursor.execute(query, (showid,)) + result = kodicursor.fetchone() + if result and result[0] is None: + # There's no episodes left, delete show and any possible remaining seasons + seasons = emby_db.getItem_byParentId(showid, "season") + for season in seasons: + self.removeSeason(season[1]) + else: + # Delete emby season entries + emby_db.removeItems_byParentId(showid, "season") + self.removeShow(showid) + emby_db.removeItem(show[0]) + + ##### IF TVSHOW ##### + + elif mediatype == "tvshow": + # Remove episodes, seasons, tvshow + seasons = emby_db.getItem_byParentId(kodiid, "season") + for season in seasons: + seasonid = season[1] + season_episodes = emby_db.getItem_byParentId(seasonid, "episode") + for episode in season_episodes: + self.removeEpisode(episode[1], episode[2]) + else: + # Remove emby episodes + emby_db.removeItems_byParentId(seasonid, "episode") + else: + # Remove emby seasons + emby_db.removeItems_byParentId(kodiid, "season") + + # Remove tvshow + self.removeShow(kodiid) + + ##### IF SEASON ##### + + elif mediatype == "season": + # Remove episodes, season, verify tvshow + season_episodes = emby_db.getItem_byParentId(kodiid, "episode") + for episode in season_episodes: + self.removeEpisode(episode[1], episode[2]) + else: + # Remove emby episodes + emby_db.removeItems_byParentId(kodiid, "episode") + + # Remove season + self.removeSeason(kodiid) + + # Show verification + seasons = emby_db.getItem_byParentId(parentid, "season") + if not seasons: + # There's no seasons, delete the show + self.removeShow(parentid) + emby_db.removeItem_byKodiId(parentid, "tvshow") + + log.info("Deleted %s: %s from kodi database", mediatype, itemid) + + def removeShow(self, kodiid): + + kodicursor = self.kodicursor + self.artwork.delete_artwork(kodiid, "tvshow", kodicursor) + self.kodi_db.remove_tvshow(kodiid) + log.debug("Removed tvshow: %s", kodiid) + + def removeSeason(self, kodiid): + + kodicursor = self.kodicursor + + self.artwork.delete_artwork(kodiid, "season", kodicursor) + self.kodi_db.remove_season(kodiid) + log.debug("Removed season: %s", kodiid) + + def removeEpisode(self, kodiid, fileid): + + kodicursor = self.kodicursor + + self.artwork.delete_artwork(kodiid, "episode", kodicursor) + self.kodi_db.remove_episode(kodiid, fileid) + log.debug("Removed episode: %s", kodiid) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index f089195a..24fa9b51 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -4,11 +4,15 @@ import json import logging +import requests +import os +import shutil import sys import xbmc import xbmcgui import xbmcplugin +import xbmcvfs import api import artwork @@ -60,6 +64,8 @@ class PlaybackUtils(): self.setProperties(playurl, listitem) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + # TODO: Review once Krypton is RC, no need for workaround. + ############### ORGANIZE CURRENT PLAYLIST ################ homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') @@ -78,8 +84,8 @@ class PlaybackUtils(): ############### RESUME POINT ################ - userdata = self.API.getUserData() - seektime = self.API.adjustResume(userdata['Resume']) + userdata = self.API.get_userdata() + seektime = self.API.adjust_resume(userdata['Resume']) # We need to ensure we add the intro and additional parts only once. # Otherwise we get a loop. @@ -94,9 +100,9 @@ class PlaybackUtils(): dummyPlaylist = True playlist.add(playurl, listitem, index=startPos) # Remove the original item from playlist - self.pl.removefromPlaylist(startPos+1) + self.pl.remove_from_playlist(startPos+1) # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insertintoPlaylist(currentPosition+1, dbid, self.item['Type'].lower()) + self.pl.insert_to_playlist(currentPosition+1, dbid, self.item['Type'].lower()) currentPosition += 1 ############### -- CHECK FOR INTROS ################ @@ -127,7 +133,7 @@ class PlaybackUtils(): pbutils = PlaybackUtils(intro) pbutils.setProperties(introPlayurl, introListItem) - self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) + self.pl.insert_to_playlist(currentPosition, url=introPlayurl) introsPlaylist = True currentPosition += 1 @@ -138,7 +144,7 @@ class PlaybackUtils(): # Extend our current playlist with the actual item to play # only if there's no playlist first log.info("Adding main item to playlist.") - self.pl.addtoPlaylist(dbid, self.item['Type'].lower()) + self.pl.add_to_playlist(dbid, self.item['Type'].lower()) # Ensure that additional parts are played after the main item currentPosition += 1 @@ -162,7 +168,7 @@ class PlaybackUtils(): pbutils.setArtwork(additionalListItem) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) - self.pl.verifyPlaylist() + self.pl.verify_playlist() currentPosition += 1 if dummyPlaylist: @@ -177,7 +183,7 @@ class PlaybackUtils(): log.debug("Resetting properties playback flag.") window('emby_playbackProps', clear=True) - #self.pl.verifyPlaylist() + #self.pl.verify_playlist() ########## SETUP MAIN ITEM ########## # For transcoding only, ask for audio/subs pref @@ -246,6 +252,9 @@ class PlaybackUtils(): except (TypeError, KeyError, IndexError): return + temp = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + kodiindex = 0 for stream in mediastreams: @@ -258,10 +267,21 @@ class PlaybackUtils(): # Direct stream url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" % (self.server, itemid, itemid, index)) + + if "Language" in stream: + + filename = "Stream.%s.srt" % stream['Language'] + try: + path = self._download_external_subs(url, temp, filename) + externalsubs.append(path) + except Exception as e: + log.error(e) + continue + else: + externalsubs.append(url) # map external subtitles for mapping mapping[kodiindex] = index - externalsubs.append(url) kodiindex += 1 mapping = json.dumps(mapping) @@ -269,9 +289,30 @@ class PlaybackUtils(): return externalsubs + def _download_external_subs(self, src, dst, filename): + + if not xbmcvfs.exists(dst): + xbmcvfs.mkdir(dst) + + path = os.path.join(dst, filename) + + try: + response = requests.get(src, stream=True) + response.encoding = 'utf-8' + response.raise_for_status() + except Exception as e: + del response + raise + else: + with open(path, 'wb') as f: + f.write(response.content) + del response + + return path + def setArtwork(self, listItem): # Set up item and item info - allartwork = self.artwork.getAllArtwork(self.item, parentInfo=True) + allartwork = self.artwork.get_all_artwork(self.item, parent_info=True) # Set artwork for listitem arttypes = { @@ -306,28 +347,32 @@ class PlaybackUtils(): else: listItem.setArt({arttype: path}) - def setListItem(self, listItem): + def setListItem(self, listItem, dbid=None): - people = self.API.getPeople() - studios = self.API.getStudios() + people = self.API.get_people() + studios = self.API.get_studios() metadata = { 'title': self.item.get('Name', "Missing name"), 'year': self.item.get('ProductionYear'), - 'plot': self.API.getOverview(), + 'plot': self.API.get_overview(), 'director': people.get('Director'), 'writer': people.get('Writer'), - 'mpaa': self.API.getMpaa(), + 'mpaa': self.API.get_mpaa(), 'genre': " / ".join(self.item['Genres']), 'studio': " / ".join(studios), - 'aired': self.API.getPremiereDate(), + 'aired': self.API.get_premiere_date(), 'rating': self.item.get('CommunityRating'), 'votes': self.item.get('VoteCount') } if "Episode" in self.item['Type']: # Only for tv shows + # For Kodi Krypton + metadata['mediatype'] = "episode" + metadata['dbid'] = dbid + thumbId = self.item.get('SeriesId') season = self.item.get('ParentIndexNumber', -1) episode = self.item.get('IndexNumber', -1) @@ -337,6 +382,11 @@ class PlaybackUtils(): metadata['season'] = season metadata['episode'] = episode + if "Movie" in self.item['Type']: + # For Kodi Krypton + metadata['mediatype'] = "movie" + metadata['dbid'] = dbid + listItem.setProperty('IsPlayable', 'true') listItem.setProperty('IsFolder', 'false') listItem.setLabel(metadata['title']) diff --git a/resources/lib/player.py b/resources/lib/player.py index 1fd19135..3f19979a 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -6,6 +6,7 @@ import json import logging import xbmc +import xbmcvfs import xbmcgui import clientinfo @@ -13,6 +14,7 @@ import downloadutils import kodidb_functions as kodidb import websocket_client as wsc from utils import window, settings, language as lang +from ga_client import GoogleAnalytics ################################################################################################# @@ -27,7 +29,6 @@ class Player(xbmc.Player): _shared_state = {} played_info = {} - playStats = {} currentFile = None @@ -37,14 +38,11 @@ class Player(xbmc.Player): self.clientInfo = clientinfo.ClientInfo() self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.ws = wsc.WebSocket_Client() + self.ws = wsc.WebSocketClient() self.xbmcplayer = xbmc.Player() log.debug("Starting playback monitor.") - - - def GetPlayStats(self): - return self.playStats + xbmc.Player.__init__(self) def onPlayBackStarted(self): # Will be called when xbmc starts playing a file @@ -231,20 +229,9 @@ class Player(xbmc.Player): self.played_info[currentFile] = data log.info("ADDING_FILE: %s" % self.played_info) - # log some playback stats - '''if(itemType != None): - if(self.playStats.get(itemType) != None): - count = self.playStats.get(itemType) + 1 - self.playStats[itemType] = count - else: - self.playStats[itemType] = 1 - - if(playMethod != None): - if(self.playStats.get(playMethod) != None): - count = self.playStats.get(playMethod) + 1 - self.playStats[playMethod] = count - else: - self.playStats[playMethod] = 1''' + ga = GoogleAnalytics() + ga.sendEventData("PlayAction", itemType, playMethod) + ga.sendScreenView(itemType) def reportPlayback(self): @@ -367,7 +354,7 @@ class Player(xbmc.Player): # Report progress via websocketclient postdata = json.dumps(postdata) log.debug("Report: %s" % postdata) - self.ws.sendProgressUpdate(postdata) + self.ws.send_progress_update(postdata) def onPlayBackPaused(self): @@ -441,6 +428,8 @@ class Player(xbmc.Player): # Prevent manually mark as watched in Kodi monitor window('emby_skipWatched%s' % itemid, value="true") + self.stopPlayback(data) + if currentPosition and runtime: try: percentComplete = (currentPosition * 10000000) / int(runtime) @@ -473,16 +462,24 @@ class Player(xbmc.Player): else: log.info("User skipped deletion.") - self.stopPlayback(data) - # Stop transcoding if playMethod == "Transcode": log.info("Transcoding for %s terminated." % itemid) - deviceId = self.clientInfo.getDeviceId() + deviceId = self.clientInfo.get_device_id() url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId self.doUtils(url, action_type="DELETE") + + path = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + + dirs, files = xbmcvfs.listdir(path) + for file in files: + xbmcvfs.delete("%s%s" % (path, file)) self.played_info.clear() + + ga = GoogleAnalytics() + ga.sendEventData("PlayAction", "Stopped") def stopPlayback(self, data): @@ -499,4 +496,11 @@ class Player(xbmc.Player): 'MediaSourceId': itemId, 'PositionTicks': positionTicks } - self.doUtils(url, postBody=postdata, action_type="POST") \ No newline at end of file + self.doUtils(url, postBody=postdata, action_type="POST") + + #If needed, close any livestreams + livestreamid = window("emby_%s.livestreamid" % self.currentFile) + if livestreamid: + url = "{server}/emby/LiveStreams/Close" + postdata = { 'LiveStreamId': livestreamid } + self.doUtils(url, postBody=postdata, action_type="POST") diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index 6f40fa71..0a534c06 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -2,18 +2,16 @@ ################################################################################################# -import json import logging import xbmc import xbmcgui -import xbmcplugin import playutils import playbackutils import embydb_functions as embydb import read_embyserver as embyserver -from utils import window, settings, language as lang, kodiSQL +from utils import window, kodiSQL, JSONRPC ################################################################################################# @@ -22,169 +20,141 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################# -class Playlist(): +class Playlist(object): def __init__(self): - - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) - self.emby = embyserver.Read_EmbyServer() - def playAll(self, itemids, startat): + def play_all(self, item_ids, start_at): - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) + conn = kodiSQL('emby') + cursor = conn.cursor() + emby_db = embydb.Embydb_Functions(cursor) player = xbmc.Player() playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist.clear() log.info("---*** PLAY ALL ***---") - log.info("Items: %s and start at: %s" % (itemids, startat)) + log.info("Items: %s and start at: %s", item_ids, start_at) started = False window('emby_customplaylist', value="true") - if startat != 0: + if start_at: # Seek to the starting position - window('emby_customplaylist.seektime', str(startat)) + window('emby_customplaylist.seektime', str(start_at)) - for itemid in itemids: - embydb_item = emby_db.getItem_byId(itemid) + for item_id in item_ids: + + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) try: - dbid = embydb_item[0] - mediatype = embydb_item[4] + db_id = item[0] + media_type = item[4] + except TypeError: # Item is not found in our database, add item manually - log.info("Item was not found in the database, manually adding item.") - item = self.emby.getItem(itemid) - self.addtoPlaylist_xbmc(playlist, item) - else: - # Add to playlist - self.addtoPlaylist(dbid, mediatype) + log.info("Item was not found in the database, manually adding item") + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item) - log.info("Adding %s to playlist." % itemid) + else: # Add to playlist + self.add_to_playlist(db_id, media_type) if not started: started = True player.play(playlist) - self.verifyPlaylist() - embycursor.close() + self.verify_playlist() + cursor.close() - def modifyPlaylist(self, itemids): + def modify_playlist(self, item_ids): - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) + conn = kodiSQL('emby') + cursor = conn.cursor() + emby_db = embydb.Embydb_Functions(cursor) log.info("---*** ADD TO PLAYLIST ***---") - log.info("Items: %s" % itemids) + log.info("Items: %s", item_ids) - player = xbmc.Player() playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - for itemid in itemids: - embydb_item = emby_db.getItem_byId(itemid) + for item_id in item_ids: + + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) try: - dbid = embydb_item[0] - mediatype = embydb_item[4] + db_id = item[0] + media_type = item[4] + except TypeError: # Item is not found in our database, add item manually - item = self.emby.getItem(itemid) - self.addtoPlaylist_xbmc(playlist, item) - else: - # Add to playlist - self.addtoPlaylist(dbid, mediatype) + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item) - log.info("Adding %s to playlist." % itemid) + else: # Add to playlist + self.add_to_playlist(db_id, media_type) - self.verifyPlaylist() - embycursor.close() + self.verify_playlist() + cursor.close() return playlist - - def addtoPlaylist(self, dbid=None, mediatype=None, url=None): - pl = { - - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Add", - 'params': { - - 'playlistid': 1 - } - } - if dbid is not None: - pl['params']['item'] = {'%sid' % mediatype: int(dbid)} - else: - pl['params']['item'] = {'file': url} - - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) - - def addtoPlaylist_xbmc(self, playlist, item): + @classmethod + def add_to_xbmc_playlist(cls, playlist, item): playurl = playutils.PlayUtils(item).getPlayUrl() if not playurl: - # Playurl failed - log.info("Failed to retrieve playurl.") + log.info("Failed to retrieve playurl") return - log.info("Playurl: %s" % playurl) + log.info("Playurl: %s", playurl) + listitem = xbmcgui.ListItem() playbackutils.PlaybackUtils(item).setProperties(playurl, listitem) - playlist.add(playurl, listitem) - def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None): + @classmethod + def add_to_playlist(cls, db_id=None, media_type=None, url=None): - pl = { + params = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Insert", - 'params': { - - 'playlistid': 1, - 'position': position - } + 'playlistid': 1 } - if dbid is not None: - pl['params']['item'] = {'%sid' % mediatype: int(dbid)} + if db_id is not None: + params['item'] = {'%sid' % media_type: int(db_id)} else: - pl['params']['item'] = {'file': url} + params['item'] = {'file': url} - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) + log.debug(JSONRPC('Playlist.Add').execute(params)) - def verifyPlaylist(self): + @classmethod + def insert_to_playlist(cls, position, db_id=None, media_type=None, url=None): - pl = { + params = { - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.GetItems", - 'params': { - - 'playlistid': 1 - } + 'playlistid': 1, + 'position': position } - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) + if db_id is not None: + params['item'] = {'%sid' % media_type: int(db_id)} + else: + params['item'] = {'file': url} - def removefromPlaylist(self, position): + log.debug(JSONRPC('Playlist.Insert').execute(params)) - pl = { + @classmethod + def verify_playlist(cls): + log.debug(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) - 'jsonrpc': "2.0", - 'id': 1, - 'method': "Playlist.Remove", - 'params': { + @classmethod + def remove_from_playlist(cls, position): - 'playlistid': 1, - 'position': position - } + params = { + + 'playlistid': 1, + 'position': position } - log.debug(xbmc.executeJSONRPC(json.dumps(pl))) \ No newline at end of file + log.debug(JSONRPC('Playlist.Remove').execute(params)) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index c683f599..e7b65af9 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -10,6 +10,7 @@ import xbmcgui import xbmcvfs import clientinfo +import downloadutils from utils import window, settings, language as lang ################################################################################################# @@ -29,7 +30,37 @@ class PlayUtils(): self.userid = window('emby_currUser') self.server = window('emby_server%s' % self.userid) + + self.doUtils = downloadutils.DownloadUtils().downloadUrl + def getPlayUrlNew(self): + ''' + New style to retrieve the best playback method based on sending the profile to the server + Based on capabilities the correct path is returned, including livestreams that need to be opened by the server + TODO: Close livestream if needed (RequiresClosing in livestream source) + ''' + playurl = None + pbinfo = self.getPlaybackInfo() + if pbinfo: + xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo)) + + if pbinfo["Protocol"] == "SupportsDirectPlay": + playmethod = "DirectPlay" + elif pbinfo["Protocol"] == "SupportsDirectStream": + playmethod = "DirectStream" + elif pbinfo.get('LiveStreamId'): + playmethod = "LiveStream" + else: + playmethod = "Transcode" + + playurl = pbinfo["Path"] + xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl)) + window('emby_%s.playmethod' % playurl, value=playmethod) + if pbinfo["RequiresClosing"] and pbinfo.get('LiveStreamId'): + window('emby_%s.livestreamid' % playurl, value=pbinfo["LiveStreamId"]) + + return playurl + def getPlayUrl(self): @@ -40,7 +71,8 @@ class PlayUtils(): # Play LiveTV or recordings log.info("File protocol is http (livetv).") playurl = "%s/emby/Videos/%s/stream.ts?audioCodec=copy&videoCodec=copy" % (self.server, self.item['Id']) - window('emby_%s.playmethod' % playurl, value="Transcode") + window('emby_%s.playmethod' % playurl, value="DirectPlay") + elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http": # Only play as http, used for channels, or online hosting of content @@ -60,6 +92,7 @@ class PlayUtils(): log.info("File is direct streaming.") playurl = self.directStream() + playurl = playurl.encode('utf-8') # Set playmethod property window('emby_%s.playmethod' % playurl, value="DirectStream") @@ -261,7 +294,7 @@ class PlayUtils(): playurl = self.directPlay() else: itemid = self.item['Id'] - deviceId = self.clientInfo.getDeviceId() + deviceId = self.clientInfo.get_device_id() playurl = ( "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" % (self.server, itemid, itemid) @@ -293,6 +326,9 @@ class PlayUtils(): '13': 16700, '14': 18200, '15': 20000, + '16': 25000, + '17': 30000, + '18': 35000, '16': 40000, '17': 100000, '18': 1000000 @@ -406,3 +442,190 @@ class PlayUtils(): playurlprefs += "&AudioBitrate=192000" return playurlprefs + + def getPlaybackInfo(self): + #Gets the playback Info for the current item + url = "{server}/emby/Items/%s/PlaybackInfo?format=json" %self.item['Id'] + body = { + "UserId": self.userid, + "DeviceProfile": self.getDeviceProfile(), + "StartTimeTicks": 0, #TODO + "AudioStreamIndex": None, #TODO + "SubtitleStreamIndex": None, #TODO + "MediaSourceId": None, + "LiveStreamId": None + } + pbinfo = self.doUtils(url, postBody=body, action_type="POST") + xbmc.log("getPlaybackInfo: %s" %pbinfo) + mediaSource = self.getOptimalMediaSource(pbinfo["MediaSources"]) + if mediaSource and mediaSource["RequiresOpening"]: + mediaSource = self.getLiveStream(pbinfo["PlaySessionId"], mediaSource) + + return mediaSource + + def getOptimalMediaSource(self, mediasources): + ''' + Select the best possible mediasource for playback + Because we posted our deviceprofile to the server, + only streams will be returned that can actually be played by this client so no need to check bitrates etc. + ''' + preferredStreamOrder = ["SupportsDirectPlay","SupportsDirectStream","SupportsTranscoding"] + bestSource = {} + for prefstream in preferredStreamOrder: + for source in mediasources: + if source[prefstream] == True: + if prefstream == "SupportsDirectPlay": + #always prefer direct play + alt_playurl = self.checkDirectPlayPath(source["Path"]) + if alt_playurl: + bestSource = source + source["Path"] = alt_playurl + elif bestSource.get("BitRate",0) < source.get("Bitrate",0): + #prefer stream with highest bitrate for http sources + bestSource = source + elif not source.get("Bitrate") and source.get("RequiresOpening"): + #livestream + bestSource = source + xbmc.log("getOptimalMediaSource: %s" %bestSource) + return bestSource + + def getLiveStream(self, playSessionId, mediaSource): + url = "{server}/emby/LiveStreams/Open?format=json" + body = { + "UserId": self.userid, + "DeviceProfile": self.getDeviceProfile(), + "ItemId": self.item["Id"], + "PlaySessionId": playSessionId, + "OpenToken": mediaSource["OpenToken"], + "StartTimeTicks": 0, #TODO + "AudioStreamIndex": None, #TODO + "SubtitleStreamIndex": None #TODO + } + streaminfo = self.doUtils(url, postBody=body, action_type="POST") + xbmc.log("getLiveStream: %s" %streaminfo) + return streaminfo["MediaSource"] + + def checkDirectPlayPath(self, playurl): + + if self.item.get('VideoType'): + # Specific format modification + if self.item['VideoType'] == "Dvd": + playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl + elif self.item['VideoType'] == "BluRay": + playurl = "%s/BDMV/index.bdmv" % playurl + + # Assign network protocol + if playurl.startswith('\\\\'): + playurl = playurl.replace("\\\\", "smb://") + playurl = playurl.replace("\\", "/") + + if xbmcvfs.exists(playurl): + return playurl + else: + return None + + def getDeviceProfile(self): + return { + "Name": "Kodi", + "MaxStreamingBitrate": self.getBitrate()*1000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + + "Identification": { + "ModelName": "Kodi", + "Headers": [ + { + "Name": "User-Agent", + "Value": "Kodi", + "Match": 2 + } + ] + }, + + "TranscodingProfiles": [ + { + "Container": "mp3", + "AudioCodec": "mp3", + "Type": 0 + }, + { + "Container": "ts", + "AudioCodec": "aac", + "VideoCodec": "h264", + "Type": 1 + }, + { + "Container": "jpeg", + "Type": 2 + } + ], + + "DirectPlayProfiles": [ + { + "Container": "", + "Type": 0 + }, + { + "Container": "", + "Type": 1 + }, + { + "Container": "", + "Type": 2 + } + ], + + "ResponseProfiles": [], + "ContainerProfiles": [], + "CodecProfiles": [], + + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": 2 + }, + { + "Format": "sub", + "Method": 2 + }, + { + "Format": "srt", + "Method": 1 + }, + { + "Format": "ass", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "ssa", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "smi", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "dvdsub", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "pgs", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "pgssub", + "Method": 1, + "DidlMode": "" + }, + { + "Format": "sub", + "Method": 1, + "DidlMode": "" + } + ] + } \ No newline at end of file diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index f2d241a9..57ef4337 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -3,6 +3,7 @@ ################################################################################################# import logging +import hashlib import xbmc @@ -123,7 +124,7 @@ class Read_EmbyServer(): return [viewName, viewId, mediatype] def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, - limit=None, sortorder="Ascending", filter=""): + limit=None, sortorder="Ascending", filter_type=""): params = { 'ParentId': parentid, @@ -135,7 +136,7 @@ class Read_EmbyServer(): 'Limit': limit, 'SortBy': sortby, 'SortOrder': sortorder, - 'Filters': filter, + 'Filters': filter_type, 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," @@ -185,7 +186,7 @@ class Read_EmbyServer(): url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" return self.doUtils(url, parameters=params) - def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None): + def getSection(self, parentid, itemtype=None, sortby="SortName", artist_id=None, basic=False, dialog=None): items = { @@ -198,6 +199,7 @@ class Read_EmbyServer(): params = { 'ParentId': parentid, + 'ArtistIds': artist_id, 'IncludeItemTypes': itemtype, 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, @@ -224,6 +226,7 @@ class Read_EmbyServer(): params = { 'ParentId': parentid, + 'ArtistIds': artist_id, 'IncludeItemTypes': itemtype, 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, @@ -246,9 +249,13 @@ class Read_EmbyServer(): "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "MediaSources,VoteCount" ) - result = self.doUtils(url, parameters=params) try: + result = self.doUtils(url, parameters=params) items['Items'].extend(result['Items']) + except Warning as error: + if "400" in error: + log.info("Something went wrong, aborting request.") + index += jump except TypeError: # Something happened to the connection if not throttled: @@ -322,25 +329,14 @@ class Read_EmbyServer(): else: for item in items: - item['Name'] = item['Name'] - if item['Type'] == "Channel": + if item['Type'] in ("Channel", "PlaylistsFolder"): # Filter view types continue # 3/4/2016 OriginalCollectionType is added itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) - - # 11/29/2015 Remove this once OriginalCollectionType is added to stable server. - # Assumed missing is mixed then. - '''if itemtype is None: - url = "{server}/emby/Library/MediaFolders?format=json" - result = self.doUtils(url) - - for folder in result['Items']: - if item['Id'] == folder['Id']: - itemtype = folder.get('CollectionType', "mixed")''' - if item['Name'] not in ('Collections', 'Trailers'): + if item['Name'] not in ('Collections', 'Trailers', 'Playlists'): if sortedlist: views.append({ @@ -441,7 +437,7 @@ class Read_EmbyServer(): return self.getSection(seasonId, "Episode") - def getArtists(self, dialog=None): + def getArtists(self, parent_id=None, dialog=None): items = { @@ -453,6 +449,7 @@ class Read_EmbyServer(): url = "{server}/emby/Artists?UserId={UserId}&format=json" params = { + 'ParentId': parent_id, 'Recursive': True, 'Limit': 1 } @@ -465,13 +462,14 @@ class Read_EmbyServer(): log.debug("%s:%s Failed to retrieve the server response." % (url, params)) else: - index = 1 + index = 0 jump = self.limitIndex while index < total: # Get items by chunk to increase retrieval speed at scale params = { + 'ParentId': parent_id, 'Recursive': True, 'IsVirtualUnaired': False, 'IsMissing': False, @@ -501,7 +499,7 @@ class Read_EmbyServer(): def getAlbumsbyArtist(self, artistId): - return self.getSection(artistId, "MusicAlbum", sortby="DateCreated") + return self.getSection(None, "MusicAlbum", sortby="DateCreated", artist_id=artistId) def getSongs(self, basic=False, dialog=None): @@ -571,4 +569,20 @@ class Read_EmbyServer(): def deleteItem(self, itemid): url = "{server}/emby/Items/%s?format=json" % itemid - self.doUtils(url, action_type="DELETE") \ No newline at end of file + self.doUtils(url, action_type="DELETE") + + def getUsers(self, server): + + url = "%s/emby/Users/Public?format=json" % server + users = self.doUtils(url, authenticate=False) + + return users or [] + + def loginUser(self, server, username, password=None): + + password = password or "" + url = "%s/emby/Users/AuthenticateByName?format=json" % server + data = {'username': username, 'password': hashlib.sha1(password).hexdigest()} + user = self.doUtils(url, postBody=data, action_type="POST", authenticate=False) + + return user \ No newline at end of file diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py new file mode 100644 index 00000000..abbedfa9 --- /dev/null +++ b/resources/lib/service_entry.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import sys +import time +import _strptime # Workaround for threads using datetime: _striptime is locked +from datetime import datetime + +import xbmc + +import userclient +import clientinfo +import initialsetup +import kodimonitor +import librarysync +import player +import videonodes +import websocket_client as wsc +from utils import window, settings, dialog, language as lang +from ga_client import GoogleAnalytics +import md5 + +################################################################################################# + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Service(object): + + startup = False + server_online = True + warn_auth = True + + userclient_running = False + userclient_thread = None + websocket_running = False + websocket_thread = None + library_running = False + library_thread = None + + last_progress = datetime.today() + lastMetricPing = time.time() + + def __init__(self): + + self.client_info = clientinfo.ClientInfo() + self.addon_name = self.client_info.get_addon_name() + log_level = settings('logLevel') + + window('emby_logLevel', value=str(log_level)) + window('emby_kodiProfile', value=xbmc.translatePath('special://profile')) + context_menu = "true" if settings('enableContext') == "true" else "" + window('emby_context', value=context_menu) + + # Initial logging + log.warn("======== START %s ========", self.addon_name) + log.warn("Python Version: %s", sys.version) + log.warn("Platform: %s", self.client_info.get_platform()) + log.warn("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) + log.warn("%s Version: %s", self.addon_name, self.client_info.get_version()) + log.warn("Using plugin paths: %s", settings('useDirectPaths') == "0") + log.warn("Log Level: %s", log_level) + + # Reset window props for profile switch + properties = [ + + "emby_online", "emby_state.json", "emby_serverStatus", "emby_onWake", + "emby_syncRunning", "emby_dbCheck", "emby_kodiScan", + "emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId", + "emby_initialScan", "emby_customplaylist", "emby_playbackProps" + ] + for prop in properties: + window(prop, clear=True) + + # Clear video nodes properties + videonodes.VideoNodes().clearProperties() + + # Set the minimum database version + window('emby_minDBVersion', value="1.1.63") + + + def service_entry_point(self): + # Important: Threads depending on abortRequest will not trigger + # if profile switch happens more than once. + self.monitor = kodimonitor.KodiMonitor() + self.kodi_player = player.Player() + kodi_profile = xbmc.translatePath('special://profile') + + # Server auto-detect + initialsetup.InitialSetup().setup() + + # Initialize important threads + self.userclient_thread = userclient.UserClient() + user_client = self.userclient_thread + self.websocket_thread = wsc.WebSocketClient() + self.library_thread = librarysync.LibrarySync() + + + while not self.monitor.abortRequested(): + + if window('emby_kodiProfile') != kodi_profile: + # Profile change happened, terminate this thread and others + log.info("Kodi profile was: %s and changed to: %s. Terminating old Emby thread.", + kodi_profile, window('emby_kodiProfile')) + raise RuntimeError("Kodi profile changed detected") + + # Before proceeding, need to make sure: + # 1. Server is online + # 2. User is set + # 3. User has access to the server + + if window('emby_online') == "true": + + # Emby server is online + # Verify if user is set and has access to the server + if user_client.get_user() is not None and user_client.get_access(): + + # If an item is playing + if self.kodi_player.isPlaying(): + # ping metrics server to keep sessions alive while playing + # ping every 5 min + timeSinceLastPing = time.time() - self.lastMetricPing + if(timeSinceLastPing > 300): + self.lastMetricPing = time.time() + ga = GoogleAnalytics() + ga.sendEventData("PlayAction", "PlayPing") + + self._report_progress() + + elif not self.startup: + self.startup = self._startup() + else: + + if (user_client.get_user() is None) and self.warn_auth: + # Alert user is not authenticated and suppress future warning + self.warn_auth = False + log.info("Not authenticated yet.") + + # User access is restricted. + # Keep verifying until access is granted + # unless server goes offline or Kodi is shut down. + self._access_check() + else: + # Wait until Emby server is online + # or Kodi is shut down. + self._server_online_check() + + if self.monitor.waitForAbort(1): + # Abort was requested while waiting. We should exit + break + + ##### Emby thread is terminating. ##### + self.shutdown() + + def _startup(self): + + serverId = settings('serverId') + if(serverId != None): + serverId = md5.new(serverId).hexdigest() + + ga = GoogleAnalytics() + ga.sendEventData("Application", "Startup", serverId) + + # Start up events + self.warn_auth = True + + username = self.userclient_thread.get_username() + if settings('connectMsg') == "true" and username: + # Get additional users + add_users = settings('additionalUsers') + if add_users: + add_users = ", "+", ".join(add_users.split(',')) + + dialog(type_="notification", + heading="{emby}", + message=("%s %s%s" + % (lang(33000), username.decode('utf-8'), + add_users.decode('utf-8'))), + icon="{emby}", + time=2000, + sound=False) + + # Start the Websocket Client + self.websocket_running = True + self.websocket_thread.start() + # Start the syncing thread + self.library_running = True + self.library_thread.start() + + return True + + def _server_online_check(self): + # Set emby_online true/false property + user_client = self.userclient_thread + while not self.monitor.abortRequested(): + + if user_client.get_server() is None: + # No server info set in add-on settings + pass + + elif not user_client.verify_server(): + # Server is offline. + # Alert the user and suppress future warning + if self.server_online: + log.info("Server is offline") + window('emby_online', value="false") + + if settings('offlineMsg') == "true": + dialog(type_="notification", + heading=lang(33001), + message="%s %s" % (self.addon_name, lang(33002)), + icon="{emby}", + sound=False) + + self.server_online = False + + elif window('emby_online') in ("sleep", "reset"): + # device going to sleep + if self.websocket_running: + self.websocket_thread.stop_client() + self.websocket_thread = wsc.WebSocketClient() + self.websocket_running = False + + if self.library_running: + self.library_thread.stopThread() + self.library_thread = librarysync.LibrarySync() + self.library_running = False + + else: + # Server is online + if not self.server_online: + # Server was offline when Kodi started. + # Wait for server to be fully established. + if self.monitor.waitForAbort(5): + # Abort was requested while waiting. + break + # Alert the user that server is online. + dialog(type_="notification", + heading="{emby}", + message=lang(33003), + icon="{emby}", + time=2000, + sound=False) + + self.server_online = True + window('emby_online', value="true") + log.info("Server is online and ready") + + # Start the userclient thread + if not self.userclient_running: + self.userclient_running = True + user_client.start() + + break + + if self.monitor.waitForAbort(1): + # Abort was requested while waiting. + break + + def _access_check(self): + # Keep verifying until access is granted + # unless server goes offline or Kodi is shut down. + while not self.userclient_thread.get_access(): + + if window('emby_online') != "true": + # Server went offline + break + + if self.monitor.waitForAbort(5): + # Abort was requested while waiting. We should exit + break + + def _report_progress(self): + # Update and report playback progress + kodi_player = self.kodi_player + try: + play_time = kodi_player.getTime() + filename = kodi_player.currentFile + + # Update positionticks + if filename in kodi_player.played_info: + kodi_player.played_info[filename]['currentPosition'] = play_time + + difference = datetime.today() - self.last_progress + difference_seconds = difference.seconds + + # Report progress to Emby server + if difference_seconds > 3: + kodi_player.reportPlayback() + self.last_progress = datetime.today() + + elif window('emby_command') == "true": + # Received a remote control command that + # requires updating immediately + window('emby_command', clear=True) + kodi_player.reportPlayback() + self.last_progress = datetime.today() + + except Exception as error: + log.exception(error) + + def shutdown(self): + + #ga = GoogleAnalytics() + #ga.sendEventData("Application", "Shutdown") + + if self.userclient_running: + self.userclient_thread.stop_client() + + if self.library_running: + self.library_thread.stopThread() + + if self.websocket_running: + self.websocket_thread.stop_client() + + log.warn("======== STOP %s ========", self.addon_name) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 37282efb..46f8b319 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -2,18 +2,17 @@ ################################################################################################## -import hashlib +import json import logging import threading import xbmc import xbmcgui -import xbmcaddon -import xbmcvfs import artwork -import clientinfo +import connectmanager import downloadutils +import read_embyserver as embyserver from utils import window, settings, language as lang ################################################################################################## @@ -22,426 +21,303 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## - class UserClient(threading.Thread): - # Borg - multiple instances, shared state - _shared_state = {} + _shared_state = {} # Borg - stop_thread = False - auth = True - retry = 0 + _stop_thread = False + _user = None + _server = None - currUser = None - currUserId = None - currServer = None - currToken = None - HasAccess = True - AdditionalUser = [] - - userSettings = None + _auth = True + _has_access = True def __init__(self): self.__dict__ = self._shared_state - self.addon = xbmcaddon.Addon() - self.doUtils = downloadutils.DownloadUtils() + self.doutils = downloadutils.DownloadUtils() + self.download = self.doutils.downloadUrl + self.emby = embyserver.Read_EmbyServer() + self.connectmanager = connectmanager.ConnectManager() threading.Thread.__init__(self) + @classmethod + def get_username(cls): + return settings('username') or settings('connectUsername') or None - def getAdditionalUsers(self): + def get_user(self, data=None): - additionalUsers = settings('additionalUsers') + if data is not None: + self._user = data + self._set_user_server() - if additionalUsers: - self.AdditionalUser = additionalUsers.split(',') + return self._user - def getUsername(self): + def get_server_details(self): + return self._server - username = settings('username') + @classmethod + def get_server(cls): - if not username: - log.debug("No username saved.") - return "" - - return username - - def getLogLevel(self): - - try: - logLevel = int(settings('logLevel')) - except ValueError: - logLevel = 0 - - return logLevel - - def getUserId(self): - - username = self.getUsername() - w_userId = window('emby_currUser') - s_userId = settings('userId%s' % username) - - # Verify the window property - if w_userId: - if not s_userId: - # Save access token if it's missing from settings - settings('userId%s' % username, value=w_userId) - log.debug("Returning userId from WINDOW for username: %s UserId: %s" - % (username, w_userId)) - return w_userId - # Verify the settings - elif s_userId: - log.debug("Returning userId from SETTINGS for username: %s userId: %s" - % (username, s_userId)) - return s_userId - # No userId found - else: - log.info("No userId saved for username: %s." % username) - - def getServer(self, prefix=True): - - alternate = settings('altip') == "true" - if alternate: - # Alternate host - HTTPS = settings('secondhttps') == "true" - host = settings('secondipaddress') - port = settings('secondport') - else: - # Original host - HTTPS = settings('https') == "true" + ###$ Begin migration $### + if settings('server') == "": + http = "https" if settings('https') == "true" else "http" host = settings('ipaddress') port = settings('port') - server = host + ":" + port + if host and port: + settings('server', value="%s://%s:%s" % (http, host, port)) + log.info("server address migration completed") + ###$ End migration $### - if not host: - log.debug("No server information saved.") - return False + return settings('server') or None - # If https is true - if prefix and HTTPS: - server = "https://%s" % server - return server - # If https is false - elif prefix and not HTTPS: - server = "http://%s" % server - return server - # If only the host:port is required - elif not prefix: - return server + def verify_server(self): - def getToken(self): - - username = self.getUsername() - userId = self.getUserId() - w_token = window('emby_accessToken%s' % userId) - s_token = settings('accessToken') - - # Verify the window property - if w_token: - if not s_token: - # Save access token if it's missing from settings - settings('accessToken', value=w_token) - log.debug("Returning accessToken from WINDOW for username: %s accessToken: %s" - % (username, w_token)) - return w_token - # Verify the settings - elif s_token: - log.debug("Returning accessToken from SETTINGS for username: %s accessToken: %s" - % (username, s_token)) - window('emby_accessToken%s' % username, value=s_token) - return s_token - else: - log.info("No token found.") - return "" - - def getSSLverify(self): - # Verify host certificate - s_sslverify = settings('sslverify') - if settings('altip') == "true": - s_sslverify = settings('secondsslverify') - - if s_sslverify == "true": + url = "%s/emby/Users/Public?format=json" % self.get_server() + result = self.download(url, authenticate=False) + if result != "": # Specific verification, due to possibility of returning empty dict return True - else: - return False - - def getSSL(self): - # Client side certificate - s_cert = settings('sslcert') - if settings('altip') == "true": - s_cert = settings('secondsslcert') - - if s_cert == "None": - return None - else: - return s_cert - - def setUserPref(self): - - doUtils = self.doUtils.downloadUrl - - result = doUtils("{server}/emby/Users/{UserId}?format=json") - self.userSettings = result - # Set user image for skin display - if result.get('PrimaryImageTag'): - window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary')) - - # Set resume point max - result = doUtils("{server}/emby/System/Configuration?format=json") - settings('markPlayed', value=str(result['MaxResumePct'])) - - def getPublicUsers(self): - # Get public Users - url = "%s/emby/Users/Public?format=json" % self.getServer() - result = self.doUtils.downloadUrl(url, authenticate=False) - if result != "": - return result else: # Server connection failed return False + @classmethod + def get_ssl(cls): + """ + Returns boolean value or path to certificate + True: Verify ssl + False: Don't verify connection + """ + certificate = settings('sslcert') + if certificate != "None": + return certificate - def hasAccess(self): - # hasAccess is verified in service.py - result = self.doUtils.downloadUrl("{server}/emby/Users?format=json") + return True if settings('sslverify') == "true" else False - if result == False: - # Access is restricted, set in downloadutils.py via exception - log.info("Access is restricted.") - self.HasAccess = False + def get_access(self): - elif window('emby_online') != "true": - # Server connection failed - pass + if not self._has_access: + self._set_access() - elif window('emby_serverStatus') == "restricted": - log.info("Access is granted.") - self.HasAccess = True - window('emby_serverStatus', clear=True) - xbmcgui.Dialog().notification(lang(29999), lang(33007)) + return self._has_access - def loadCurrUser(self, authenticated=False): + def _set_access(self): - doUtils = self.doUtils - username = self.getUsername() - userId = self.getUserId() + try: + self.download("{server}/emby/Users?format=json") + except Warning as error: + if self._has_access and "restricted" in error: + self._has_access = False + log.info("access is restricted") + else: + if not self._has_access: + self._has_access = True + window('emby_serverStatus', clear=True) + log.info("access is granted") + xbmcgui.Dialog().notification(lang(29999), lang(33007)) - # Only to be used if token exists - self.currUserId = userId - self.currServer = self.getServer() - self.currToken = self.getToken() - self.ssl = self.getSSLverify() - self.sslcert = self.getSSL() + @classmethod + def get_userid(cls): - # Test the validity of current token - if authenticated == False: - url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) - window('emby_currUser', value=userId) - window('emby_accessToken%s' % userId, value=self.currToken) - result = doUtils.downloadUrl(url) + ###$ Begin migration $### + if settings('userId') == "": + settings('userId', value=settings('userId%s' % settings('username'))) + log.info("userid migration completed") + ###$ End migration $### - if result == 401: - # Token is no longer valid - self.resetClient() - return False + return settings('userId') or None - # Set to windows property - window('emby_currUser', value=userId) - window('emby_accessToken%s' % userId, value=self.currToken) - window('emby_server%s' % userId, value=self.currServer) - window('emby_server_%s' % userId, value=self.getServer(prefix=False)) + @classmethod + def get_token(cls): - # Set DownloadUtils values - doUtils.setUsername(username) - doUtils.setUserId(self.currUserId) - doUtils.setServer(self.currServer) - doUtils.setToken(self.currToken) - doUtils.setSSL(self.ssl, self.sslcert) - # parental control - let's verify if access is restricted - self.hasAccess() - # Start DownloadUtils session - doUtils.startSession() - self.getAdditionalUsers() - # Set user preferences in settings - self.currUser = username - self.setUserPref() + ###$ Begin migration $### + if settings('token') == "": + settings('token', value=settings('accessToken')) + log.info("token migration completed") + ###$ End migration $### + return settings('token') or None - def authenticate(self): + def _set_user_server(self): - dialog = xbmcgui.Dialog() + user = self.download("{server}/emby/Users/{UserId}?format=json") + settings('username', value=user['Name']) + self._user = user - # Get /profile/addon_data - addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') - hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) + if "PrimaryImageTag" in self._user: + window('EmbyUserImage', + value=artwork.Artwork().get_user_artwork(self._user['Id'], 'Primary')) - username = self.getUsername() - server = self.getServer() + self._server = self.download("{server}/emby/System/Configuration?format=json") + settings('markPlayed', value=str(self._server['MaxResumePct'])) - # If there's no settings.xml - if not hasSettings: - log.info("No settings.xml found.") - self.auth = False - return - # If no user information - elif not server or not username: - log.info("Missing server information.") - self.auth = False - return - # If there's a token, load the user - elif self.getToken(): - result = self.loadCurrUser() + def _authenticate(self): - if result == False: - pass + if not self.get_server() or not self.get_username(): + log.info('missing server or user information') + self._auth = False + + elif self.get_token(): + try: + self._load_user() + except Warning: + log.info("token is invalid") + self._reset_client() else: - log.info("Current user: %s" % self.currUser) - log.info("Current userId: %s" % self.currUserId) - log.debug("Current accessToken: %s" % self.currToken) + log.info("current user: %s", self.get_username()) + log.info("current userid: %s", self.get_userid()) + log.debug("current token: %s", self.get_token()) return ##### AUTHENTICATE USER ##### - - users = self.getPublicUsers() - password = "" - - # Find user in list - for user in users: - name = user['Name'] - - if username.decode('utf-8') in name: - # If user has password - if user['HasPassword'] == True: - password = dialog.input( - heading="%s %s" % (lang(33008), username.decode('utf-8')), - option=xbmcgui.ALPHANUM_HIDE_INPUT) - # If password dialog is cancelled - if not password: - log.warn("No password entered.") - window('emby_serverStatus', value="Stop") - self.auth = False - return - break - else: - # Manual login, user is hidden - password = dialog.input( - heading="%s %s" % (lang(33008), username.decode('utf-8')), - option=xbmcgui.ALPHANUM_HIDE_INPUT) - sha1 = hashlib.sha1(password) - sha1 = sha1.hexdigest() - - # Authenticate username and password - data = {'username': username, 'password': sha1} - log.debug(data) - - url = "%s/emby/Users/AuthenticateByName?format=json" % server - result = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False) + server = self.get_server() + username = self.get_username().decode('utf-8') try: - log.info("Auth response: %s" % result) - accessToken = result['AccessToken'] - - except (KeyError, TypeError): - log.info("Failed to retrieve the api key.") - accessToken = None - - if accessToken is not None: - self.currUser = username - dialog.notification(lang(29999), - "%s %s!" % (lang(33000), self.currUser.decode('utf-8'))) - settings('accessToken', value=accessToken) - settings('userId%s' % username, value=result['User']['Id']) - log.info("User Authenticated: %s" % accessToken) - self.loadCurrUser(authenticated=True) - window('emby_serverStatus', clear=True) - self.retry = 0 + user = self.connectmanager.login_manual(server, username) + except RuntimeError: + window('emby_serverStatus', value="stop") + self._auth = False + return else: - log.error("User authentication failed.") - settings('accessToken', value="") - settings('userId%s' % username, value="") - dialog.ok(lang(33001), lang(33009)) + log.info("user: %s", user) + settings('username', value=user['User']['Name']) + settings('token', value=user['AccessToken']) + settings('userId', value=user['User']['Id']) + xbmcgui.Dialog().notification(lang(29999), + "%s %s!" % (lang(33000), username)) + self._load_user(authenticated=True) + window('emby_serverStatus', clear=True) - # Give two attempts at entering password - if self.retry == 2: - log.info("Too many retries. " - "You can retry by resetting attempts in the addon settings.") - window('emby_serverStatus', value="Stop") - dialog.ok(lang(33001), lang(33010)) + def _load_user(self, authenticated=False): - self.retry += 1 - self.auth = False + doutils = self.doutils - def resetClient(self): + userid = self.get_userid() + server = self.get_server() + token = self.get_token() - log.info("Reset UserClient authentication.") - if self.currToken is not None: - # In case of 401, removed saved token - settings('accessToken', value="") - window('emby_accessToken%s' % self.getUserId(), clear=True) - self.currToken = None - log.info("User token has been removed.") + # Set properties + # TODO: Remove old reference once code converted + window('emby_currUser', value=userid) + window('emby_server%s' % userid, value=server) + window('emby_accessToken%s' % userid, value=token) - self.auth = True - self.currUser = None + server_json = { + 'UserId': userid, + 'Server': server, + 'ServerId': settings('serverId'), + 'Token': token, + 'SSL': self.get_ssl() + } + # Set downloadutils.py values + doutils.set_session(**server_json) + + # Test the validity of the current token + if not authenticated: + try: + self.download("{server}/emby/Users/{UserId}?format=json") + except Warning as error: + if "401" in error: + # Token is not longer valid + raise + + # verify user access + self._set_access() + # Start downloadutils.py session + doutils.start_session() + # Set _user and _server + self._set_user_server() + + def load_connect_servers(self): + # Set connect servers + if not settings('connectUsername'): + return + + servers = self.connectmanager.get_connect_servers() + added_servers = [] + for server in servers: + if server['Id'] != settings('serverId'): + # TODO: SSL setup + self.doutils.add_server(server, False) + added_servers.append(server['Id']) + + # Set properties + log.info(added_servers) + window('emby_servers.json', value=added_servers) + + def _reset_client(self): + + log.info("reset UserClient authentication") + + settings('accessToken', value="") + window('emby_accessToken', clear=True) + + log.info("user token revoked.") + + self._user = None + self.auth = None + + current_state = self.connectmanager.get_state() + for server in current_state['Servers']: + + if server['Id'] == settings('serverId'): + # Update token + server['AccessToken'] = None + self.connectmanager.update_token(server) def run(self): monitor = xbmc.Monitor() - log.warn("----===## Starting UserClient ##===----") - while not monitor.abortRequested(): + log.warn("----====# Starting UserClient #====----") + + while not self._stop_thread: status = window('emby_serverStatus') if status: # Verify the connection status to server if status == "restricted": # Parental control is restricting access - self.HasAccess = False + self._has_access = False elif status == "401": # Unauthorized access, revoke token - window('emby_serverStatus', value="Auth") - self.resetClient() + window('emby_serverStatus', value="auth") + self._reset_client() - if self.auth and (self.currUser is None): + if self._auth and self._user is None: # Try to authenticate user status = window('emby_serverStatus') - if not status or status == "Auth": + if not status or status == "auth": # Set auth flag because we no longer need # to authenticate the user - self.auth = False - self.authenticate() + self._auth = False + self._authenticate() - - if not self.auth and (self.currUser is None): + if not self._auth and self._user is None: # If authenticate failed. - server = self.getServer() - username = self.getUsername() + server = self.get_server() + username = self.get_username() status = window('emby_serverStatus') # The status Stop is for when user cancelled password dialog. - if server and username and status != "Stop": + if server and username and status != "stop": # Only if there's information found to login - log.debug("Server found: %s" % server) - log.debug("Username found: %s" % username) - self.auth = True - - - if self.stop_thread == True: - # If stopping the client didn't work - break + log.info("Server found: %s", server) + log.info("Username found: %s", username) + self._auth = True if monitor.waitForAbort(1): # Abort was requested while waiting. We should exit break - self.doUtils.stopSession() - log.warn("##===---- UserClient Stopped ----===##") + self.doutils.stop_session() + log.warn("#====---- UserClient Stopped ----====#") - def stopClient(self): - # When emby for kodi terminates - self.stop_thread = True \ No newline at end of file + def stop_client(self): + self._stop_thread = True diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 12aba200..d1ac9859 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -2,14 +2,14 @@ ################################################################################################# -import cProfile + import inspect import json import logging -import pstats import sqlite3 import StringIO import os +import sys import time import unicodedata import xml.etree.ElementTree as etree @@ -18,6 +18,7 @@ from datetime import datetime import xbmc import xbmcaddon import xbmcgui +import xbmcplugin import xbmcvfs ################################################################################################# @@ -27,16 +28,21 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################# # Main methods -def window(property, value=None, clear=False, window_id=10000): +def window(property_, value=None, clear=False, window_id=10000): # Get or set window property WINDOW = xbmcgui.Window(window_id) if clear: - WINDOW.clearProperty(property) + WINDOW.clearProperty(property_) elif value is not None: - WINDOW.setProperty(property, value) + if ".json" in property_: + value = json.dumps(value) + WINDOW.setProperty(property_, value) else: - return WINDOW.getProperty(property) + result = WINDOW.getProperty(property_) + if result and ".json" in property_: + result = json.loads(result) + return result def settings(setting, value=None): # Get or add addon setting @@ -51,9 +57,69 @@ def language(string_id): # Central string retrieval - unicode return xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(string_id) +def dialog(type_, *args, **kwargs): + + d = xbmcgui.Dialog() + + if "icon" in kwargs: + kwargs['icon'] = kwargs['icon'].replace("{emby}", + "special://home/addons/plugin.video.emby/icon.png") + if "heading" in kwargs: + kwargs['heading'] = kwargs['heading'].replace("{emby}", language(29999)) + + types = { + 'yesno': d.yesno, + 'ok': d.ok, + 'notification': d.notification, + 'input': d.input, + 'select': d.select, + 'numeric': d.numeric + } + return types[type_](*args, **kwargs) + + +class JSONRPC(object): + + id_ = 1 + jsonrpc = "2.0" + + def __init__(self, method, **kwargs): + + self.method = method + + for arg in kwargs: # id_(int), jsonrpc(str) + self.arg = arg + + def _query(self): + + query = { + + 'jsonrpc': self.jsonrpc, + 'id': self.id_, + 'method': self.method, + } + if self.params is not None: + query['params'] = self.params + + return json.dumps(query) + + def execute(self, params=None): + + self.params = params + return json.loads(xbmc.executeJSONRPC(self._query())) + ################################################################################################# # Database related methods +def should_stop(): + # Checkpoint during the syncing process + if xbmc.Monitor().abortRequested(): + return True + elif window('emby_shouldStop') == "true": + return True + else: # Keep going + return False + def kodiSQL(media_type="video"): if media_type == "emby": @@ -161,32 +227,19 @@ def querySQL(query, args=None, cursor=None, conntype=None): def getScreensaver(): # Get the current screensaver value - query = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Settings.getSettingValue", - 'params': { - - 'setting': "screensaver.mode" - } - } - return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value'] + result = JSONRPC('Settings.getSettingValues').execute({'setting': "screensaver.mode"}) + try: + return result['result']['value'] + except KeyError: + return "" def setScreensaver(value): # Toggle the screensaver - query = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Settings.setSettingValue", - 'params': { - - 'setting': "screensaver.mode", - 'value': value - } + params = { + 'setting': "screensaver.mode", + 'value': value } - result = xbmc.executeJSONRPC(json.dumps(query)) + result = JSONRPC('Settings.setSettingValue').execute(params) log.info("Toggling screensaver: %s %s" % (value, result)) def convertDate(date): @@ -254,11 +307,28 @@ def indent(elem, level=0): if level and (not elem.tail or not elem.tail.strip()): elem.tail = i +def catch_except(errors=(Exception, ), default_value=False): + # Will wrap method with try/except and print parameters for easier debugging + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except errors as error: + log.exception(error) + log.error("function: %s \n args: %s \n kwargs: %s", + func.__name__, args, kwargs) + return default_value + + return wrapper + return decorator + def profiling(sortby="cumulative"): # Will print results to Kodi log def decorator(func): def wrapper(*args, **kwargs): - + import cProfile + import pstats + pr = cProfile.Profile() pr.enable() @@ -286,6 +356,7 @@ def reset(): return # first stop any db sync + window('emby_online', value="reset") window('emby_shouldStop', value="true") count = 10 while window('emby_dbScan') == "true": @@ -340,36 +411,16 @@ def reset(): cursor.execute("DELETE FROM " + tablename) cursor.execute('DROP table IF EXISTS emby') cursor.execute('DROP table IF EXISTS view') + cursor.execute("DROP table IF EXISTS version") connection.commit() cursor.close() # Offer to wipe cached thumbnails - resp = dialog.yesno(language(29999), language(33086)) - if resp: - log.warn("Resetting all cached artwork.") + if dialog.yesno(language(29999), language(33086)): + log.warn("Resetting all cached artwork") # Remove all existing textures first - path = xbmc.translatePath("special://thumbnails/").decode('utf-8') - if xbmcvfs.exists(path): - allDirs, allFiles = xbmcvfs.listdir(path) - for dir in allDirs: - allDirs, allFiles = xbmcvfs.listdir(path+dir) - for file in allFiles: - if os.path.supports_unicode_filenames: - xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8'))) - else: - xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file)) - - # remove all existing data from texture DB - connection = kodiSQL('texture') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tableName = row[0] - if(tableName != "version"): - cursor.execute("DELETE FROM " + tableName) - connection.commit() - cursor.close() + import artwork + artwork.Artwork().delete_cache() # reset the install run flag settings('SyncInstallRunDone', value="false") @@ -377,15 +428,18 @@ def reset(): # Remove emby info resp = dialog.yesno(language(29999), language(33087)) if resp: + import connectmanager # Delete the settings addon = xbmcaddon.Addon() - addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') + addondir = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/").decode('utf-8') dataPath = "%ssettings.xml" % addondir xbmcvfs.delete(dataPath) - log.info("Deleting: settings.xml") + connectmanager.ConnectManager().clear_data() dialog.ok(heading=language(29999), line1=language(33088)) xbmc.executebuiltin('RestartApp') + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) def sourcesXML(): # To make Master lock compatible diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index a2d21cf2..3a9a4640 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -62,23 +62,26 @@ class VideoNodes(object): # Verify the video directory if not xbmcvfs.exists(path): - shutil.copytree( - src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), - dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) + try: + shutil.copytree( + src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), + dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) + except Exception as error: + log.error(error) + xbmcvfs.exists(path) + if delete: + dirs, files = xbmcvfs.listdir(nodepath) + for file in files: + xbmcvfs.delete(nodepath + file) + + log.info("Sucessfully removed videonode: %s." % tagname) + return # Create the node directory if not xbmcvfs.exists(nodepath) and not mediatype == "photos": # We need to copy over the default items xbmcvfs.mkdirs(nodepath) - else: - if delete: - dirs, files = xbmcvfs.listdir(nodepath) - for file in files: - xbmcvfs.delete(nodepath + file) - - log.info("Sucessfully removed videonode: %s." % tagname) - return # Create index entry nodeXML = "%sindex.xml" % nodepath @@ -319,7 +322,7 @@ class VideoNodes(object): nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) path = "library://video/emby_%s.xml" % cleantagname - windowpath = "ActivateWindow(Video,%s,return)" % path + windowpath = "ActivateWindow(Videos,%s,return)" % path # Create the video node directory if not xbmcvfs.exists(nodepath): @@ -333,6 +336,7 @@ class VideoNodes(object): 'Favorite movies': 30180, 'Favorite tvshows': 30181, + 'Favorite episodes': 30182, 'channels': 30173 } label = lang(labels[tagname]) @@ -349,6 +353,9 @@ class VideoNodes(object): if itemtype == "channels": root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels" + elif itemtype == "favourites" and mediatype == "episodes": + root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2) + etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=favepisodes" %(tagname, mediatype) else: root = self.commonRoot(order=1, label=label, tagname=tagname) etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index db0f8c00..fb86f82a 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -8,14 +8,13 @@ import threading import websocket import xbmc -import xbmcgui import clientinfo import downloadutils import librarysync import playlist import userclient -from utils import window, settings, language as lang +from utils import window, settings, dialog, language as lang, JSONRPC ################################################################################################## @@ -24,278 +23,316 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################## -class WebSocket_Client(threading.Thread): +class WebSocketClient(threading.Thread): _shared_state = {} - client = None - stopWebsocket = False + _client = None + _stop_websocket = False def __init__(self): self.__dict__ = self._shared_state self.monitor = xbmc.Monitor() - - self.doUtils = downloadutils.DownloadUtils() - self.clientInfo = clientinfo.ClientInfo() - self.deviceId = self.clientInfo.getDeviceId() - self.librarySync = librarysync.LibrarySync() - + + self.doutils = downloadutils.DownloadUtils() + self.client_info = clientinfo.ClientInfo() + self.device_id = self.client_info.get_device_id() + self.library_sync = librarysync.LibrarySync() + threading.Thread.__init__(self) - def sendProgressUpdate(self, data): - + def send_progress_update(self, data): + log.debug("sendProgressUpdate") try: - messageData = { + message = { 'MessageType': "ReportPlaybackProgress", 'Data': data } - messageString = json.dumps(messageData) - self.client.send(messageString) - log.debug("Message data: %s" % messageString) + message_str = json.dumps(message) + self._client.send(message_str) + log.debug("Message data: %s", message_str) - except Exception as e: - log.exception(e) + except Exception as error: + log.exception(error) def on_message(self, ws, message): result = json.loads(message) - messageType = result['MessageType'] + message_type = result['MessageType'] data = result['Data'] - dialog = xbmcgui.Dialog() - if messageType not in ('SessionEnded'): + if message_type not in ('NotificationAdded', 'SessionEnded', 'RestartRequired', + 'PackageInstalling'): # Mute certain events - log.info("Message: %s" % message) + log.info("Message: %s", message) - if messageType == "Play": + if message_type == 'Play': # A remote control play command has been sent from the server. - itemIds = data['ItemIds'] - command = data['PlayCommand'] + self._play(data) - pl = playlist.Playlist() - - if command == "PlayNow": - dialog.notification( - heading=lang(29999), - message="%s %s" % (len(itemIds), lang(33004)), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - startat = data.get('StartPositionTicks', 0) - pl.playAll(itemIds, startat) - - elif command == "PlayNext": - dialog.notification( - heading=lang(29999), - message="%s %s" % (len(itemIds), lang(33005)), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - newplaylist = pl.modifyPlaylist(itemIds) - player = xbmc.Player() - if not player.isPlaying(): - # Only start the playlist if nothing is playing - player.play(newplaylist) - - elif messageType == "Playstate": + elif message_type == 'Playstate': # A remote control update playstate command has been sent from the server. - command = data['Command'] - player = xbmc.Player() + self._playstate(data) - actions = { - - 'Stop': player.stop, - 'Unpause': player.pause, - 'Pause': player.pause, - 'NextTrack': player.playnext, - 'PreviousTrack': player.playprevious, - 'Seek': player.seekTime - } - action = actions[command] - if command == "Seek": - seekto = data['SeekPositionTicks'] - seektime = seekto / 10000000.0 - action(seektime) - log.info("Seek to %s." % seektime) - else: - action() - log.info("Command: %s completed." % command) - - window('emby_command', value="true") - - elif messageType == "UserDataChanged": + elif message_type == "UserDataChanged": # A user changed their personal rating for an item, or their playstate was updated userdata_list = data['UserDataList'] - self.librarySync.triage_items("userdata", userdata_list) + self.library_sync.triage_items("userdata", userdata_list) - elif messageType == "LibraryChanged": - - librarySync = self.librarySync - processlist = { + elif message_type == "LibraryChanged": + self._library_changed(data) - 'added': data['ItemsAdded'], - 'update': data['ItemsUpdated'], - 'remove': data['ItemsRemoved'] - } - for action in processlist: - librarySync.triage_items(action, processlist[action]) + elif message_type == "GeneralCommand": + self._general_commands(data) - elif messageType == "GeneralCommand": - - command = data['Name'] - arguments = data['Arguments'] + elif message_type == "ServerRestarting": + self._server_restarting() - if command in ('Mute', 'Unmute', 'SetVolume', - 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'): - - player = xbmc.Player() - # These commands need to be reported back - if command == "Mute": - xbmc.executebuiltin('Mute') - elif command == "Unmute": - xbmc.executebuiltin('Mute') - elif command == "SetVolume": - volume = arguments['Volume'] - xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) - elif command == "SetAudioStreamIndex": - index = int(arguments['Index']) - player.setAudioStream(index - 1) - elif command == "SetSubtitleStreamIndex": - embyindex = int(arguments['Index']) - currentFile = player.getPlayingFile() - - mapping = window('emby_%s.indexMapping' % currentFile) - if mapping: - externalIndex = json.loads(mapping) - # If there's external subtitles added via playbackutils - for index in externalIndex: - if externalIndex[index] == embyindex: - player.setSubtitleStream(int(index)) - break - else: - # User selected internal subtitles - external = len(externalIndex) - audioTracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(external + embyindex - audioTracks - 1) - else: - # Emby merges audio and subtitle index together - audioTracks = len(player.getAvailableAudioStreams()) - player.setSubtitleStream(index - audioTracks - 1) - - # Let service know - window('emby_command', value="true") - - elif command == "DisplayMessage": - - header = arguments['Header'] - text = arguments['Text'] - dialog.notification( - heading=header, - message=text, - icon="special://home/addons/plugin.video.emby/icon.png", - time=4000) - - elif command == "SendString": - - string = arguments['String'] - text = { - - 'jsonrpc': "2.0", - 'id': 0, - 'method': "Input.SendText", - 'params': { - - 'text': "%s" % string, - 'done': False - } - } - result = xbmc.executeJSONRPC(json.dumps(text)) - - else: - builtin = { - - 'ToggleFullscreen': 'Action(FullScreen)', - 'ToggleOsdMenu': 'Action(OSD)', - 'ToggleContextMenu': 'Action(ContextMenu)', - 'MoveUp': 'Action(Up)', - 'MoveDown': 'Action(Down)', - 'MoveLeft': 'Action(Left)', - 'MoveRight': 'Action(Right)', - 'Select': 'Action(Select)', - 'Back': 'Action(back)', - 'GoHome': 'ActivateWindow(Home)', - 'PageUp': 'Action(PageUp)', - 'NextLetter': 'Action(NextLetter)', - 'GoToSearch': 'VideoLibrary.Search', - 'GoToSettings': 'ActivateWindow(Settings)', - 'PageDown': 'Action(PageDown)', - 'PreviousLetter': 'Action(PrevLetter)', - 'TakeScreenshot': 'TakeScreenshot', - 'ToggleMute': 'Mute', - 'VolumeUp': 'Action(VolumeUp)', - 'VolumeDown': 'Action(VolumeDown)', - } - action = builtin.get(command) - if action: - xbmc.executebuiltin(action) - - elif messageType == "ServerRestarting": - if settings('supressRestartMsg') == "true": - dialog.notification( - heading=lang(29999), - message=lang(33006), - icon="special://home/addons/plugin.video.emby/icon.png") - - elif messageType == "UserConfigurationUpdated": + elif message_type == "UserConfigurationUpdated": # Update user data set in userclient - userclient.UserClient().userSettings = data - self.librarySync.refresh_views = True + userclient.UserClient().get_user(data) + self.library_sync.refresh_views = True + + elif message_type == "ServerShuttingDown": + # Server went offline + window('emby_online', value="false") + + @classmethod + def _play(cls, data): + + item_ids = data['ItemIds'] + command = data['PlayCommand'] + + playlist_ = playlist.Playlist() + + if command == 'PlayNow': + startat = data.get('StartPositionTicks', 0) + playlist_.play_all(item_ids, startat) + dialog(type_="notification", + heading="{emby}", + message="%s %s" % (len(item_ids), lang(33004)), + icon="{emby}", + sound=False) + + elif command == 'PlayNext': + new_playlist = playlist_.modify_playlist(item_ids) + dialog(type_="notification", + heading="{emby}", + message="%s %s" % (len(item_ids), lang(33005)), + icon="{emby}", + sound=False) + player = xbmc.Player() + if not player.isPlaying(): + # Only start the playlist if nothing is playing + player.play(new_playlist) + + @classmethod + def _playstate(cls, data): + + command = data['Command'] + player = xbmc.Player() + + actions = { + + 'Stop': player.stop, + 'Unpause': player.pause, + 'Pause': player.pause, + 'NextTrack': player.playnext, + 'PreviousTrack': player.playprevious + } + if command == 'Seek': + + seek_to = data['SeekPositionTicks'] + seek_time = seek_to / 10000000.0 + player.seekTime(seek_time) + log.info("Seek to %s", seek_time) + + elif command in actions: + actions[command]() + log.info("Command: %s completed", command) + + else: + log.info("Unknown command: %s", command) + return + + window('emby_command', value="true") + + def _library_changed(self, data): + + process_list = { + + 'added': data['ItemsAdded'], + 'update': data['ItemsUpdated'], + 'remove': data['ItemsRemoved'] + } + for action in process_list: + self.library_sync.triage_items(action, process_list[action]) + + @classmethod + def _general_commands(cls, data): + + command = data['Name'] + arguments = data['Arguments'] + + if command in ('Mute', 'Unmute', 'SetVolume', + 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'): + + player = xbmc.Player() + # These commands need to be reported back + if command == 'Mute': + xbmc.executebuiltin('Mute') + + elif command == 'Unmute': + xbmc.executebuiltin('Mute') + + elif command == 'SetVolume': + volume = arguments['Volume'] + xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume) + + elif command == 'SetAudioStreamIndex': + index = int(arguments['Index']) + player.setAudioStream(index - 1) + + elif command == 'SetSubtitleStreamIndex': + emby_index = int(arguments['Index']) + current_file = player.getPlayingFile() + mapping = window('emby_%s.indexMapping' % current_file) + + if emby_index == -1: + player.showSubtitles(False) + + elif mapping: + external_index = json.loads(mapping) + # If there's external subtitles added via playbackutils + for index in external_index: + if external_index[index] == emby_index: + player.setSubtitleStream(int(index)) + break + else: + # User selected internal subtitles + external = len(external_index) + audio_tracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(external + emby_index - audio_tracks - 1) + else: + # Emby merges audio and subtitle index together + audio_tracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(emby_index - audio_tracks - 1) + + # Let service know + window('emby_command', value="true") + + elif command == 'DisplayMessage': + + header = arguments['Header'] + text = arguments['Text'] + dialog(type_="notification", + heading=header, + message=text, + icon="{emby}", + time=int(settings('displayMessage'))*1000) + + elif command == 'SendString': + + params = { + + 'text': arguments['String'], + 'done': False + } + JSONRPC('Input.SendText').execute(params) + + elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): + # Commands that should wake up display + actions = { + + 'MoveUp': "Input.Up", + 'MoveDown': "Input.Down", + 'MoveRight': "Input.Right", + 'MoveLeft': "Input.Left" + } + JSONRPC(actions[command]).execute() + + elif command == 'GoHome': + JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) + + else: + builtin = { + + 'ToggleFullscreen': 'Action(FullScreen)', + 'ToggleOsdMenu': 'Action(OSD)', + 'ToggleContextMenu': 'Action(ContextMenu)', + 'Select': 'Action(Select)', + 'Back': 'Action(back)', + 'PageUp': 'Action(PageUp)', + 'NextLetter': 'Action(NextLetter)', + 'GoToSearch': 'VideoLibrary.Search', + 'GoToSettings': 'ActivateWindow(Settings)', + 'PageDown': 'Action(PageDown)', + 'PreviousLetter': 'Action(PrevLetter)', + 'TakeScreenshot': 'TakeScreenshot', + 'ToggleMute': 'Mute', + 'VolumeUp': 'Action(VolumeUp)', + 'VolumeDown': 'Action(VolumeDown)', + } + if command in builtin: + xbmc.executebuiltin(builtin[command]) + + @classmethod + def _server_restarting(cls): + + if settings('supressRestartMsg') == "true": + dialog(type_="notification", + heading="{emby}", + message=lang(33006), + icon="{emby}") + window('emby_online', value="false") def on_close(self, ws): - log.debug("Closed.") + log.debug("closed") def on_open(self, ws): - self.doUtils.postCapabilities(self.deviceId) + self.doutils.post_capabilities(self.device_id) def on_error(self, ws, error): + if "10061" in str(error): # Server is offline pass else: - log.debug("Error: %s" % error) + log.debug("Error: %s", error) def run(self): - loglevel = int(window('emby_logLevel')) # websocket.enableTrace(True) - - userId = window('emby_currUser') - server = window('emby_server%s' % userId) - token = window('emby_accessToken%s' % userId) + user_id = window('emby_currUser') + server = window('emby_server%s' % user_id) + token = window('emby_accessToken%s' % user_id) # Get the appropriate prefix for the websocket if "https" in server: server = server.replace('https', "wss") else: server = server.replace('http', "ws") - websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.deviceId) - log.info("websocket url: %s" % websocket_url) + websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.device_id) + log.info("websocket url: %s", websocket_url) - self.client = websocket.WebSocketApp(websocket_url, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close) - - self.client.on_open = self.on_open + self._client = websocket.WebSocketApp(websocket_url, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + self._client.on_open = self.on_open log.warn("----===## Starting WebSocketClient ##===----") while not self.monitor.abortRequested(): - self.client.run_forever(ping_interval=10) - if self.stopWebsocket: + if window('emby_online') == "true": + self._client.run_forever(ping_interval=10) + + if self._stop_websocket: break if self.monitor.waitForAbort(5): @@ -304,8 +341,8 @@ class WebSocket_Client(threading.Thread): log.warn("##===---- WebSocketClient Stopped ----===##") - def stopClient(self): + def stop_client(self): - self.stopWebsocket = True - self.client.close() - log.info("Stopping thread.") \ No newline at end of file + self._stop_websocket = True + self._client.close() + log.info("Stopping thread") diff --git a/resources/settings.xml b/resources/settings.xml index 25e8b247..6cf93f47 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,27 +1,24 @@ - - - - - - - - - - - - - + + + + + + + - + + + + + + + - - - - + @@ -52,8 +49,8 @@ - - + + @@ -65,11 +62,14 @@ - + + + + @@ -77,8 +77,11 @@ + + + diff --git a/resources/skins/default/1080i/script-emby-connect-login-manual.xml b/resources/skins/default/1080i/script-emby-connect-login-manual.xml new file mode 100644 index 00000000..57e4a88a --- /dev/null +++ b/resources/skins/default/1080i/script-emby-connect-login-manual.xml @@ -0,0 +1,145 @@ + + + 200 + 0 + dialogeffect + + + Background fade + 100% + 100% + emby-bg-fade.png + + + + 600 + 35% + 20% + + Background box + white.png + 600 + 480 + + + + 485 + False + + Error box + white.png + 100% + 50 + + + + Error message + white + font10 + center + center + 50 + + + + + Emby logo + logo-white.png + keep + 120 + 49 + 30 + 25 + + + + 500 + 50 + + Please sign in + + white + font12 + top + center + 100% + 100 + + + + 150 + + Username + + ffa6a6a6 + font10 + top + + + + separator + 102% + 0.5 + 66 + -10 + emby-separator.png + + + + + Password + 225 + + Password label + + ffa6a6a6 + font10 + top + + + + separator + 102% + 0.5 + 66 + -10 + emby-separator.png + + + + + Buttons + 335 + + Sign in + box.png + box.png + + font10 + ffa6a6a6 + white + center + 100% + 50 + 201 + + + + Cancel + box.png + box.png + + font10 + ffa6a6a6 + white + center + 100% + 50 + 55 + 200 + + + + + + \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-connect-login.xml b/resources/skins/default/1080i/script-emby-connect-login.xml index b5b01632..cb69387c 100644 --- a/resources/skins/default/1080i/script-emby-connect-login.xml +++ b/resources/skins/default/1080i/script-emby-connect-login.xml @@ -1,6 +1,6 @@ - 200 + 200 0 dialogeffect @@ -13,21 +13,41 @@ 600 - 33% + 35% 15% Background box - box.png + white.png 600 700 + + + 705 + False + + Error box + white.png + 100% + 50 + + + + Error message + white + font10 + center + center + 50 + + Emby logo logo-white.png 160 49 - 20 + 30 25 @@ -47,7 +67,7 @@ 190 Username email - + ffa6a6a6 font10 top @@ -59,7 +79,7 @@ 0.5 66 -10 - separator.png + emby-separator.png @@ -80,7 +100,7 @@ 0.5 66 -10 - separator.png + emby-separator.png @@ -89,10 +109,12 @@ 385 Sign in - box.png - box.png + box.png + box.png font10 + ffa6a6a6 + white center 100% 50 @@ -100,11 +122,13 @@ - Later + Cancel box.png - box.png + box.png font10 + ffa6a6a6 + white center 100% 50 @@ -124,30 +148,32 @@ true top 340 + 100% - - qrcode - qrcode_disclaimer.png - 140 - 140 - 10 - 360 - + + + Scan me + + font12 + ff0b8628 + top + 200 + 120 + 230 + - - Scan me - - font12 - green - right - top - 120 - 160 + + qrcode + qrcode_disclaimer.png + 140 + 140 + 10 + 360 + - \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-connect-server-manual.xml b/resources/skins/default/1080i/script-emby-connect-server-manual.xml new file mode 100644 index 00000000..27e50037 --- /dev/null +++ b/resources/skins/default/1080i/script-emby-connect-server-manual.xml @@ -0,0 +1,154 @@ + + + 200 + 0 + dialogeffect + + + Background fade + 100% + 100% + emby-bg-fade.png + + + + 600 + 35% + 20% + + Background box + white.png + 600 + 525 + + + + 530 + False + + Error box + white.png + 100% + 50 + + + + Error message + white + font10 + center + center + 50 + + + + + Emby logo + logo-white.png + keep + 120 + 49 + 30 + 25 + + + + 500 + 50 + + Connect to server + + white + font12 + top + center + 100% + 100 + + + + 150 + + Host + + ffa6a6a6 + font10 + top + + + + separator + 102% + 0.5 + 66 + -10 + emby-separator.png + + + + Host example + + ff464646 + font10 + top + 70 + + + + + Port + 275 + + Port label + + ffa6a6a6 + font10 + top + + + + separator + 102% + 0.5 + 66 + -10 + emby-separator.png + + + + + Buttons + 380 + + Connect + box.png + box.png + + font10 + ffa6a6a6 + white + center + 100% + 50 + 201 + + + + Cancel + box.png + box.png + + font10 + ffa6a6a6 + white + center + 100% + 50 + 55 + 200 + + + + + + \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-connect-server.xml b/resources/skins/default/1080i/script-emby-connect-server.xml new file mode 100644 index 00000000..bc751e8c --- /dev/null +++ b/resources/skins/default/1080i/script-emby-connect-server.xml @@ -0,0 +1,280 @@ + + + 205 + 0 + dialogeffect + + + Background fade + 100% + 100% + emby-bg-fade.png + + + + 450 + 38% + 15% + + Background box + white.png + 450 + 710 + + + + Emby logo + logo-white.png + keep + 120 + 49 + 30 + 25 + + + + User info + 70 + 350 + 50 + + User image + userflyoutdefault.png + keep + center + 100% + 70 + 40 + + + + Busy animation + center + 23 + 100% + 105 + False + fading_circle.png + keep + conditional + + + + Welcome user + white + font12 + center + top + 120 + 100% + 50 + + + + separator + 102% + 0.5 + 165 + -10 + emby-separator.png + + + + Select server + ffa6a6a6 + + font10 + center + top + 170 + 100% + 50 + + + + + 290 + 100% + 184 + + Connect servers + 0 + 100% + 100% + 10 + 55 + 155 + 205 + 206 + 205 + 206 + 155 + 60 + 250 + + + 45 + 45 + + Network + keep + network.png + StringCompare(ListItem.Property(server_type),network) + + + Wifi + keep + wifi.png + StringCompare(ListItem.Property(server_type),wifi) + + + + + 300 + 40 + 55 + font10 + center + ff838383 + ListItem.Label + + + + + 45 + 45 + + Network + keep + network.png + StringCompare(ListItem.Property(server_type),network) + + + Wifi + keep + wifi.png + StringCompare(ListItem.Property(server_type),wifi) + + + + + 300 + 40 + 55 + font10 + center + white + ListItem.Label + Control.HasFocus(155) + + + 300 + 40 + 55 + font10 + center + ff838383 + ListItem.Label + !Control.HasFocus(155) + + + + + + 395 + 10 + 5 + 100% + 155 + 60 + 60 + box.png + box.png + box.png + false + + + + 100% + 220 + + 45 + 150 + + True + Sign in Connect + box.png + box.png + + font10 + ffa6a6a6 + white + center + 350 + 50 + 50 + 155 + 206 + + + + Manually add server + box.png + box.png + + font10 + ffa6a6a6 + white + center + 55 + 350 + 50 + 50 + 205 + 155 + 201 + + + + Cancel + box.png + box.png + + font10 + ffa6a6a6 + white + center + 110 + 350 + 50 + 50 + 206 + + + + + 100% + False + + Message box + white.png + 100% + 50 + 20 + + + + Message + white + font10 + center + center + 50 + 20 + + + + + + + \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-connect-users.xml b/resources/skins/default/1080i/script-emby-connect-users.xml new file mode 100644 index 00000000..0acd2b32 --- /dev/null +++ b/resources/skins/default/1080i/script-emby-connect-users.xml @@ -0,0 +1,198 @@ + + + 155 + 0 + dialogeffect + + + Background fade + 100% + 100% + emby-bg-fade.png + + + + 715 + 32% + 20% + + Background box + white.png + 100% + 525 + + + + Emby logo + logo-white.png + keep + 120 + 49 + 30 + 25 + + + + Please sign in + + white + font12 + top + center + 80 + 100% + + + + 100 + 620 + 245 + 50 + + Select User + 0 + 100% + 40 + 155 + 155 + 200 + 60 + horizontal + 250 + + + 150 + + User image + ff888888 + ListItem.Icon + keep + 100% + 150 + + + + Background label + white.png + 100% + 50 + 150 + + + + 100% + center + 50 + 150 + font10 + white + ListItem.Label + + + + + + 150 + + User image + ListItem.Icon + keep + 100% + 150 + Control.HasFocus(155) + + + User image + ff888888 + ListItem.Icon + keep + 100% + 150 + !Control.HasFocus(155) + + + + Background label + white.png + 100% + 50 + 150 + Control.HasFocus(155) + + + Background label + white.png + 100% + 50 + 150 + !Control.HasFocus(155) + + + + 100% + center + 50 + 150 + font10 + white + ListItem.Label + + + + + + + 100% + 615 + 5 + 155 + 60 + 60 + box.png + box.png + box.png + false + horizontal + + + + 615 + 325 + 100% + + + Manual Login button + box.png + box.png + + center + 100% + 50 + 35 + font10 + ffa6a6a6 + white + 201 + 155 + + + + Cancel + box.png + box.png + + font10 + ffa6a6a6 + white + center + 100% + 50 + 90 + 200 + + + + + + + \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-context.xml b/resources/skins/default/1080i/script-emby-context.xml new file mode 100644 index 00000000..df4f0588 --- /dev/null +++ b/resources/skins/default/1080i/script-emby-context.xml @@ -0,0 +1,109 @@ + + + 155 + 0 + dialogeffect + + + Background fade + 100% + 100% + emby-bg-fade.png + + + + 450 + 38% + 36% + + Background box + white.png + 90 + + + + Emby logo + emby-icon.png + keep + 30 + 20 + 370 + + + + User image + + keep + 34 + 20 + 285 + + + + separator + 100% + 0.5 + 70 + -5 + emby-separator.png + + + + 450 + 90 + + 100% + 100% + center + 155 + 155 + 155 + 155 + 200 + + + Background box + 450 + white.png + + + 400 + font11 + ff525252 + 25 + center + center + ListItem.Label + + + + + + Background box + 450 + white.png + + + 400 + 25 + center + white.png + Control.HasFocus(155) + Visible + Hidden + + + 400 + font11 + white + 25 + center + center + ListItem.Label + + + + + + + \ No newline at end of file diff --git a/resources/skins/default/1080i/script-emby-kodi-UserPreferences.xml b/resources/skins/default/1080i/script-emby-kodi-UserPreferences.xml deleted file mode 100644 index 329fd9a4..00000000 --- a/resources/skins/default/1080i/script-emby-kodi-UserPreferences.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - 8011 - 0 - - - 560 - 140 - 800 - 800 - - - box.png - 100 - - - box.png - 100 - 700 - - - user image - 80 - 100 - - - 90 - 30 - 40 - 20% - left - font16 - ffeeeeee - - - - 320 - 30 - left - 40 - 100% - font16 - ffeeeeee - - - - separator - -10 - 103 - 101% - 1 - separator.png - - - 50 - 600 - vertical - left - 20 - - - 8 - 8012 - 8012 - 100 - 785 - left - 70 - radio-on.png - radio-off.png - white.png - 750 - 32 - 32 - 30 - B3dddddd - - - - - - - separator - 70 - -10 - 101% - 1 - separator.png - - - Save - 150 - 20 - 230 - 8011 - 8013 - 8013 - font14 - - FFededed - B3dddddd - FF000000 - 40 - ff333333 - box.png - box.png - - - Cancel - 430 - 20 - 230 - 8011 - 8012 - 8012 - font14 - - FFededed - B3dddddd - FF000000 - 40 - ff333333 - box.png - box.png - - - - - \ No newline at end of file diff --git a/resources/skins/default/media/emby-icon.png b/resources/skins/default/media/emby-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7c85caf3cba44da1dd5bb456035b7622204ff4 GIT binary patch literal 1850 zcmb`Idr%Ws6vppvf(cQZh)ONcCPhWDBGeaJ#9fREj1Uk+5Y(Wwr4?IfD-scQv#12r z*0fIXfkLRI6h)#U-~*CX6z~P26p%OgAPP2sAqfeabo-d;w9}c+bUJfpe&?Kf&-b0b z?#&Dq`A+0GZ~y=k{ry&k007Y`1lSB3aYYg9Xp{#BtXoAhq;$>y9#*Thur)udOhj46 zHeU&kBR|lNY9GJqRd$@oZak9p;OjlNF%vb~In+_#e=qX#x?$7sZB6aPuJft2l33N2 z8_%vKT;Dcg9?|IgF87vwS)ZBLoFluNCaL~>YsJQ^1@yV+2aq=0eh~W; zo&!3)V$t84f4%ZwEB>s69md=~YI@QW{4bvWAV`R*EAVdWG^}Z!VyqOe?XJhX35wbV zzhim>Zu3l^7WGpQZ%J~;1KyT{7+Yb@*uaKD5axaU3TY!3(#d#&5h0-7X8KV)6lX)* z5N2_fg$Gw)?2T^Jk$WrHirh58lyXG@+iB`#G>y%UF8u4_& z16&~uutM+(%858Yn3uv)_PY*{&ga;3;Xg2e$hP0bBu(@7Tw zbW3dtcO$@bBCYWXl0faILCMkOC?3i5f; z#FeTF1S2n4XkV!Oy%C1O;OduZg6>mOzMn^r;hc~|7+@~3DHI~W`9#_cEsJcebUvB3 zQp-XQhRRzg!2XofmnjGCA(BTVN+wDlh1MvfuKuzbOG^NI0UL$OgPM@-+YjL8tg&NCf5cL5~P3 z;DZJcRLBR{L=dgwW+=YtDxeo_bnPBt2p+g9bPUDlPNXek=+})V1e@C!%gi6ftk#wn zjg1+nR{v~kTlW- zTpit4nq!dhYKwe3TQ=hph<4qE@Kg=gSzZ`V=HWN$a?hvVX-pC{@K*~KAY+8HU%PA&ax1Q_Q?! zJ|LCglXzNpxR+XmR{Y09bS;uX=?6*BZ}uW+}&YZFXE99-(r-%zeX_6^`4;otknBX|5Y`Np5wX2s2-c zF)ebP%WJS=&R)T8747w7YOeLnABKd;X(pKPp)ovajGN<>6N)*fT+cFdCh z`P7NylwQ2|`WTg1Cl8xre*b?}j}JNGxSs*p&Ba|rT=oEb`6QfV*sli3)f+#rWrg^3 zd)EhhRL^S-vvZ8zsxHe|Ccb@+Z%_O9xTq^pD!E`c6*?m#0>ar_TX@9HZoS3bDc6M! z3K4o@mP&NM1lQm}!UF-{j}{|_&*pb-fN_NjyVB8rnYo(RO>K}(9=m)}y(Td_c(XY-#4>YTnJih)rnBaOYq6OkB;6NqfXA?K-?e5A<1)_Z{ zh+`oiLOnmj_mX3>L;mjD-sAO9c=xEPw9#~Qj8M$Gju6rJ?P?Ql{7CoGiB?!2T8ce0 zEJY7Ah?510We`VGom-Mxo}y+qHAYjINlDHRUlac1g1F>c#pcC}ZI-$k{h^0$ z?T$7-NG??T*yg(w+0iVPDE ze$~NRFJ9v++7iK$*Rdt$vNOcskuOUVn2M#g2R%>0^`nQ2UGrNUNh?3tkvVJ&j!vee zRf(U;#(vH!d$91cbJ~>L9)ib++<{)UAaAZ*cvTW0Fmh69@Y!RfDN3?=*~TNY}Wht6od46_7JE++d=$^rAP3O?|k& zXuW}L*SeiLh5L{vy2bbHzYQ-nsXl4B_N)p}K7o#w_oI|*=D!>8wYVTYMKlfKxbqaD zm_afDG}cLdVLG@D>ZP5zCEm$wpEn8D4$AnVC*7@h_4L_2h`#zd_@bMCfT-H}pb_Ne zvcG$tziq}#H}}|A`1c+?n5>sShPLS)Own3&ij2ZZSK1~$;=AsMfpduG-nVZ;`lz;4 zh0rtfbyHA)o!ja6Sw&cgwD6Z?bqPcHI(6_K zZ-#l-oKpgHv}9-h6Rd%=@|GYVv_|~T`(O=M`Q;ivPSje{s)YA+E-{3L zr&lsst=a-T0-(}O^212Se@5XCRtW6v_ZpXh!A)`@xN~M~py&q>{+qx}o7aCnpk(l0 z(q~y;qHow=bpX(OO+n_7^&4YZ-J(aI>62)%e{6C>9qOizymC>}@U=(=yaKWvQ(l2TLEMFgFgc}>v{Lq6LNxjI>v4p9cKF=IL z3rnwma>dG;So}|uwR)(Ba&2qW`x{j#zL2xHm*^sRWjK+6R5Pr?5LQzHaUWp&TGV{M z$ESR$2)X=jS1Rk2Gs@Y^6OzL`Qllmj*dZ&uCMG5AdyiME7xCWp{>ncK5Re9wiA|Hq qecG%l$$rcK#oYGoj`_%ohmvs9fz?W#<+_*uO6M7H(C<@r09&c7wR#u*o?vo@l7O*Bul6@NAu*(1$0A#zEhQ#)Up~oh1 z40!?ZfFkD?!ax*Y(ijR|LpZjOiC{a|@dB9)US=uwkRY&ULiK$i(^s5de+!ubY>-1{ zVu@H05hK=IUwuAz`AmjTVA4Jpk_mhN6+(hnweIQ;K43;7VDyRbAY~@BBqpvT3zM`>?MA4_YrY99Dvk5dgcTNy z2dFN42BX?J83Cb^a%@zFN-QtY!)4-1OhA2XtID}^RXrP3)!DhKYLygLH{+#C){BUo zy3tkS+9mdd-0`lX7Ps!ErM} zOo@KD<0Uran0kKv+jelOKXrq%vP-mh-I~W6$@Tx|=)>`0x&L$vy3rL9i9)>u@gP^P zb3EqvFQ&1s*a{FD(0wQ!Y?sl%iPDPFIe>B;exN=0Z%K_#re(Xd<}=?$CsvxUQDcP(BpwW}uALYKpack=?EI~mV;x;Tbp+t zyY2G^!S%)Fx1y8Tt0uG8{JzopfA7ClN^jJnDpRj>v4vbWl@e!FzLFaGIPB1bPunW` zgpN&_&RVSz;c)iW(X|P!S?7)`MJUbcbc}eM8ZxgY;;{W*gN`=WFIyt5B9_iRBQLk+ zk?+&#Qqxwd7wN7sK6PSmQ)!5P{e+K!CM*}~9|Y0+>`pF?^UX|CSMN8t{6dg4?HIB+I z^+^@EGi5#VQ{f{&&IQ^n>(Mt`8tIT(Z6IaQ6DG2B<*^L@AgQ@M({vaAJd?3Fn2o9D z;<5kynQwY#_ZeCk%QMQQ&&V{HW_zol^8JcG9nSyXzUFq1I}))!=o8;*!|rUm_ggo7 cd3cV$IEZ%^lVPScFbObty85}Sb4q9e0Pz`5JOBUy literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/userflyoutdefault.png b/resources/skins/default/media/userflyoutdefault.png new file mode 100644 index 0000000000000000000000000000000000000000..046676e597da515a8969467dad0fe42877b092fb GIT binary patch literal 1338 zcmai!do&XY0LGWRTB(ub6`Q7vD6utnbnk3zLndLSxYmk1Vj;#DceFKA&MCvCbW4__ zsd*Q&d1W3cbXVq;R}spMn0t+@|L-~Xcg}ad?|k3?AJ@whrv)?w0ssIl0v_uP0Bk$* zLz>&S8qZ=TZp#9^JbYZZT<%shFfafDfe8r-85tQ+C{#^NEhs3+-rk-@qY;TjU0q!# zCnpAj;p^)g9v)7mQn6Sp2m~S!2sSo0mX?-radBWUI4vy=jYfxthLXu-BodkCCgK19 zs)YnB+J`QmAK6hm0S2jgndCcMaeJH0QASra5Q*Z2Qs}s;ReOWGq|ySY!oL20Z18j5 zE!oyw9+HL0eTMd6T5AI{8MTAv>@Rl=ITUgQe3?)*VQ|*Ak=fwiIDud{S)Eao#$p%> z%k%e)hh-by(YDW-e?=YgJRm?~$uaPJK{l;=CEpTWF-WPe;&#g^ zZ0YB6lh|i`E}!Zn&4nZ;k9*1_OIYg-9XD89E|S3BI(Zp@)gU2HRGzZ|B{I zn~C%Tie#Ff!gC&jdiyNi9lYa_XhhRmOr3e`jMpfZYeO803ou?Kr{^^1 z3iX8dLCk`&-^WwPs)9ACA6ue(?^WQWhHP;%i98>6MZ}VuRri)&ZMJembT;1-+55ae zUSNF+PPw6qp{vvKF~Nt6N!gOD^J*&nO|TINcmX$&povd7Wk~28_nK&YDoND!t!`3G%P?%7*+fL9gR4#B6eS_SYeUCGuB8V48;D&(?Qr#6 zvDHWJTmfGtmOP?lzt-U;9$$Fe^15r+!0f1vv1r@_>D-(FKXloavWBY3Kg9dE!6VAx z0;#U47jvSxgKpEb`GMG}P5rY%7^Bj|Rm!pRS+@P}!`Y}jy{6;s8h9u36AHpXPf{u$ z*BC+-K2ZS390ZnwvVMG4-A{4GZ$vh#)$;S}NWUC``@PyRQRTFW&=BRVSg464t~WFicZ+)OJM zwmjXvsz3XI-w?xLbcTCZ=vOzK8%v9cz^-06j`eqFax{%{TG(or@o@9RI86=9Q(MSInGi6IzjRF zlPb$=`*W8;y}N5Dp>rmC(z7Xb(}DG@yY=1uu1Dt{7*%I>LWW9aZ}go-@K;{Wh-Yxb t_I&jyX1Bjc{UKX^>6>h@b~RsfW%+QqE;k7W`c_+_K9;a>t8_ab^`za literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/userflyoutdefault2.png b/resources/skins/default/media/userflyoutdefault2.png new file mode 100644 index 0000000000000000000000000000000000000000..24d771eb7e9f8c1d50ce033f37c57e25892432d4 GIT binary patch literal 1700 zcmb`Hdo8`l$IUqth0aYKYPwT=Q;1^dB4y1InVpo%VW@8l@-(# z007EVHzzLuAbAM^0$Psf>;6nRPBCaqXF2ER=jZ0;7EgKcuOaU)#$xyH+X9#TONJ$J zNm-g&GA|X29v+W(u=Z4){54w+dC|QAW|kaNo;lrJT`>A8UY*bqM%kpZ`pcWkM5T$N z%?&KZ$H%^Kue%j5DbUH{#`YlquotOL4$Or9@z-Oq{S4xXVULy0Q-k-gw1K+=^Pijz z=t`-V9lNUPCT6PhC(R?o-B9J^%^?yj^Bz(XfrSLv&c7%EjQ>Z{jsp{umGuP}wx2{2 zp4Ki=7FAdzdN-EP3JaB3}tmgO}9N|BXN_wMSj+5orDa;j< zhIqV+`pMjIa4AQKF-E2B28VaTWW+-u5XA==BViywgCdbT|hoR=+4xMOnScNgf$`Vpq30?}a6ya4gY$(#@bQ2Lsib;5DQ~)s)y2g}aRf<;& zWSo?^&)EqZ59tGD{#`s~KQ=GZ%){w3wkz!WdBRf#S?kmvuzaOQX_d{w>Q4XBt+E(k zV9wSX_fqUym8j2kMYegraLNk+GsWU34=3=9pKnuA-(#f~BkPy#MQ7GwC_m_AxJ-u& z1wxrOF_fedlAtG32&&L&eM&y~nR8Kycy?HGu2h3H7@FNSL$?IDkk%)c-q}h3)lP@B zqK5wqppk^G<7Jv6!_!u{N6P{II62nRie?XA=cJ#pKgiSsU2&w!*3DnPbW(SwsIJWjVMhU!GW1bS<*+pLSt7TO?o!abGrGrxwn_}5rOB#a370u zg8Ad4pz=qgDZol z4sY)bFj=1ZRSU9d$IicRoXZ!n7@TeGCIoM^%w}KTvXq8o_v*Hwiq{k6P-G+JP5oAP zi^R}nFdi|NNUnvMJYMFNScPWIi9SC6p4KImmE1myLlXH}0YUmw%LMR* zzm@#Tf9NxUrX>d6e_wuSPm$Eq2Ik!em|9nOz~6S7nsos2ylW{ zSU(C}&*i+TG%{gkDKWxpZ?~W6g-kUlcjAMBt4!2bEr&;Jy`X+Y7iyPU8Ar)fawH%< z6g5RMa%fo{qgWF@m6{YkqX@T|L*086G2DXmqMSx|9ptL3_)5Jho*NIPH7TTmY|X5U z;2?G6$*k|i$EXC!S4<3k2k^Ndny9}F!hKqIK57v+LuSc88SM6^0=st`L+-N(+F@>P z<(AUJ{AU~tiV~~k~J9Z*hFTw=D^h&sM1K2{cB@4xt&{cA9cDK? z%=fJvWuF8e&hD6wA^<75ee@-oxz^I*!Y~mQHFS)yYw`Z3F~SKnNAk;jzRazjGbXCR!nq+@yk%-dWS1YmGcP5cd=vIfx7V&fV}u-*sy!z_Aw6 zfV)T>f{OSLlB{CA4G{q3sP>fOmtKuWYaCIk8({=M|A5HYSWCvHTYppK_7%uKctCZg KJ5@Qdj{OO+*$OQH literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/wifi.png b/resources/skins/default/media/wifi.png new file mode 100644 index 0000000000000000000000000000000000000000..2a646c5599262ee182823c73a615d5210f617895 GIT binary patch literal 1095 zcmV-N1i1T&P)h`|Vkz8*+h1fk~|PiG8uq!4(g4q}HIS#=ADzy+Av2bkIlhrbk4 zZxTme8%0?ZK2a54fEZVE5P7H&S$Y^?fevh!4QrMIsr3K<{{yr4BB}J(;rOWW{`dU< z*Yf=XsrUE)|Df^y|NQ;;_5APj{MX+1GO6_6`TmgV{W8w`1G)G@v-Icu|G@J8o9g|D zu<8837i z_lV2)BEI*i%l44O^?AAUX}R>*`Twx={(0v8-|_qd!TPw~`AN(9-{kmd!S-3R^pu)n z{{R309duGoQvk!4OnHZg5Vas|YD_MNr=^E3AP^8fE^IzLTwDqywQ&Fd0@O)FK~!ko z?U`v)+E5gRbFzSFSQHVpyN*+qGH{xOZWBv|CDwZ( zmY18vNJ&Xak&(??mhG5Crej-HJ{!Mb(@JxzI#m_t!~RLhCShu|vOLj)(yVUx+@GMHiCzrQ|3c3FW3pN+Kpo z5%;iub>8r5k9glMZoAP2%o1U|dAC!WpmX$AgkjB`@O%5U5Dq?p%vY_}4J)7q*K18C zsqeOG(|NHaldOeEY+0q=*Zg>1;~ytp;D@h1ib-tsYMr%weg5%VJTd&)_f10kKD1sq zQks4KZ;!f@6W1I6`dBqusuS(l9xLvJjNw@?yfHbnB|%C1kpIHOScFO}C}9itRnA_I z9SV|ykXo+0+=an;W5@G4jf)Sw>)%i1Q*1*CTYhaleHl!a&Ug~)U!}vKP!7M(k9|u4`vZA~R#VZn 3): - kplayer.reportPlayback() - lastProgressUpdate = datetime.today() - - elif window('emby_command') == "true": - # Received a remote control command that - # requires updating immediately - window('emby_command', clear=True) - kplayer.reportPlayback() - lastProgressUpdate = datetime.today() - - except Exception: - log.exception("Exception in Playback Monitor Service") - else: - # Start up events - self.warn_auth = True - if settings('connectMsg') == "true" and self.welcome_msg: - # Reset authentication warnings - self.welcome_msg = False - # Get additional users - additionalUsers = user.AdditionalUser - if additionalUsers: - add = ", %s" % ", ".join(additionalUsers) - else: - add = "" - xbmcgui.Dialog().notification( - heading=lang(29999), - message=("%s %s%s!" - % (lang(33000), user.currUser.decode('utf-8'), - add.decode('utf-8'))), - icon="special://home/addons/plugin.video.emby/icon.png", - time=2000, - sound=False) - - # Start monitoring kodi events - if not self.kodimonitor_running: - self.kodimonitor_running = kodimonitor.KodiMonitor() - - # Start the Websocket Client - if not self.websocket_running: - self.websocket_running = True - ws.start() - # Start the syncing thread - if not self.library_running: - self.library_running = True - library.start() - else: - - if (user.currUser is None) and self.warn_auth: - # Alert user is not authenticated and suppress future warning - self.warn_auth = False - log.info("Not authenticated yet.") - - # User access is restricted. - # Keep verifying until access is granted - # unless server goes offline or Kodi is shut down. - while user.HasAccess == False: - # Verify access with an API call - user.hasAccess() - - if window('emby_online') != "true": - # Server went offline - break - - if monitor.waitForAbort(5): - # Abort was requested while waiting. We should exit - break - else: - # Wait until Emby server is online - # or Kodi is shut down. - while not monitor.abortRequested(): - - if user.getServer() == False: - # No server info set in add-on settings - pass - - elif user.getPublicUsers() == False: - # Server is offline. - # Alert the user and suppress future warning - if self.server_online: - log.info("Server is offline.") - window('emby_online', value="false") - - xbmcgui.Dialog().notification( - heading=lang(33001), - message="%s %s" % (self.addonName, lang(33002)), - icon="special://home/addons/plugin.video.emby/icon.png", - sound=False) - - self.server_online = False - - elif window('emby_online') == "sleep": - # device going to sleep - if self.websocket_running: - ws.stopClient() - ws = wsc.WebSocket_Client() - self.websocket_running = False - - if self.library_running: - library.stopThread() - library = librarysync.LibrarySync() - self.library_running = False - - else: - # Server is online - if not self.server_online: - # Server was offline when Kodi started. - # Wait for server to be fully established. - if monitor.waitForAbort(5): - # Abort was requested while waiting. - break - # Alert the user that server is online. - xbmcgui.Dialog().notification( - heading=lang(29999), - message=lang(33003), - icon="special://home/addons/plugin.video.emby/icon.png", - time=2000, - sound=False) - - self.server_online = True - log.info("Server is online and ready.") - window('emby_online', value="true") - - # Start the userclient thread - if not self.userclient_running: - self.userclient_running = True - user.start() - - break - - if monitor.waitForAbort(1): - # Abort was requested while waiting. - break - - if monitor.waitForAbort(1): - # Abort was requested while waiting. We should exit - break - - ##### Emby thread is terminating. ##### - - if self.userclient_running: - user.stopClient() - - if self.library_running: - library.stopThread() - - if self.websocket_running: - ws.stopClient() - - log.warn("======== STOP %s ========" % self.addonName) - -# Delay option -delay = int(settings('startupDelay')) -log.warn("Delaying emby startup by: %s sec..." % delay) - -if delay and xbmc.Monitor().waitForAbort(delay): - # Start the service - log.warn("Abort requested while waiting. Emby for kodi not started.") -else: - Service().ServiceEntryPoint() \ No newline at end of file + try: + if DELAY and xbmc.Monitor().waitForAbort(DELAY): + raise RuntimeError("Abort event while waiting to start Emby for kodi") + # Start the service + service.service_entry_point() + except Exception as error: + ga = GoogleAnalytics() + errStrings = ga.formatException() + ga.sendEventData("Exception", errStrings[0], errStrings[1]) + log.exception(error) + log.info("Forcing shutdown") + service.shutdown()