From 9ac37a1c405ee8f6e3204067cbf6e64fd3d23a1d Mon Sep 17 00:00:00 2001 From: angelblue05 Date: Sun, 7 Jan 2018 20:13:11 -0600 Subject: [PATCH] 3.0.0 Revision Krypton Update playback for Krytpon Support Multi source Add Force Transcode Add a small listener for external players Update dialog skin (thank you sualfred) --- addon.xml | 26 +- contextmenu_play.py | 37 + default.py | 8 +- resources/language/English/strings.xml | 7 +- resources/lib/api.py | 31 +- resources/lib/artwork.py | 9 +- resources/lib/clientinfo.py | 15 - resources/lib/context_entry.py | 48 +- resources/lib/dialogs/context.py | 1 - resources/lib/dialogs/loginconnect.py | 30 +- resources/lib/dialogs/loginmanual.py | 32 +- resources/lib/dialogs/resume.py | 60 + resources/lib/dialogs/serverconnect.py | 8 +- resources/lib/dialogs/servermanual.py | 29 +- resources/lib/dialogs/usersconnect.py | 2 +- resources/lib/downloadutils.py | 2 +- resources/lib/entrypoint.py | 147 ++- resources/lib/kodimonitor.py | 67 +- resources/lib/librarysync.py | 2 +- resources/lib/objects/_kodi_movies.py | 73 +- resources/lib/objects/_kodi_music.py | 45 +- resources/lib/objects/_kodi_tvshows.py | 27 +- resources/lib/objects/movies.py | 58 +- resources/lib/objects/music.py | 18 +- resources/lib/objects/tvshows.py | 159 +-- resources/lib/playbackutils.py | 544 ++++----- resources/lib/player.py | 158 +-- resources/lib/playlist.py | 170 ++- resources/lib/playutils.py | 1053 +++++++---------- resources/lib/read_embyserver.py | 51 +- resources/lib/service_entry.py | 12 +- resources/lib/userclient.py | 2 +- resources/lib/utils.py | 2 +- resources/lib/views.py | 12 +- resources/lib/websocket_client.py | 29 +- resources/settings.xml | 15 +- .../script-emby-connect-login-manual.xml | 289 +++-- .../1080i/script-emby-connect-login.xml | 359 +++--- .../script-emby-connect-server-manual.xml | 296 +++-- .../1080i/script-emby-connect-server.xml | 470 ++++---- .../1080i/script-emby-connect-users.xml | 388 +++--- .../default/1080i/script-emby-context.xml | 156 ++- .../default/1080i/script-emby-resume.xml | 110 ++ .../media/buttons/shadow_smallbutton.png | Bin 0 -> 407 bytes .../default/media/dialogs/dialog_back.png | Bin 0 -> 15141 bytes .../skins/default/media/dialogs/menu_back.png | Bin 0 -> 15358 bytes .../default/media/dialogs/menu_bottom.png | Bin 0 -> 14781 bytes .../skins/default/media/dialogs/menu_top.png | Bin 0 -> 14768 bytes .../skins/default/media/dialogs/white.jpg | Bin 0 -> 8060 bytes .../default/media/items/focus_square.png | Bin 0 -> 1970 bytes .../default/media/items/logindefault.png | Bin 0 -> 17637 bytes .../skins/default/media/items/mask_square.png | Bin 0 -> 1075 bytes .../default/media/items/shadow_square.png | Bin 0 -> 1374 bytes resources/skins/default/media/network.png | Bin 727 -> 3818 bytes resources/skins/default/media/spinner.gif | Bin 0 -> 123744 bytes resources/skins/default/media/wifi.png | Bin 1095 -> 3959 bytes 56 files changed, 2679 insertions(+), 2378 deletions(-) create mode 100644 contextmenu_play.py create mode 100644 resources/lib/dialogs/resume.py create mode 100644 resources/skins/default/1080i/script-emby-resume.xml create mode 100644 resources/skins/default/media/buttons/shadow_smallbutton.png create mode 100644 resources/skins/default/media/dialogs/dialog_back.png create mode 100644 resources/skins/default/media/dialogs/menu_back.png create mode 100644 resources/skins/default/media/dialogs/menu_bottom.png create mode 100644 resources/skins/default/media/dialogs/menu_top.png create mode 100644 resources/skins/default/media/dialogs/white.jpg create mode 100644 resources/skins/default/media/items/focus_square.png create mode 100644 resources/skins/default/media/items/logindefault.png create mode 100644 resources/skins/default/media/items/mask_square.png create mode 100644 resources/skins/default/media/items/shadow_square.png create mode 100644 resources/skins/default/media/spinner.gif diff --git a/addon.xml b/addon.xml index 44e3d141..2971d10d 100644 --- a/addon.xml +++ b/addon.xml @@ -1,10 +1,10 @@ - + @@ -16,12 +16,17 @@ - - - - Settings for the Emby Server - [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(embyid))] + !IsEmpty(Window(10000).Property(emby_context)) - + + + + + [!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + !String.IsEmpty(Window(10000).Property(emby_context)) + + + + [[!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + [String.IsEqual(ListItem.DBTYPE,movie) | String.IsEqual(ListItem.DBTYPE,episode)]] + !String.IsEmpty(Window(10000).Property(emby_context)) + !String.IsEmpty(Window(10000).Property(emby_context_transcode)) + + all @@ -32,8 +37,9 @@ 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! - v2.3.56 - - Add support for Kodi Leia (Alpha) MusicDatabase v67 + v3.0.0 + - Minimum version supported is now Kodi Krypton. + - For more info, check the emby Kodi forum thread. \ No newline at end of file diff --git a/contextmenu_play.py b/contextmenu_play.py new file mode 100644 index 00000000..f0766f40 --- /dev/null +++ b/contextmenu_play.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import os +import sys + +import xbmc +import xbmcaddon + +################################################################################################# + +_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_play") + +################################################################################################# + +if __name__ == "__main__": + + try: + # Start the context menu + ContextMenu(True) + except Exception as error: + log.exception(error) diff --git a/default.py b/default.py index cd26b78c..2c0896c4 100644 --- a/default.py +++ b/default.py @@ -102,7 +102,13 @@ class Main(object): 'deviceid': entrypoint.resetDeviceId, 'delete': entrypoint.deleteItem, 'connect': entrypoint.emby_connect, - 'backup': entrypoint.emby_backup + 'backup': entrypoint.emby_backup, + + 'manuallogin': entrypoint.test_manual_login, + 'connectlogin': entrypoint.test_connect_login, + 'manualserver': entrypoint.test_manual_server, + 'connectservers': entrypoint.test_connect_servers, + 'connectusers': entrypoint.test_connect_users } if mode in modes: # Simple functions diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 5510dfaf..915379e4 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -5,7 +5,7 @@ Emby for Kodi Server address Server name - Play from HTTP instead of SMB + Enable HTTP playback Log level Device Name Advanced @@ -175,7 +175,8 @@ Channels - Emby options + Emby Options + Emby Force transcode Add to Emby favorites Remove from Emby favorites Set custom song rating @@ -206,7 +207,7 @@ Network credentials Enable Emby cinema mode Ask to play trailers - Skip Emby delete confirmation for the context menu (use at your own risk) + Skip Emby delete confirmation (use at your own risk) Jump back on resume (in seconds) Force transcode H265 Music metadata options (not compatible with direct stream) diff --git a/resources/lib/api.py b/resources/lib/api.py index a26d9ced..9ce8083b 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -7,6 +7,8 @@ import logging from utils import settings +import artwork + ################################################################################################## log = logging.getLogger("EMBY."+__name__) @@ -19,6 +21,7 @@ class API(object): def __init__(self, item): # item is the api response self.item = item + self.artwork = artwork.Artwork() def get_userdata(self): # Default @@ -71,12 +74,8 @@ class API(object): writer = [] cast = [] - try: - people = self.item['People'] - except KeyError: - pass - else: - for person in people: + if 'People' in self.item: + for person in self.item['People']: type_ = person['Type'] name = person['Name'] @@ -95,6 +94,26 @@ class API(object): 'Cast': cast } + def get_actors(self): + + cast = [] + + if 'People' in self.item: + + self.artwork.get_people_artwork(self.item['People']) + + for person in self.item['People']: + + if person['Type'] == "Actor": + cast.append({ + 'name': person['Name'], + 'role': person['Role'], + 'order': len(cast) + 1, + 'thumbnail': person['imageurl'] + }) + + return cast + def get_media_streams(self): video_tracks = [] diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4b537140..6240b254 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -468,7 +468,7 @@ class Artwork(object): image = "" person_id = person['Id'] - if "PrimaryImageTag" in person: + if 'PrimaryImageTag' in person: image = ( "%s/emby/Items/%s/Images/Primary?" "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" @@ -518,14 +518,14 @@ class Artwork(object): tag, custom_query)) all_artwork['Backdrop'].append(artwork) - def get_artwork(item_id, type_, tag): + def get_artwork(item_id, type_, tag, override_name=None): if not tag: return 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 + all_artwork[override_name or type_] = artwork # Process backdrops get_backdrops(item_id, backdrops) @@ -554,6 +554,9 @@ class Artwork(object): get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork, item['Parent%sImageTag' % parent_artwork]) + if 'SeriesPrimaryImageTag' in item: + get_artwork(item['SeriesId'], "Primary", item['SeriesPrimaryImageTag'], "Series.Primary" if all_artwork['Primary'] else None) + # Parent album works a bit differently if not all_artwork['Primary']: diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 10bc6a7e..ee8a0b05 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -78,22 +78,7 @@ class ClientInfo(object): 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(emby_guid) guid = xbmcvfs.File(emby_guid) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 5daf439e..e21a1a74 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -4,21 +4,26 @@ import logging import sys +from datetime import timedelta import xbmc import xbmcaddon import api import read_embyserver as embyserver +import playbackutils as pbutils import embydb_functions as embydb import musicutils as musicutils from utils import settings, dialog, language as lang -from dialogs import context +from dialogs import context, resume from database import DatabaseConn ################################################################################################# log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +XML_PATH = (addon.getAddonInfo('path'), "default", "1080i") OPTIONS = { 'Refresh': lang(30410), @@ -38,7 +43,7 @@ class ContextMenu(object): _selected_option = None - def __init__(self): + def __init__(self, force_transcode=False): self.emby = embyserver.Read_EmbyServer() @@ -53,7 +58,10 @@ class ContextMenu(object): self.item = self.emby.getItem(self.item_id) self.api = api.API(self.item) - if self._select_menu(): + if force_transcode: + self._force_transcode() + + elif self._select_menu(): self._action_menu() if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], @@ -104,10 +112,6 @@ class ContextMenu(object): 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']) @@ -126,9 +130,7 @@ class ContextMenu(object): # 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 = context.ContextMenu("script-emby-context.xml", *XML_PATH) context_menu.set_options(options) context_menu.doModal() @@ -141,10 +143,7 @@ class ContextMenu(object): selected = self._selected_option.decode('utf-8') - if selected == OPTIONS['Transcode']: - pass - - elif selected == OPTIONS['Refresh']: + if selected == OPTIONS['Refresh']: self.emby.refreshItem(self.item_id) elif selected == OPTIONS['AddFav']: @@ -198,3 +197,24 @@ class ContextMenu(object): if delete: log.info("Deleting request: %s", self.item_id) self.emby.deleteItem(self.item_id) + + def _force_transcode(self): + + log.info("Force transcode called.") + seektime = self.api.adjust_resume(self.api.get_userdata()['Resume']) + + if seektime: + log.info("Resume dialog called.") + + dialog = resume.ResumeDialog("script-emby-resume.xml", *XML_PATH) + dialog.set_resume_point("Resume from %s" % str(timedelta(seconds=seektime)).split(".")[0]) + dialog.doModal() + + if dialog.is_selected(): + if not dialog.get_selected(): # Start from beginning selected. + self.item['UserData']['PlaybackPositionTicks'] = 0 + else: # User backed out + log.info("User exited without a selection.") + return + + pbutils.PlaybackUtils(self.item).play(self.item['Id'], self.kodi_id, True) diff --git a/resources/lib/dialogs/context.py b/resources/lib/dialogs/context.py index 1f47f625..28b7fd78 100644 --- a/resources/lib/dialogs/context.py +++ b/resources/lib/dialogs/context.py @@ -57,7 +57,6 @@ class ContextMenu(xbmcgui.WindowXMLDialog): 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): diff --git a/resources/lib/dialogs/loginconnect.py b/resources/lib/dialogs/loginconnect.py index db7c39cc..a7c630c0 100644 --- a/resources/lib/dialogs/loginconnect.py +++ b/resources/lib/dialogs/loginconnect.py @@ -22,6 +22,8 @@ SIGN_IN = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 +USER = 204 +PASSWORD = 205 ERROR = { 'Invalid': 1, 'Empty': 2 @@ -52,21 +54,14 @@ class LoginConnect(xbmcgui.WindowXMLDialog): def onInit(self): - self.user_field = self._add_editcontrol(725, 385, 40, 500) + self.user_field = self.getControl(USER) self.setFocus(self.user_field) - self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1) + self.password_field = self.getControl(PASSWORD) 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: @@ -97,23 +92,6 @@ class LoginConnect(xbmcgui.WindowXMLDialog): 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) diff --git a/resources/lib/dialogs/loginmanual.py b/resources/lib/dialogs/loginmanual.py index 1c663ccc..c4aee908 100644 --- a/resources/lib/dialogs/loginmanual.py +++ b/resources/lib/dialogs/loginmanual.py @@ -23,6 +23,8 @@ SIGN_IN = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 +USER = 204 +PASSWORD = 205 ERROR = { 'Invalid': 1, 'Empty': 2 @@ -50,7 +52,7 @@ class LoginManual(xbmcgui.WindowXMLDialog): self.server = server def set_user(self, user): - self.username = user or {} + self.username = user or None def get_user(self): return self._user @@ -61,8 +63,8 @@ class LoginManual(xbmcgui.WindowXMLDialog): 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) + self.user_field = self.getControl(USER) + self.password_field = self.getControl(PASSWORD) if self.username: self.user_field.setText(self.username) @@ -70,13 +72,6 @@ class LoginManual(xbmcgui.WindowXMLDialog): 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: @@ -106,23 +101,6 @@ class LoginManual(xbmcgui.WindowXMLDialog): 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): try: diff --git a/resources/lib/dialogs/resume.py b/resources/lib/dialogs/resume.py new file mode 100644 index 00000000..fe51ea5c --- /dev/null +++ b/resources/lib/dialogs/resume.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui +import xbmcaddon + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +RESUME = 3010 +START_BEGINNING = 3011 + +################################################################################################## + + +class ResumeDialog(xbmcgui.WindowXMLDialog): + + _resume_point = None + selected_option = None + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_resume_point(self, time): + self._resume_point = time + + def is_selected(self): + return True if self.selected_option is not None else False + + def get_selected(self): + return self.selected_option + + def onInit(self): + + self.getControl(RESUME).setLabel(self._resume_point) + self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021)) + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def onClick(self, controlID): + + if controlID == RESUME: + self.selected_option = 1 + self.close() + + if controlID == START_BEGINNING: + self.selected_option = 0 + self.close() diff --git a/resources/lib/dialogs/serverconnect.py b/resources/lib/dialogs/serverconnect.py index 541ca6f9..65ec0035 100644 --- a/resources/lib/dialogs/serverconnect.py +++ b/resources/lib/dialogs/serverconnect.py @@ -21,7 +21,6 @@ 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 @@ -35,7 +34,6 @@ MANUAL_SERVER = 206 class ServerConnect(xbmcgui.WindowXMLDialog): - username = "" user_image = None servers = [] @@ -49,7 +47,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog): xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) def set_args(self, **kwargs): - # connect_manager, username, user_image, servers, emby_connect + # connect_manager, user_image, servers, emby_connect for key, value in kwargs.iteritems(): setattr(self, key, value) @@ -77,13 +75,11 @@ class ServerConnect(xbmcgui.WindowXMLDialog): 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]") + self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]%s[/B][/UPPERCASE]" % lang(30618)) if self.servers: self.setFocus(self.list_) diff --git a/resources/lib/dialogs/servermanual.py b/resources/lib/dialogs/servermanual.py index c3a9ce19..3aa0b268 100644 --- a/resources/lib/dialogs/servermanual.py +++ b/resources/lib/dialogs/servermanual.py @@ -24,6 +24,8 @@ CONNECT = 200 CANCEL = 201 ERROR_TOGGLE = 202 ERROR_MSG = 203 +HOST = 204 +PORT = 205 ERROR = { 'Invalid': 1, 'Empty': 2 @@ -57,19 +59,12 @@ class ServerManual(xbmcgui.WindowXMLDialog): 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.host_field = self.getControl(HOST) + self.port_field = self.getControl(PORT) 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: @@ -99,22 +94,6 @@ class ServerManual(xbmcgui.WindowXMLDialog): 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) if port else server diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py index 770b0a2c..b91ff69d 100644 --- a/resources/lib/dialogs/usersconnect.py +++ b/resources/lib/dialogs/usersconnect.py @@ -54,7 +54,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog): self.list_ = self.getControl(LIST) for user in self.users: - user_image = ("userflyoutdefault2.png" if 'PrimaryImageTag' not in user + user_image = ("items/logindefault.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)) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index e609a336..0f86065e 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -110,7 +110,7 @@ class DownloadUtils(object): "SetAudioStreamIndex,SetSubtitleStreamIndex," "SetRepeatMode," "Mute,Unmute,SetVolume," - "Play,Playstate,PlayNext" + "Play,Playstate,PlayNext,PlayMediaSource" ) } diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 58c975dc..56a4b549 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -30,12 +30,15 @@ import playbackutils as pbutils import playutils import api from views import Playlist, VideoNodes -from utils import window, settings, dialog, language as lang, plugin_path +from utils import window, settings, dialog, language as lang, urllib_path ################################################################################################# log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon(id='plugin.video.emby') +XML_PATH = (addon.getAddonInfo('path'), "default", "1080i") + ################################################################################################# @@ -107,6 +110,12 @@ def doMainListing(): log.info(window('emby_server%s.name' % server)) addDirectoryItem(window('emby_server%s.name' % server), "plugin://plugin.video.emby/?mode=%s" % server)''' + addDirectoryItem("Manual login dialog", "plugin://plugin.video.emby/?mode=manuallogin") + addDirectoryItem("Connect login dialog", "plugin://plugin.video.emby/?mode=connectlogin") + addDirectoryItem("Manual server dialog", "plugin://plugin.video.emby/?mode=manualserver") + addDirectoryItem("Connect servers dialog", "plugin://plugin.video.emby/?mode=connectservers") + addDirectoryItem("Connect users dialog", "plugin://plugin.video.emby/?mode=connectusers") + 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") @@ -123,6 +132,140 @@ def doMainListing(): xbmcplugin.endOfDirectory(int(sys.argv[1])) +def test_manual_login(): + from dialogs import LoginManual + dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH) + dialog.set_server("Test server") + dialog.set_user("Test user") + dialog.doModal() + +def test_connect_login(): + from dialogs import LoginConnect + dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH) + dialog.doModal() + +def test_manual_server(): + from dialogs import ServerManual + dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) + dialog.doModal() + +def test_connect_servers(): + from dialogs import ServerConnect + dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH) + test_servers = [ + { + u'LastConnectionMode': 2, + u'Name': u'Server Name', + u'AccessToken': u'Token', + u'RemoteAddress': u'http://remote.address:8096', + u'UserId': u'd4000909883845059aadef13b7110375', + u'ManualAddress': u'http://manual.address:8096', + u'DateLastAccessed': '2018-01-01T02:36:58Z', + u'LocalAddress': u'http://local.address:8096', + u'Id': u'b1ef1940b1964e2188f00b73611d53fd', + u'Users': [ + { + u'IsSignedInOffline': True, + u'Id': u'd4000909883845059aadef13b7110375' + } + ] + } + ] + kwargs = { + 'username': "Test user", + 'user_image': None, + 'servers': test_servers, + 'emby_connect': True + } + dialog.set_args(**kwargs) + dialog.doModal() + +def test_connect_users(): + from dialogs import UsersConnect + test_users = [{ + u'Name': u'Guest', + u'HasConfiguredEasyPassword': False, + u'LastActivityDate': u'2018-01-01T04:10:15.4195197Z', + u'HasPassword': False, + u'LastLoginDate': u'2017-12-28T02:53:01.5770625Z', + u'Policy': { + u'EnabledDevices': [ + + ], + u'EnableMediaPlayback': True, + u'EnableRemoteControlOfOtherUsers': False, + u'RemoteClientBitrateLimit': 0, + u'BlockUnratedItems': [ + + ], + u'EnableAllDevices': True, + u'InvalidLoginAttemptCount': 0, + u'EnableUserPreferenceAccess': True, + u'EnableLiveTvManagement': False, + u'EnableLiveTvAccess': False, + u'IsAdministrator': False, + u'EnableContentDeletion': False, + u'EnabledChannels': [ + + ], + u'IsDisabled': False, + u'EnableSyncTranscoding': False, + u'EnableAudioPlaybackTranscoding': True, + u'EnableSharedDeviceControl': False, + u'AccessSchedules': [ + + ], + u'IsHidden': False, + u'EnableContentDeletionFromFolders': [ + + ], + u'EnableContentDownloading': False, + u'EnableVideoPlaybackTranscoding': True, + u'EnabledFolders': [ + u'0c41907140d802bb58430fed7e2cd79e', + u'a329cda1727467c08a8f1493195d32d3', + u'f137a2dd21bbc1b99aa5c0f6bf02a805', + u'4514ec850e5ad0c47b58444e17b6346c' + ], + u'EnableAllChannels': False, + u'BlockedTags': [ + + ], + u'EnableAllFolders': False, + u'EnablePublicSharing': False, + u'EnablePlaybackRemuxing': True + }, + u'ServerId': u'Test', + u'Configuration': { + u'SubtitleMode': u'Default', + u'HidePlayedInLatest': True, + u'GroupedFolders': [ + + ], + u'DisplayCollectionsView': False, + u'OrderedViews': [ + + ], + u'SubtitleLanguagePreference': u'', + u'AudioLanguagePreference': u'', + u'LatestItemsExcludes': [ + + ], + u'EnableLocalPassword': False, + u'RememberAudioSelections': True, + u'RememberSubtitleSelections': True, + u'DisplayMissingEpisodes': False, + u'PlayDefaultAudioTrack': True, + u'EnableNextEpisodeAutoPlay': True + }, + u'Id': u'a9d56d37cb6b47a3bfd3453c55138ff1', + u'HasConfiguredPassword': False + }] + dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH) + dialog.set_server("Test server") + dialog.set_users(test_users) + dialog.doModal() + def emby_connect(): # Login user to emby connect @@ -657,7 +800,7 @@ def BrowseContent(viewname, browse_type="", folderid=""): 'type': browse_type, 'folderid': item['Id'] } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True) else: #playable item, set plugin path and mediastreams xbmcplugin.setContent(int(sys.argv[1]), 'episodes' if folderid else 'files') diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index fd376dee..b41fe331 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -4,6 +4,7 @@ import json import logging +import threading import xbmc import xbmcgui @@ -26,11 +27,15 @@ KODI = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) class KodiMonitor(xbmc.Monitor): retry = True + special_monitor = None def __init__(self): xbmc.Monitor.__init__(self) + if settings('useDirectPaths') == "0": + self.special_monitor = SpecialMonitor().start() + self.download = downloadutils.DownloadUtils().downloadUrl log.info("Kodi monitor started") @@ -60,6 +65,11 @@ class KodiMonitor(xbmc.Monitor): log.info("New context setting: %s", current_context) window('emby_context', value=current_context) + current_context = "true" if settings('enableContextTranscode') == "true" else "" + if window('emby_context_transcode') != current_context: + log.info("New context transcode setting: %s", current_context) + window('emby_context_transcode', value=current_context) + @log_error() def onNotification(self, sender, method, data): @@ -142,7 +152,7 @@ class KodiMonitor(xbmc.Monitor): else: window('emby_%s.playmethod' % playurl, value="DirectPlay") # Set properties for player.py - playback.setProperties(playurl, listitem) + playback.set_properties(playurl, listitem) def _video_update(self, data): # Manually marking as watched/unwatched @@ -199,3 +209,58 @@ class KodiMonitor(xbmc.Monitor): log.info("Could not retrieve item Id") return item_id + + +class SpecialMonitor(threading.Thread): + + _stop_thread = False + external_count = 0 + + def run(self): + + ''' Detect the resume dialog for widgets. + Detect external players. + ''' + monitor = xbmc.Monitor() + + log.warn("----====# Starting Special Monitor #====----") + + while not self._stop_thread: + + player = xbmc.Player() + isPlaying = player.isPlaying() + + if (not isPlaying and xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)') and + not xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and + xbmc.getInfoLabel('Control.GetLabel(1002)') == xbmc.getLocalizedString(12021)): + + control = int(xbmcgui.Window(10106).getFocusId()) + if control == 1002: # Start from beginning + log.info("Resume dialog: Start from beginning selected.") + window('emby.resume', value="true") + else: + window('emby.resume', clear=True) + + elif isPlaying and not window('emby.external_check'): + time = player.getTime() + + if time > 1: + window('emby.external_check', value="true") + self.external_count = 0 + elif self.external_count == 15: + log.info("External player detected.") + window('emby.external', value="true") + window('emby.external_check', value="true") + self.external_count = 0 + elif time == 0: + self.external_count += 1 + + + if monitor.waitForAbort(0.5): + # Abort was requested while waiting. We should exit + break + + log.warn("#====---- Special Monitor Stopped ----====#") + + def stop_monitor(self): + self._stop_thread = True diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 11598b57..83181a01 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -241,7 +241,7 @@ class LibrarySync(threading.Thread): # use emby and video DBs with database.DatabaseConn('emby') as cursor_emby: - with database.DatabaseConn('video') as cursor_video: + with database.DatabaseConn('video') as cursor_video: # content sync: movies, tvshows, musicvideos, music if manualrun: diff --git a/resources/lib/objects/_kodi_movies.py b/resources/lib/objects/_kodi_movies.py index 5ed72f5a..5e55787f 100644 --- a/resources/lib/objects/_kodi_movies.py +++ b/resources/lib/objects/_kodi_movies.py @@ -63,18 +63,6 @@ class KodiMovies(KodiItems): 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 = ( ''' @@ -88,17 +76,6 @@ class KodiMovies(KodiItems): 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", @@ -177,48 +154,16 @@ class KodiMovies(KodiItems): def add_countries(self, kodi_id, countries): - if self.kodi_version > 14: + for country in countries: + country_id = self._get_country(country) - 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)) + query = ( + ''' + INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) + VALUES (?, ?, ?) + ''' + ) + self.cursor.execute(query, (country_id, kodi_id, "movie")) def _add_country(self, country): diff --git a/resources/lib/objects/_kodi_music.py b/resources/lib/objects/_kodi_music.py index 6c4655b9..831801b9 100644 --- a/resources/lib/objects/_kodi_music.py +++ b/resources/lib/objects/_kodi_music.py @@ -189,26 +189,6 @@ class KodiMusic(KodiItems): 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_18(self, *args): - query = ' '.join(( - - "UPDATE album", - "SET strArtistsDisp = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iUserrating = ?, lastScraped = ?, strReleaseType = ?", - "WHERE idAlbum = ?" - )) - self.cursor.execute(query, (args)) - - def update_album_17(self, *args): query = ' '.join(( "UPDATE album", @@ -218,27 +198,6 @@ class KodiMusic(KodiItems): )) 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(( @@ -356,8 +315,8 @@ class KodiMusic(KodiItems): "UPDATE song", "SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack = ?,", - "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", - "rating = ?, comment = ?", + "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", + "rating = ?, comment = ?", "WHERE idSong = ?" )) self.cursor.execute(query, (args)) diff --git a/resources/lib/objects/_kodi_tvshows.py b/resources/lib/objects/_kodi_tvshows.py index 59a1e63b..fb2c2043 100644 --- a/resources/lib/objects/_kodi_tvshows.py +++ b/resources/lib/objects/_kodi_tvshows.py @@ -174,9 +174,8 @@ class KodiTVShows(KodiItems): 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)) + query = "UPDATE seasons SET name = ? WHERE idSeason = ?" + self.cursor.execute(query, (name, season_id)) return season_id @@ -189,18 +188,6 @@ class KodiTVShows(KodiItems): 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( @@ -213,16 +200,6 @@ class KodiTVShows(KodiItems): 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", diff --git a/resources/lib/objects/movies.py b/resources/lib/objects/movies.py index a469a103..77006ad1 100644 --- a/resources/lib/objects/movies.py +++ b/resources/lib/objects/movies.py @@ -250,28 +250,18 @@ class Movies(Items): if update_item: log.info("UPDATE movie itemid: %s - Title: %s", itemid, title) - # update new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.get_ratingid(movieid) + # update ratings + ratingid = self.kodi_db.get_ratingid(movieid) + self.kodi_db.update_ratings(movieid, "movie", "default", rating, votecount, ratingid) - self.kodi_db.update_ratings(movieid, "movie", "default", rating, votecount, ratingid) - - # update new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.get_uniqueid(movieid) - - self.kodi_db.update_uniqueid(movieid, "movie", imdb, "imdb", uniqueid) + # update uniqueid + uniqueid = self.kodi_db.get_uniqueid(movieid) + self.kodi_db.update_uniqueid(movieid, "movie", imdb, "imdb", uniqueid) # Update the movie entry - if self.kodi_version >= 17: - self.kodi_db.update_movie_17(title, plot, shortplot, tagline, votecount, uniqueid, - writer, year, uniqueid, 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) + self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, uniqueid, + writer, year, uniqueid, sorttitle, runtime, mpaa, genre, + director, title, studio, trailer, country, year, movieid) # Update the checksum in emby table emby_db.updateReference(itemid, checksum) @@ -280,17 +270,13 @@ class Movies(Items): else: log.info("ADD movie itemid: %s - Title: %s", itemid, title) - # add new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.create_entry_rating() + # Add ratings + ratingid = self.kodi_db.create_entry_rating() + self.kodi_db.add_ratings(ratingid, movieid, "movie", "default", rating, votecount) - self.kodi_db.add_ratings(ratingid, movieid, "movie", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, movieid, "movie", imdb, "imdb") + # Add uniqueid + uniqueid = self.kodi_db.create_entry_uniqueid() + self.kodi_db.add_uniqueid(uniqueid, movieid, "movie", imdb, "imdb") # Add path pathid = self.kodi_db.add_path(path) @@ -298,16 +284,10 @@ class Movies(Items): fileid = self.kodi_db.add_file(filename, pathid) # Create the movie entry - if self.kodi_version >= 17: - self.kodi_db.add_movie_17(movieid, fileid, title, plot, shortplot, tagline, - votecount, uniqueid, writer, year, uniqueid, 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) + self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline, + votecount, uniqueid, writer, year, uniqueid, sorttitle, + runtime, mpaa, genre, director, title, studio, trailer, + country, year) # Create the reference in emby table emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, diff --git a/resources/lib/objects/music.py b/resources/lib/objects/music.py index 05757b2d..9b8b3dc0 100644 --- a/resources/lib/objects/music.py +++ b/resources/lib/objects/music.py @@ -303,22 +303,8 @@ class Music(Items): emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum) # Process the album info - if self.kodi_version in [17,18]: - # Kodi Krypton/Leia - 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) + self.kodi_db.update_album(artistname, year, genre, bio, thumb, rating, lastScraped, + "album", albumid) # Assign main artists to album for artist in item['AlbumArtists']: diff --git a/resources/lib/objects/tvshows.py b/resources/lib/objects/tvshows.py index b8a79d16..9c614229 100644 --- a/resources/lib/objects/tvshows.py +++ b/resources/lib/objects/tvshows.py @@ -9,7 +9,7 @@ import api import embydb_functions as embydb import _kodi_tvshows from _common import Items, catch_except -from utils import window, settings, language as lang, plugin_path +from utils import window, settings, language as lang, urllib_path ################################################################################################## @@ -234,11 +234,11 @@ class TVShows(Items): artwork = self.artwork API = api.API(item) - # Server api changed or something? RecursiveItemCount always returns 0 - """if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): - if item.get('Name', None) is not None: - log.info("Skipping empty show: %s", item['Name']) - return""" + # If the show is empty, try to remove it. + if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): + log.info("Skipping empty show: %s", item.get('Name', item['Id'])) + return self.remove(item['Id']) + # 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 @@ -308,12 +308,8 @@ class TVShows(Items): if self.emby_db.get_view_grouped_series(viewid) and tvdb: # search kodi db for same provider id - if self.kodi_version > 16: - query = "SELECT idShow FROM tvshow_view WHERE uniqueid_value = ?" - kodicursor.execute(query, (tvdb,)) - else: - query = "SELECT idShow FROM tvshow WHERE C12 = ?" - kodicursor.execute(query, (tvdb,)) + query = "SELECT idShow FROM tvshow_view WHERE uniqueid_value = ?" + kodicursor.execute(query, (tvdb,)) try: temps_showid = kodicursor.fetchall() @@ -342,51 +338,22 @@ class TVShows(Items): force_episodes = True - # 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""" - - ##### UPDATE THE TVSHOW ##### if update_item: log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) - # update new ratings Kodi 17 - if self.kodi_version > 16: - ratingid = self.kodi_db.get_ratingid("tvshow", showid) + # update ratings + ratingid = self.kodi_db.get_ratingid("tvshow", showid) + self.kodi_db.update_ratings(showid, "tvshow", "default", rating, votecount,ratingid) - self.kodi_db.update_ratings(showid, "tvshow", "default", rating, votecount,ratingid) - - # update new uniqueid Kodi 17 - if self.kodi_version > 16: - uniqueid = self.kodi_db.get_uniqueid("tvshow", showid) - - self.kodi_db.update_uniqueid(showid, "tvshow", tvdb, "unknown", uniqueid) + # update uniqueid + uniqueid = self.kodi_db.get_uniqueid("tvshow", showid) + self.kodi_db.update_uniqueid(showid, "tvshow", tvdb, "unknown", uniqueid) # Update the tvshow entry - if self.kodi_version > 16: - self.kodi_db.update_tvshow(title, plot, uniqueid, premieredate, genre, title, - uniqueid, mpaa, studio, sorttitle, showid) - else: - self.kodi_db.update_tvshow(title, plot, rating, premieredate, genre, title, - tvdb, mpaa, studio, sorttitle, showid) + self.kodi_db.update_tvshow(title, plot, uniqueid, premieredate, genre, title, + uniqueid, mpaa, studio, sorttitle, showid) + # Update the checksum in emby table emby_db.updateReference(itemid, checksum) @@ -394,17 +361,13 @@ class TVShows(Items): else: log.info("ADD tvshow itemid: %s - Title: %s", itemid, title) - # add new ratings Kodi 17 - if self.kodi_version > 16: - ratingid = self.kodi_db.create_entry_rating() + # add ratings + ratingid = self.kodi_db.create_entry_rating() + self.kodi_db.add_ratings(ratingid, showid, "tvshow", "default", rating, votecount) - self.kodi_db.add_ratings(ratingid, showid, "tvshow", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version > 16: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, showid, "tvshow", tvdb, "unknown") + # add uniqueid + uniqueid = self.kodi_db.create_entry_uniqueid() + self.kodi_db.add_uniqueid(uniqueid, showid, "tvshow", tvdb, "unknown") # Add top path toppathid = self.kodi_db.add_path(toplevelpath) @@ -414,12 +377,8 @@ class TVShows(Items): pathid = self.kodi_db.add_path(path) # Create the tvshow entry - if self.kodi_version > 16: - self.kodi_db.add_tvshow(showid, title, plot, uniqueid, premieredate, genre, - title, uniqueid, mpaa, studio, sorttitle) - else: - self.kodi_db.add_tvshow(showid, title, plot, rating, premieredate, genre, - title, tvdb, mpaa, studio, sorttitle) + self.kodi_db.add_tvshow(showid, title, plot, uniqueid, premieredate, genre, + title, uniqueid, mpaa, studio, sorttitle) # Create the reference in emby table emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, @@ -629,39 +588,24 @@ class TVShows(Items): 'dbid': episodeid, 'mode': "play" } - filename = plugin_path(path, params) + filename = urllib_path(path, params) ##### UPDATE THE EPISODE ##### if update_item: log.info("UPDATE episode itemid: %s - Title: %s", itemid, title) - # update new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.get_ratingid("episode", episodeid) + # update ratings + ratingid = self.kodi_db.get_ratingid("episode", episodeid) + self.kodi_db.update_ratings(episodeid, "episode", "default", rating, votecount, ratingid) - self.kodi_db.update_ratings(episodeid, "episode", "default", rating, votecount,ratingid) - - # update new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.get_uniqueid("episode", episodeid) - - self.kodi_db.update_uniqueid(episodeid, "episode", tvdb, "tvdb",uniqueid) + # update uniqueid + uniqueid = self.kodi_db.get_uniqueid("episode", episodeid) + self.kodi_db.update_uniqueid(episodeid, "episode", tvdb, "tvdb", uniqueid) # Update the episode entry - if self.kodi_version >= 17: - # Kodi Krypton - self.kodi_db.update_episode_16(title, plot, uniqueid, writer, premieredate, runtime, - director, season, episode, title, airsBeforeSeason, - airsBeforeEpisode, seasonid, showid, episodeid) - elif self.kodi_version >= 16 and self.kodi_version < 17: - # Kodi Jarvis - 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) + self.kodi_db.update_episode(title, plot, uniqueid, writer, premieredate, runtime, + director, season, episode, title, airsBeforeSeason, + airsBeforeEpisode, seasonid, showid, episodeid) # Update the checksum in emby table emby_db.updateReference(itemid, checksum) @@ -672,17 +616,13 @@ class TVShows(Items): else: log.info("ADD episode itemid: %s - Title: %s", itemid, title) - # add new ratings Kodi 17 - if self.kodi_version >= 17: - ratingid = self.kodi_db.create_entry_rating() + # add ratings + ratingid = self.kodi_db.create_entry_rating() + self.kodi_db.add_ratings(ratingid, episodeid, "episode", "default", rating, votecount) - self.kodi_db.add_ratings(ratingid, episodeid, "episode", "default", rating, votecount) - - # add new uniqueid Kodi 17 - if self.kodi_version >= 17: - uniqueid = self.kodi_db.create_entry_uniqueid() - - self.kodi_db.add_uniqueid(uniqueid, episodeid, "episode", tvdb, "tvdb") + # add uniqueid + uniqueid = self.kodi_db.create_entry_uniqueid() + self.kodi_db.add_uniqueid(uniqueid, episodeid, "episode", tvdb, "tvdb") # Add path pathid = self.kodi_db.add_path(path) @@ -690,20 +630,9 @@ class TVShows(Items): fileid = self.kodi_db.add_file(filename, pathid) # Create the episode entry - if self.kodi_version >= 17: - # Kodi Krypton - self.kodi_db.add_episode_16(episodeid, fileid, title, plot, uniqueid, writer, - premieredate, runtime, director, season, episode, title, - showid, airsBeforeSeason, airsBeforeEpisode, seasonid) - elif self.kodi_version >= 16 and self.kodi_version < 17: - # Kodi Jarvis - 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) + self.kodi_db.add_episode(episodeid, fileid, title, plot, uniqueid, writer, + premieredate, runtime, director, season, episode, title, + showid, airsBeforeSeason, airsBeforeEpisode, seasonid) # Create the reference in emby table emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index aad56aa4..3611d8f3 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -21,6 +21,8 @@ import playutils as putils import playlist import read_embyserver as embyserver import shutil +import embydb_functions as embydb +from database import DatabaseConn from utils import window, settings, language as lang ################################################################################################# @@ -30,329 +32,147 @@ log = logging.getLogger("EMBY."+__name__) ################################################################################################# -class PlaybackUtils(): - - - def __init__(self, item): +class PlaybackUtils(object): - self.item = item - self.API = api.API(self.item) - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) + def __init__(self, item=None, item_id=None): self.artwork = artwork.Artwork() self.emby = embyserver.Read_EmbyServer() - self.pl = playlist.Playlist() + self.item = item or self.emby.getItem(item_id) + self.API = api.API(self.item) - def play(self, itemid, dbid=None): + self.server = window('emby_server%s' % window('emby_currUser')) + + self.stack = [] + + if self.item['Type'] == "Audio": + self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + else: + self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + def play(self, item_id, dbid=None, force_transcode=False): listitem = xbmcgui.ListItem() - playutils = putils.PlayUtils(self.item) - log.info("Play called.") - playurl = playutils.getPlayUrl() - if not playurl: + log.info("Play called: %s", self.item['Name']) + + resume = window('emby.resume') + window('emby.resume', clear=True) + + play_url = putils.PlayUtils(self.item, listitem).get_play_url(force_transcode) + + if not play_url: + if play_url == False: # User backed-out of menu + self.playlist.clear() return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - if dbid is None: - # Item is not in Kodi database - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) + seektime = 0 if resume == "true" else self.API.adjust_resume(self.API.get_userdata()['Resume']) - # TODO: Review once Krypton is RC, no need for workaround. + if force_transcode: + log.info("Clear the playlist.") + self.playlist.clear() - ############### ORGANIZE CURRENT PLAYLIST ################ - - homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - startPos = max(playlist.getposition(), 0) # Can return -1 - sizePlaylist = playlist.size() - currentPosition = startPos + self.set_playlist(play_url, item_id, listitem, seektime, dbid) - propertiesPlayback = window('emby_playbackProps') == "true" - introsPlaylist = False - dummyPlaylist = False + ##### SETUP PLAYBACK - log.debug("Playlist start position: %s" % startPos) - log.debug("Playlist plugin position: %s" % currentPosition) - log.debug("Playlist size: %s" % sizePlaylist) + ''' To get everything to work together, play the first item in the stack with setResolvedUrl, + add the rest to the regular playlist. + ''' - ############### RESUME POINT ################ - - userdata = self.API.get_userdata() - seektime = self.API.adjust_resume(userdata['Resume']) + index = max(self.playlist.getposition(), 0) + 1 # Can return -1 + force_play = False - # We need to ensure we add the intro and additional parts only once. - # Otherwise we get a loop. - if not propertiesPlayback: - - window('emby_playbackProps', value="true") - log.info("Setting up properties in playlist.") - - if not homeScreen and not seektime and window('emby_customPlaylist') != "true": - - log.debug("Adding dummy file to playlist.") - dummyPlaylist = True - playlist.add(playurl, listitem, index=startPos) - # Remove the original item from playlist - self.pl.remove_from_playlist(startPos+1) - # Readd the original item to playlist - via jsonrpc so we have full metadata - self.pl.insert_to_playlist(currentPosition+1, dbid, self.item['Type'].lower()) - currentPosition += 1 - - ############### -- CHECK FOR INTROS ################ - - if settings('enableCinema') == "true" and not seektime: - # if we have any play them when the movie/show is not being resumed - url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid - intros = self.doUtils(url) - - if intros['TotalRecordCount'] != 0: - getTrailers = True - - if settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016)) - if not resp: - # User selected to not play trailers - getTrailers = False - log.info("Skip trailers.") - - if getTrailers: - for intro in intros['Items']: - # The server randomly returns intros, process them. - introListItem = xbmcgui.ListItem() - introPlayurl = putils.PlayUtils(intro).getPlayUrl() - log.info("Adding Intro: %s" % introPlayurl) - - # Set listitem and properties for intros - pbutils = PlaybackUtils(intro) - pbutils.setProperties(introPlayurl, introListItem) - - self.pl.insert_to_playlist(currentPosition, url=introPlayurl) - introsPlaylist = True - currentPosition += 1 - - - ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### - - if homeScreen and not seektime and not sizePlaylist: - # 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.add_to_playlist(dbid, self.item['Type'].lower()) - - # Ensure that additional parts are played after the main item - currentPosition += 1 - - ############### -- CHECK FOR ADDITIONAL PARTS ################ - - if self.item.get('PartCount'): - # Only add to the playlist after intros have played - partcount = self.item['PartCount'] - url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid - parts = self.doUtils(url) - for part in parts['Items']: - - additionalListItem = xbmcgui.ListItem() - additionalPlayurl = putils.PlayUtils(part).getPlayUrl() - log.info("Adding additional part: %s" % partcount) - - # Set listitem and properties for each additional parts - pbutils = PlaybackUtils(part) - pbutils.setProperties(additionalPlayurl, additionalListItem) - pbutils.setArtwork(additionalListItem) - - playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) - self.pl.verify_playlist() - currentPosition += 1 - - if dummyPlaylist: - # Added a dummy file to the playlist, - # because the first item is going to fail automatically. - log.info("Processed as a playlist. First item is skipped.") - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) - - - # We just skipped adding properties. Reset flag for next time. - elif propertiesPlayback: - log.debug("Resetting properties playback flag.") - window('emby_playbackProps', clear=True) - - #self.pl.verify_playlist() - ########## SETUP MAIN ITEM ########## - - # For transcoding only, ask for audio/subs pref - if window('emby_%s.playmethod' % playurl) == "Transcode": - # Filter ISO since Emby does not probe anymore - if self.item.get('VideoType') == "Iso": - log.info("Skipping audio/subs prompt, ISO detected.") - else: - playurl = playutils.audioSubsPref(playurl, listitem) - window('emby_%s.playmethod' % playurl, value="Transcode") - - listitem.setPath(playurl) - self.setProperties(playurl, listitem) - - ############### PLAYBACK ################ - - if homeScreen and seektime and window('emby_customPlaylist') != "true": - log.info("Play as a widget item.") - self.setListItem(listitem, dbid) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - elif ((introsPlaylist and window('emby_customPlaylist') == "true") or - (homeScreen and not sizePlaylist)): - # Playlist was created just now, play it. - log.info("Play playlist.") - xbmc.Player().play(playlist, startpos=startPos) - - else: - log.info("Play as a regular item.") - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - - def setProperties(self, playurl, listitem): - - # Set all properties necessary for plugin path playback - itemid = self.item['Id'] - itemtype = self.item['Type'] - - embyitem = "emby_%s" % playurl - window('%s.runtime' % embyitem, value=str(self.item.get('RunTimeTicks'))) - window('%s.type' % embyitem, value=itemtype) - window('%s.itemid' % embyitem, value=itemid) - - if itemtype == "Episode": - window('%s.refreshid' % embyitem, value=self.item.get('SeriesId')) - else: - window('%s.refreshid' % embyitem, value=itemid) - - # Append external subtitles to stream - playmethod = window('%s.playmethod' % embyitem) - # Only for direct stream - if playmethod in ("DirectStream") and settings('enableExternalSubs') == "true": - # Direct play automatically appends external - subtitles = self.externalSubs(playurl) - listitem.setSubtitles(subtitles) - - self.setArtwork(listitem) - - def externalSubs(self, playurl): - - externalsubs = [] - mapping = {} - - itemid = self.item['Id'] + # Stack: [(url, listitem), (url, ...), ...] + self.stack[0][1].setPath(self.stack[0][0]) try: - mediastreams = self.item['MediaSources'][0]['MediaStreams'] - except (TypeError, KeyError, IndexError): - return + #if not xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'): # Causes infinite loop with play from here + if xbmc.getCondVisibility('Window.IsVisible(10000).xml'): + # widgets do not fill artwork correctly + log.info("Detected widget.") + raise IndexError - temp = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1]) + self.stack.pop(0) # remove the first item we just started. + except IndexError: + log.info("Playback activated via the context menu or widgets.") + force_play = True + self.stack[0][1].setProperty('StartOffset', str(seektime)) - kodiindex = 0 - for stream in mediastreams: + for stack in self.stack: + self.playlist.add(url=stack[0], listitem=stack[1], index=index) + index += 1 - index = stream['Index'] - # Since Emby returns all possible tracks together, have to pull only external subtitles. - # IsTextSubtitleStream if true, is available to download from emby. - if (stream['Type'] == "Subtitle" and - stream['IsExternal'] and stream['IsTextSubtitleStream']): + if force_play: + xbmc.Player().play(self.playlist) - # Direct stream - url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.%s" - % (self.server, itemid, itemid, index, stream['Codec'])) + def set_playlist(self, play_url, item_id, listitem, seektime=None, db_id=None): - if "Language" in stream: - - filename = "Stream.%s.%s" % (stream['Language'], stream['Codec']) - try: - path = self._download_external_subs(url, temp, filename) - externalsubs.append(path) - except Exception as e: - log.error(e) - externalsubs.append(url) - else: - externalsubs.append(url) - - # map external subtitles for mapping - mapping[kodiindex] = index - kodiindex += 1 - - mapping = json.dumps(mapping) - window('emby_%s.indexMapping' % playurl, value=mapping) + ##### CHECK FOR INTROS - return externalsubs + if settings('enableCinema') == "true" and not seektime: + self._set_intros(item_id) - def _download_external_subs(self, src, dst, filename): + ##### ADD MAIN ITEM - if not xbmcvfs.exists(dst): - xbmcvfs.mkdir(dst) + self.set_properties(play_url, listitem) + self.set_listitem(listitem, db_id) + self.stack.append([play_url, listitem]) - path = os.path.join(dst, filename) + ##### ADD ADDITIONAL PARTS - try: - response = requests.get(src, stream=True) - response.raise_for_status() - except Exception as e: - raise - else: - response.encoding = 'utf-8' - with open(path, 'wb') as f: - f.write(response.content) - del response + if self.item.get('PartCount'): + self._set_additional_parts(item_id) - return path + def _set_intros(self, item_id): + # if we have any play them when the movie/show is not being resumed + intros = self.emby.get_intros(item_id) - def setArtwork(self, listItem): - # Set up item and item info - allartwork = self.artwork.get_all_artwork(self.item, parent_info=True) - # Set artwork for listitem - arttypes = { + if intros['Items']: + enabled = True - 'poster': "Primary", - 'tvshow.poster': "Primary", - 'clearart': "Art", - 'tvshow.clearart': "Art", - 'clearlogo': "Logo", - 'tvshow.clearlogo': "Logo", - 'discart': "Disc", - 'fanart_image': "Backdrop", - 'landscape': "Thumb" - } - for arttype in arttypes: + if settings('askCinema') == "true": - art = arttypes[arttype] - if art == "Backdrop": - try: # Backdrop is a list, grab the first backdrop - self.setArtProp(listItem, arttype, allartwork[art][0]) - except: pass - else: - self.setArtProp(listItem, arttype, allartwork[art]) + resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016)) + if not resp: + # User selected to not play trailers + enabled = False + log.info("Skip trailers.") - def setArtProp(self, listItem, arttype, path): - - if arttype in ( - 'thumb', 'fanart_image', 'small_poster', 'tiny_poster', - 'medium_landscape', 'medium_poster', 'small_fanartimage', - 'medium_fanartimage', 'fanart_noindicators'): - - listItem.setProperty(arttype, path) - else: - listItem.setArt({arttype: path}) + if enabled: + for intro in intros['Items']: - def setListItem(self, listItem, dbid=None): + listitem = xbmcgui.ListItem() + url = putils.PlayUtils(intro, listitem).get_play_url() + log.info("Adding Intro: %s" % url) + + self.stack.append([url, listitem]) + + def _set_additional_parts(self, item_id): + + parts = self.emby.get_additional_parts(item_id) + + for part in parts['Items']: + + listitem = xbmcgui.ListItem() + url = putils.PlayUtils(part, listitem).get_play_url() + log.info("Adding additional part: %s" % url) + + # Set listitem and properties for each additional parts + pb = PlaybackUtils(part) + pb.set_properties(url, listitem) + pb.setArtwork(listitem) + + self.stack.append([url, listitem]) + + def set_listitem(self, listitem, dbid=None): people = self.API.get_people() - studios = self.API.get_studios() + mediatype = self.item['Type'] metadata = { - 'title': self.item.get('Name', "Missing name"), 'year': self.item.get('ProductionYear'), 'plot': self.API.get_overview(), @@ -360,33 +180,151 @@ class PlaybackUtils(): 'writer': people.get('Writer'), 'mpaa': self.API.get_mpaa(), 'genre': " / ".join(self.item['Genres']), - 'studio': " / ".join(studios), + 'studio': " / ".join(self.API.get_studios()), 'aired': self.API.get_premiere_date(), 'rating': self.item.get('CommunityRating'), 'votes': self.item.get('VoteCount') } - if "Episode" in self.item['Type']: + if mediatype == "Episode": # Only for tv shows - # For Kodi Krypton metadata['mediatype'] = "episode" - metadata['dbid'] = dbid + metadata['TVShowTitle'] = self.item.get('SeriesName', "") + metadata['season'] = self.item.get('ParentIndexNumber', -1) + metadata['episode'] = self.item.get('IndexNumber', -1) - thumbId = self.item.get('SeriesId') - season = self.item.get('ParentIndexNumber', -1) - episode = self.item.get('IndexNumber', -1) - show = self.item.get('SeriesName', "") - - metadata['TVShowTitle'] = show - metadata['season'] = season - metadata['episode'] = episode - - if "Movie" in self.item['Type']: - # For Kodi Krypton + elif mediatype == "Movie": metadata['mediatype'] = "movie" + + elif mediatype == "MusicVideo": + metadata['mediatype'] = "musicvideo" + + elif mediatype == "Audio": + metadata['mediatype'] = "song" + + if dbid: metadata['dbid'] = dbid - listItem.setProperty('IsPlayable', 'true') - listItem.setProperty('IsFolder', 'false') - listItem.setLabel(metadata['title']) - listItem.setInfo('video', infoLabels=metadata) \ No newline at end of file + listitem.setProperty('IsPlayable', 'true') + listitem.setProperty('IsFolder', 'false') + listitem.setLabel(metadata['title']) + listitem.setInfo('Music' if mediatype == "Audio" else 'Video', infoLabels=metadata) + + def set_properties(self, url, listitem): + + # Set all properties necessary for plugin path playback + + item_id = self.item['Id'] + item_type = self.item['Type'] + + play_method = window('emby_%s.playmethod' % url) + window('emby_%s.playmethod' % url, clear=True) + window('emby_%s.json' % url, { + + 'url': url, + 'runtime': str(self.item.get('RunTimeTicks')), + 'type': item_type, + 'id': item_id, + 'refreshid': self.item.get('SeriesId') if item_type == "Episode" else item_id, + 'playmethod': play_method + }) + + self.set_artwork(listitem, item_type) + listitem.setCast(self.API.get_actors()) + + def set_artwork(self, listitem, item_type): + + all_artwork = self.artwork.get_all_artwork(self.item, parent_info=True) + # Set artwork for listitem + if item_type == "Episode": + art = { + 'poster': "Series.Primary", + 'tvshow.poster': "Series.Primary", + 'clearart': "Art", + 'tvshow.clearart': "Art", + 'clearlogo': "Logo", + 'tvshow.clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'tvshow.landscape': "Thumb", + 'thumb': "Primary" + } + else: + art = { + 'poster': "Primary", + 'clearart': "Art", + 'clearlogo': "Logo", + 'discart': "Disc", + 'fanart_image': "Backdrop", + 'landscape': "Thumb", + 'thumb': "Primary" + } + + for k_art, e_art in art.items(): + + if e_art == "Backdrop" and all_artwork[e_art]: + self._set_art(listitem, k_art, all_artwork[e_art][0]) + else: + self._set_art(listitem, k_art, all_artwork.get(e_art)) + + def _set_art(self, listitem, art, path): + + if path: + if art in ('fanart_image', 'small_poster', 'tiny_poster', + 'medium_landscape', 'medium_poster', 'small_fanartimage', + 'medium_fanartimage', 'fanart_noindicators'): + + listitem.setProperty(art, path) + else: + listitem.setArt({art: path}) + + def play_all(self, item_ids, seektime=None, **kwargs): + + self.playlist.clear() + started = False + + for item_id in item_ids: + + listitem = xbmcgui.ListItem() + db_id = None + + item = self.emby.getItem(item_id) + play_url = putils.PlayUtils(item, listitem, **kwargs if item_ids.index(item_id) == 0 else {}).get_play_url() + + if not play_url: + log.info("Failed to retrieve playurl") + continue + + log.info("Playurl: %s", play_url) + + with DatabaseConn('emby') as cursor: + item_db = embydb.Embydb_Functions(cursor).getItem_byId(item_id) + db_id = item_db[0] if item_db else None + + pbutils = PlaybackUtils(item) + pbutils.set_playlist(play_url, item_id, listitem, seektime if item_ids.index(item_id) == 1 else None, db_id) + + if item_ids.index(item_id) == 1 and seektime: + log.info("Seektime detected: %s", self.API.adjust_resume(seektime)) + listitem.setProperty('StartOffset', str(self.API.adjust_resume(seektime))) + + + index = max(pbutils.playlist.getposition(), 0) + 1 # Can return -1 + for stack in pbutils.stack: + pbutils.playlist.add(url=stack[0], listitem=stack[1], index=index) + index += 1 + + if not started: + started = True + + item = window('emby_%s.json' % play_url) + item['forcedaudio'] = kwargs.get('AudioStreamIndex') + item['forcedsubs'] = kwargs.get('SubtitleStreamIndex') + window('emby_%s.json' % play_url, value=item) + + player = xbmc.Player() + player.play(pbutils.playlist) + + if started: + return True diff --git a/resources/lib/player.py b/resources/lib/player.py index 13945f4e..71af6671 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -12,7 +12,7 @@ import xbmcgui import clientinfo import downloadutils import websocket_client as wsc -from utils import window, settings, language as lang +from utils import window, settings, language as lang, JSONRPC from ga_client import GoogleAnalytics, log_error ################################################################################################# @@ -43,6 +43,39 @@ class Player(xbmc.Player): log.debug("Starting playback monitor.") xbmc.Player.__init__(self) + def set_audio_subs(self, audio_index=None, subs_index=None): + + ''' Only for after playback started + ''' + player = xbmc.Player() + log.info("Setting audio: %s subs: %s", audio_index, subs_index) + + if audio_index and len(player.getAvailableAudioStreams()) > 1: + player.setAudioStream(audio_index - 1) + + if subs_index: + mapping = window('emby_%s.indexMapping.json' % self.current_file) + + if subs_index == -1: + player.showSubtitles(False) + + elif mapping: + external_index = mapping + # If there's external subtitles added via playbackutils + for index in external_index: + if external_index[index] == subs_index: + player.setSubtitleStream(int(index)) + break + else: + # User selected internal subtitles + external = len(external_index) + audio_tracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(external + subs_index - audio_tracks - 1) + else: + # Emby merges audio and subtitle index together + audio_tracks = len(player.getAvailableAudioStreams()) + player.setSubtitleStream(subs_index - audio_tracks - 1) + @log_error() def onPlayBackStarted(self): # Will be called when xbmc starts playing a file @@ -74,28 +107,31 @@ class Player(xbmc.Player): self.currentFile = currentFile # We may need to wait for info to be set in kodi monitor - itemId = window("emby_%s.itemid" % currentFile) + item = window('emby_%s.json' % currentFile) + #itemId = window("emby_%s.itemid" % currentFile) tryCount = 0 - while not itemId: + while not item: xbmc.sleep(200) - itemId = window("emby_%s.itemid" % currentFile) + item = window('emby_%s.json' % currentFile) if tryCount == 20: # try 20 times or about 10 seconds - log.info("Could not find itemId, cancelling playback report...") + log.info("Could not find item, cancelling playback report...") break else: tryCount += 1 else: - log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId)) + item_id = item.get('id') + log.info("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, item_id)) # Only proceed if an itemId was found. - embyitem = "emby_%s" % currentFile - runtime = window("%s.runtime" % embyitem) - refresh_id = window("%s.refreshid" % embyitem) - playMethod = window("%s.playmethod" % embyitem) - itemType = window("%s.type" % embyitem) - window('emby_skipWatched%s' % itemId, value="true") + runtime = item.get('runtime') + refresh_id = item.get('refreshid') + play_method = item.get('playmethod') + item_type = item.get('type') + #self.set_audio_subs(item.get('forcedaudio'), item.get('forcedsubs')) + + window('emby_skipWatched%s' % item_id, value="true") customseek = window('emby_customPlaylist.seektime') if window('emby_customPlaylist') == "true" and customseek: # Start at, when using custom playlist (play to Kodi from webclient) @@ -110,18 +146,7 @@ class Player(xbmc.Player): return # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) result = result.get('result') volume = result.get('volume') @@ -133,33 +158,26 @@ class Player(xbmc.Player): 'QueueableMediaTypes': "Video", 'CanSeek': True, - 'ItemId': itemId, - 'MediaSourceId': itemId, - 'PlayMethod': playMethod, + 'ItemId': item_id, + 'MediaSourceId': item_id, + 'PlayMethod': play_method, 'VolumeLevel': volume, 'PositionTicks': int(seekTime * 10000000), 'IsMuted': muted } # Get the current audio track and subtitles - if playMethod == "Transcode": + if play_method == "Transcode": # property set in PlayUtils.py postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) else: # Get the current kodi audio and subtitles and convert to Emby equivalent - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } + params = { + 'playerid': 1, + 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"] } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) + result = JSONRPC('Player.GetProperties').execute(params) tracks_data = None try: tracks_data = json.loads(result) @@ -190,7 +208,7 @@ class Player(xbmc.Player): # Number of audiotracks to help get Emby Index audioTracks = len(xbmc.Player().getAvailableAudioStreams()) - mapping = window("%s.indexMapping" % embyitem) + mapping = window("emby_%s.indexMapping" % currentFile) if mapping: # Set in playbackutils.py @@ -230,13 +248,13 @@ class Player(xbmc.Player): data = { 'runtime': runtime, - 'item_id': itemId, + 'item_id': item_id, 'refresh_id': refresh_id, 'currentfile': currentFile, 'AudioStreamIndex': postdata['AudioStreamIndex'], 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], - 'playmethod': playMethod, - 'Type': itemType, + 'playmethod': play_method, + 'Type': item_type, 'currentPosition': int(seekTime) } @@ -244,8 +262,8 @@ class Player(xbmc.Player): log.info("ADDING_FILE: %s" % self.played_info) ga = GoogleAnalytics() - ga.sendEventData("PlayAction", itemType, playMethod) - ga.sendScreenView(itemType) + ga.sendEventData("PlayAction", item_type, play_method) + ga.sendScreenView(item_type) def reportPlayback(self): @@ -267,18 +285,7 @@ class Player(xbmc.Player): # Get playback volume - volume_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Application.GetProperties", - "params": { - - "properties": ["volume", "muted"] - } - } - result = xbmc.executeJSONRPC(json.dumps(volume_query)) - result = json.loads(result) + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) result = result.get('result') volume = result.get('volume') @@ -305,19 +312,11 @@ class Player(xbmc.Player): else: # Get current audio and subtitles track - tracks_query = { - - "jsonrpc": "2.0", - "id": 1, - "method": "Player.GetProperties", - "params": { - - "playerid": 1, - "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - } - result = xbmc.executeJSONRPC(json.dumps(tracks_query)) - result = json.loads(result) + params = { + 'playerid': 1, + 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"] + } + result = JSONRPC('Player.GetProperties').execute(params) result = result.get('result') try: # Audio tracks @@ -413,10 +412,15 @@ class Player(xbmc.Player): def onPlayBackStopped(self): # Will be called when user stops xbmc playing a file log.debug("ONPLAYBACK_STOPPED") - window('emby_customPlaylist', clear=True) window('emby_customPlaylist.seektime', clear=True) - window('emby_playbackProps', clear=True) - log.info("Clear playlist properties.") + + if self.currentFile in self.played_info: + log.info("Clear playlist.") + if self.played_info[self.currentFile]['Type'] == "Audio": + xbmc.PlayList(xbmc.PLAYLIST_MUSIC).clear() + else: + xbmc.PlayList(xbmc.PLAYLIST_VIDEO).clear() + self.stopAll() @log_error() @@ -456,10 +460,16 @@ class Player(xbmc.Player): if currentPosition and runtime: try: + if window('emby.external'): + window('emby.external', clear=True) + raise ValueError + percentComplete = (currentPosition * 10000000) / int(runtime) except ZeroDivisionError: # Runtime is 0. percentComplete = 0 + except ValueError: + percentComplete = 100 markPlayedAt = float(settings('markPlayed')) / 100 log.info("Percent complete: %s Mark played at: %s" @@ -486,6 +496,8 @@ class Player(xbmc.Player): else: log.info("User skipped deletion.") + window('emby.external_check', clear=True) + # Stop transcoding if playMethod == "Transcode": log.info("Transcoding for %s terminated." % itemid) diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index e1abccc5..1c47bac2 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -145,7 +145,7 @@ class Playlist(object): @classmethod def verify_playlist(cls): - log.debug(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) + log.info(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) @classmethod def remove_from_playlist(cls, position): @@ -156,3 +156,171 @@ class Playlist(object): 'position': position } log.debug(JSONRPC('Playlist.Remove').execute(params)) + + + + +''' +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging + +import xbmc +import xbmcgui + +import playutils +import playbackutils +import embydb_functions as embydb +import read_embyserver as embyserver +from utils import window, JSONRPC +from database import DatabaseConn + +################################################################################################# + +log = logging.getLogger("EMBY."+__name__) + +################################################################################################# + + +class Playlist(object): + + + def __init__(self): + + self.emby = embyserver.Read_EmbyServer() + self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + + def play_all(self, item_ids, start_at, **kwargs): + + with DatabaseConn('emby') as 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", item_ids, start_at) + + started = False + window('emby_customplaylist', value="true") + + if start_at: + # Seek to the starting position + window('emby_customplaylist.seektime', str(start_at)) + + for item_id in item_ids: + + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) + try: + 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(item_id) + self.add_to_xbmc_playlist(playlist, item, **kwargs) + + else: # Add to playlist + #self.add_to_playlist(db_id, media_type) + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item, db_id, **kwargs) + + if not started: + started = True + player.play(playlist) + + self.verify_playlist() + + def modify_playlist(self, item_ids): + + with DatabaseConn('emby') as cursor: + emby_db = embydb.Embydb_Functions(cursor) + + log.info("---*** ADD TO PLAYLIST ***---") + log.info("Items: %s", item_ids) + + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + + for item_id in item_ids: + + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) + try: + db_id = item[0] + media_type = item[4] + + except TypeError: + # Item is not found in our database, add item manually + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item) + + else: # Add to playlist + self.add_to_playlist(db_id, media_type) + + self.verify_playlist() + + return playlist + + @classmethod + def add_to_xbmc_playlist(cls, playlist, item, db_id, **kwargs): + + listitem = xbmcgui.ListItem() + + play_url = playutils.PlayUtils(item, listitem, **kwargs).get_play_url() + if not play_url: + log.info("Failed to retrieve playurl") + return + + log.info("Playurl: %s", play_url) + + #playbackutils.PlaybackUtils(item).set_playlist(play_url, item['Id'], listitem, seektime=None, db_id): + #playbackutils.PlaybackUtils(item).set_properties(playurl, listitem) + #playlist.add(playurl, listitem) + + @classmethod + def add_to_playlist(cls, db_id, media_type): + + params = { + + 'playlistid': 1, + 'item': { + '%sid' % media_type: int(db_id) + } + } + log.info(JSONRPC('Playlist.Add').execute(params)) + + @classmethod + def insert_to_playlist(cls, position, db_id=None, media_type=None, url=None): + + params = { + + 'playlistid': 1, + 'position': position + } + if db_id is not None: + params['item'] = {'%sid' % media_type: int(db_id)} + else: + params['item'] = {'file': url} + + log.debug(JSONRPC('Playlist.Insert').execute(params)) + + @classmethod + def verify_playlist(cls): + log.info(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) + + @classmethod + def remove_from_playlist(cls, position): + + params = { + + 'playlistid': 1, + 'position': position + } + log.debug(JSONRPC('Playlist.Remove').execute(params)) +''' \ No newline at end of file diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index ba451ba9..ff33c14b 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -2,8 +2,8 @@ ################################################################################################# +import collections import logging -import sys import urllib import xbmc @@ -13,7 +13,7 @@ import xbmcvfs import clientinfo import downloadutils import read_embyserver as embyserver -from utils import window, settings, language as lang +from utils import window, settings, language as lang, urllib_path ################################################################################################# @@ -24,338 +24,405 @@ log = logging.getLogger("EMBY."+__name__) class PlayUtils(): - - def __init__(self, item): + method = "DirectPlay" + force_transcode = False + + + def __init__(self, item, listitem, **kwargs): + + self.info = kwargs self.item = item + self.listitem = listitem + self.clientInfo = clientinfo.ClientInfo() self.emby = embyserver.Read_EmbyServer() - self.userid = window('emby_currUser') - self.server = window('emby_server%s' % self.userid) - - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - def getPlayUrlNew(self): + self.server = window('emby_server%s' % window('emby_currUser')) + + def get_play_url(self, force_transcode=False): + + ''' 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 ''' - 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" + + self.force_transcode = force_transcode + info = self.get_playback_info() + play_url = False if info == False else None + + if info: + play_url = info['Path'].encode('utf-8') + window('emby_%s.playmethod' % play_url, value=self.method) + + if self.method == "DirectPlay": + # Log filename, used by other addons eg subtitles which require the file name + window('embyfilename', value=play_url) + + log.info("playback info: %s", info) + log.info("play method: %s play url: %s", self.method, play_url) + + return play_url + + def get_playback_info(self): + + # Get the playback info for the current item + + info = self.emby.get_playback_info(self.item['Id'], self.get_device_profile()) + media_sources = info['MediaSources'] + + # Select the mediasource + if not media_sources: + log.error('No media sources found: %s', info) + return + + selected_source = media_sources[0] + + if self.info.get('MediaSourceId'): + for source in media_sources: + if source['Id'] == self.info['MediaSourceId']: + selected_source = source + break + + elif len(media_sources) > 1: + # Offer choices + sources = [] + for source in media_sources: + sources.append(source.get('Name', "na")) + + resp = xbmcgui.Dialog().select("Select the source", sources) + if resp > -1: + selected_source = media_sources[resp] 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): - - # log filename, used by other addons eg subtitles which require the file name - try: - window('embyfilename', value=self.directPlay()) - except: - log.info("Could not get file path for embyfilename window prop") - - playurl = None - user_token = downloadutils.DownloadUtils().get_token() - - if (self.item.get('Type') in ("Recording", "TvChannel") and self.item.get('MediaSources') - and self.item['MediaSources'][0]['Protocol'] == "Http"): - # 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']) - playurl += "&api_key=" + str(user_token) - 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 - log.info("File protocol is http.") - playurl = self.httpPlay() - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isDirectPlay(): - - log.info("File is direct playing.") - playurl = self.directPlay() - playurl = playurl.encode('utf-8') - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectPlay") - - elif self.isDirectStream(): - - log.info("File is direct streaming.") - playurl = self.directStream() - playurl = playurl.encode('utf-8') - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="DirectStream") - - elif self.isTranscoding(): - - log.info("File is transcoding.") - playurl = self.transcoding() - # Set playmethod property - window('emby_%s.playmethod' % playurl, value="Transcode") - - return playurl - - def httpPlay(self): - # Audio, Video, Photo - - itemid = self.item['Id'] - mediatype = self.item['MediaType'] - - if mediatype == "Audio": - playurl = "%s/emby/Audio/%s/stream?" % (self.server, itemid) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid) - - user_token = downloadutils.DownloadUtils().get_token() - playurl += "&api_key=" + str(user_token) - return playurl - - def isDirectPlay(self): - - # Requirement: Filesystem, Accessible path - if settings('playFromStream') == "true": - # User forcing to play via HTTP - log.info("Can't direct play, play from HTTP enabled.") - return False - - videotrack = self.item['MediaSources'][0]['Name'] - transcodeH265 = settings('transcodeH265') - videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x] - transcodeHi10P = settings('transcodeHi10P') - - if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles: - return False - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - try: - resolution = int(videotrack.split("P", 1)[0]) - except ValueError: # 4k resolution - resolution = 3064 - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log.info("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265])) - if res[transcodeH265] <= resolution: + log.info("No media source selected.") return False - canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay'] - # Make sure direct play is supported by the server - if not canDirectPlay: - log.info("Can't direct play, server doesn't allow/support it.") - return False + return self.get_optimal_source(selected_source) - # Verify screen resolution - if self.resolutionConflict(): - log.info("Can't direct play, resolution limit is enabled") - return False + def get_optimal_source(self, source): - location = self.item['LocationType'] - if location == "FileSystem": - # Verify the path - if not self.fileExists(): - log.info("Unable to direct play.") - log.info(self.directPlay()) - xbmcgui.Dialog().ok( - heading=lang(29999), - line1=lang(33011), - line2=(self.directPlay())) - sys.exit() + ''' 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. + ''' - return True + if (not self.force_transcode and self.is_h265(source) or self.is_strm(source) or + (source['SupportsDirectPlay'] and settings('playFromStream') == "false" and self.is_file_exists(source))): + # Do nothing, path is updated with our verification if applies. + pass + else: + source['Path'] = self.get_http_path(source, self.force_transcode or source['SupportsDirectStream'] == False) - def directPlay(self): + log.debug('get source: %s', source) + return source - try: - playurl = self.item['MediaSources'][0]['Path'] - except (IndexError, KeyError): - playurl = self.item['Path'] + def is_file_exists(self, source): - 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 + path = self.get_direct_path(source) + + if xbmcvfs.exists(path) or ":" not in path: + log.info("Path exists or assumed linux or web.") + + self.method = "DirectPlay" + source['Path'] = path + + return True + + log.info("Failed to find file.") + return False + + def is_strm(self, source): + + if source['Container'] == "strm": + log.info('Strm detected.') + + self.method = "DirectPlay" + source['Path'] = self.get_direct_path(source) + + return True + + return False + + def is_h265(self, source): + + if source['MediaStreams']: + force_transcode = False + + for stream in source['MediaStreams']: + if self._is_h265(stream) or self._is_high10(stream): + force_transcode = True + break + + if force_transcode: + source['Path'] = self.get_http_path(source, True) + return True + + return False + + @classmethod + def _is_h265(cls, stream): + + if stream['Type'] == "Video" and stream['Codec'] in ("hevc", "h265"): + if settings('transcode_h265') == "true": + log.info("Force transcode h265/hevc detected.") + return True + + return False + + @classmethod + def _is_high10(cls, stream): + + if stream.get('Profile') == "High 10": + if settings('transcodeHi10P') == "true": + log.info("Force transcode hi10p detected.") + return True + + return False + + def get_direct_path(self, source): + + path = source['Path'] + + if 'VideoType' in source: + if source['VideoType'] == "Dvd": + path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path + elif source['VideoType'] == "BluRay": + path = "%s/BDMV/index.bdmv" % path # Assign network protocol - if playurl.startswith('\\\\'): - playurl = playurl.replace("\\\\", "smb://") - playurl = playurl.replace("\\", "/") + if path.startswith('\\\\'): + path = path.replace('\\\\', "smb://") + path = path.replace('\\', "/") - if "apple.com" in playurl: - USER_AGENT = "QuickTime/7.7.4" - playurl += "?|User-Agent=%s" % USER_AGENT + return path - # Strm - if playurl.endswith('.strm'): - playurl = urllib.urlencode(playurl) + def get_http_path(self, source, transcode=False): + + if transcode and settings('ignoreTranscode') and source['MediaStreams']: + # Specified by user should not be transcoded. + ignore_codecs = settings('ignoreTranscode').split(',') - return playurl - - def fileExists(self): - - if 'Path' not in self.item: - # File has no path defined in server - return False - - # Convert path to direct play - path = self.directPlay() - log.info("Verifying path: %s" % path) - - if xbmcvfs.exists(path): - log.info("Path exists.") - return True - - elif ":" not in path: - log.info("Can't verify path, assumed linux. Still try to direct play.") - return True - - else: - log.info("Failed to find file.") - return False - - def isDirectStream(self): - - videotrack = self.item['MediaSources'][0]['Name'] - transcodeH265 = settings('transcodeH265') - videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x] - transcodeHi10P = settings('transcodeHi10P') - - if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles: - return False - - if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack): - # Avoid H265/HEVC depending on the resolution - try: - resolution = int(videotrack.split("P", 1)[0]) - except ValueError: # 4k resolution - resolution = 3064 - res = { - - '1': 480, - '2': 720, - '3': 1080 - } - log.info("Resolution is: %sP, transcode for resolution: %sP+" - % (resolution, res[transcodeH265])) - if res[transcodeH265] <= resolution: - return False - - # Requirement: BitRate, supported encoding - canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream'] - # Make sure the server supports it - if not canDirectStream: - return False - - # Verify the bitrate - if not self.isNetworkSufficient(): - log.info("The network speed is insufficient to direct stream file.") - return False - - # Verify screen resolution - if self.resolutionConflict(): - log.info("Can't direct stream, resolution limit is enabled") - return False - - return True - - def directStream(self): - - if 'Path' in self.item and self.item['Path'].endswith('.strm'): - # Allow strm loading when direct streaming - playurl = self.directPlay() - elif self.item['Type'] == "Audio": - playurl = "%s/emby/Audio/%s/stream.mp3?" % (self.server, self.item['Id']) - else: - playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id']) + for stream in source['MediaStreams']: + if stream['Type'] == "Video" and stream['Codec'] in ignore_codecs: + log.info("Ignoring transcode for: %s", stream['Codec']) + transcode = False + break + play_url = self.get_transcode_url(source) if transcode else self.get_direct_url(source) + user_token = downloadutils.DownloadUtils().get_token() - playurl += "&api_key=" + str(user_token) - return playurl + play_url += "&api_key=" + user_token + + return play_url - def isNetworkSufficient(self): + def get_direct_url(self, source): - settings = self.getBitrate()*1000 + self.method = "DirectStream" + + if self.item['Type'] == "Audio": + play_url = "%s/emby/Audio/%s/stream.%s?static=true" % (self.server, self.item['Id'], self.item['MediaSources'][0]['Container']) + else: + play_url = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id']) + + # Append external subtitles + if settings('enableExternalSubs') == "true": + self.set_external_subs(source, play_url) + + return play_url + + def get_transcode_url(self, source): + + self.method = "Transcode" + + item_id = self.item['Id'] + play_url = urllib_path("%s/emby/Videos/%s/master.m3u8" % (self.server, item_id), { + 'MediaSourceId': source['Id'], + 'VideoCodec': "h264", + 'AudioCodec': "ac3", + 'MaxAudioChannels': 6, + 'deviceId': self.clientInfo.get_device_id(), + 'VideoBitrate': self.get_bitrate() * 1000 + }) + + # Limit to 8 bit if user selected transcode Hi10P + if settings('transcodeHi10P') == "true": + play_url += "&MaxVideoBitDepth=8" + + # Adjust the video resolution + play_url += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution()) + # Select audio and subtitles + play_url += self.get_audio_subs(source) + + return play_url + + def set_external_subs(self, source, play_url): + + subs = [] + mapping = {} + + item_id = self.item['Id'] + streams = source['MediaStreams'] + + if not source['MediaStreams']: + log.info("No media streams found.") + return + + temp = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8') + + ''' Since Emby returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + ''' + + kodi_index = 0 + for stream in streams: + + if stream['Type'] == "Subtitle" and stream['IsExternal'] and stream['IsTextSubtitleStream']: + index = stream['Index'] + + url = self.server + stream['DeliveryUrl'] + + if 'Language' in stream: + filename = "Stream.%s.%s" % (stream['Language'].encode('utf-8'), stream['Codec']) + try: + subs.append(self._download_external_subs(url, temp, filename)) + except Exception as error: + log.warn(error) + subs.append(url) + else: + subs.append(url) + + # Map external subtitles for player.py + mapping[kodi_index] = index + kodi_index += 1 + + window('emby_%s.indexMapping.json' % play_url, value=mapping) + self.listitem.setSubtitles(subs) + + return + + @classmethod + def _download_external_subs(cls, src, dst, filename): + + if not xbmcvfs.exists(dst): + xbmcvfs.mkdir(dst) + + path = os.path.join(dst, filename) try: - sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) - except (KeyError, TypeError): - log.info("Bitrate value is missing.") + response = requests.get(src, stream=True) + response.raise_for_status() + except Exception as e: + raise else: - log.info("The add-on settings bitrate is: %s, the video bitrate required is: %s" - % (settings, sourceBitrate)) - if settings < sourceBitrate: - return False + response.encoding = 'utf-8' + with open(path, 'wb') as f: + f.write(response.content) + del response - return True + return path - def isTranscoding(self): - # Make sure the server supports it - if not self.item['MediaSources'][0]['SupportsTranscoding']: - return False + def get_audio_subs(self, source): - return True + ''' For transcoding only + Present the list of audio/subs to select from, before playback starts. + Returns part of the url to append. + ''' - def transcoding(self): + prefs = "" + streams = source['MediaStreams'] - if 'Path' in self.item and self.item['Path'].endswith('.strm'): - # Allow strm loading when transcoding - playurl = self.directPlay() - else: - itemid = self.item['Id'] - deviceId = self.clientInfo.get_device_id() - playurl = ( - "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" - % (self.server, itemid, itemid) - ) - playurl = ( - "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" - % (playurl, deviceId, self.getBitrate()*1000)) + audio_streams = collections.OrderedDict() + subs_streams = collections.OrderedDict() - # Limit to 8 bit if user selected transcode Hi10P - transcodeHi10P = settings('transcodeHi10P') - if transcodeHi10P == "true": - playurl = "%s&MaxVideoBitDepth=8" % playurl + if streams: - # Adjust video resolution - if self.resolutionConflict(): - screenRes = self.getScreenResolution() - playurl = "%s&maxWidth=%s&maxHeight=%s" % (playurl, screenRes['width'], screenRes['height']) + ''' Since Emby returns all possible tracks together, sort them. + IsTextSubtitleStream if true, is available to download from server. + ''' - user_token = downloadutils.DownloadUtils().get_token() - playurl += "&api_key=" + str(user_token) + for stream in streams: + index = stream['Index'] + stream_type = stream['Type'] - return playurl + if stream_type == "Audio": + codec = stream['Codec'] + channel = stream.get('ChannelLayout', "") - def getBitrate(self): + if 'Language' in stream: + track = "%s - %s - %s %s" % (index, stream['Language'], codec, channel) + else: + track = "%s - %s %s" % (index, codec, channel) + + audio_streams[track] = index + + elif stream_type == "Subtitle": + + if 'Language' in stream: + track = "%s - %s" % (index, stream['Language']) + else: + track = "%s - %s" % (index, stream['Codec']) + + if stream['IsDefault']: + track = "%s - Default" % track + if stream['IsForced']: + track = "%s - Forced" % track + + subs_streams[track] = index + + dialog = xbmcgui.Dialog() + skip_dialog = int(settings('skipDialogTranscode') or 0) + audio_selected = None + + if self.info.get('AudioStreamIndex'): + audio_selected = self.info['AudioStreamIndex'] + elif skip_dialog in (0, 1): + if len(audio_streams) > 1: + selection = list(audio_streams.keys()) + resp = dialog.select(lang(33013), selection) + audio_selected = audio_streams[selection[resp]] if resp else source['DefaultAudioStreamIndex'] + else: # Only one choice + audio_selected = audio_streams[next(iter(audio_streams))] + else: + audio_selected = source['DefaultAudioStreamIndex'] + + prefs += "&AudioStreamIndex=%s" % audio_selected + prefs += "&AudioBitrate=384000" if streams[audio_selected].get('Channels', 0) > 2 else "&AudioBitrate=192000" + + if self.info.get('SubtitleStreamIndex'): + index = self.info['SubtitleStreamIndex'] + + if index: + server_settings = self.emby.get_server_transcoding_settings() + if server_settings['EnableSubtitleExtraction'] and streams[index]['SupportsExternalStream']: + self._get_subtitles(source, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + elif (skip_dialog in (0, 2) and len(subs_streams) > 1): + selection = list(['No subtitles']) + list(subs_streams.keys()) + resp = dialog.select(lang(33014), selection) + if resp: + index = subs_streams[selection[resp]] if resp > -1 else sources.get('DefaultSubtitleStreamIndex') + if index is not None: + server_settings = self.emby.get_server_transcoding_settings() + if server_settings['EnableSubtitleExtraction'] and streams[index]['SupportsExternalStream']: + self._get_subtitles(source, index) + else: + prefs += "&SubtitleStreamIndex=%s" % index + + return prefs + + def _get_subtitles(self, source, index): + + url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" + % (self.server, self.item['Id'], source['Id'], index))] + + log.info("Set up subtitles: %s %s", index, url) + self.listitem.setSubtitles(url) + + def get_bitrate(self): # get the addon video quality + bitrate = { '0': 664, @@ -381,327 +448,117 @@ class PlayUtils(): '17': 100000, '18': 1000000 } - # max bit rate supported by server (max signed 32bit integer) return bitrate.get(settings('videoBitrate'), 2147483) - - def audioSubsPref(self, url, listitem): - - dialog = xbmcgui.Dialog() - # For transcoding only - # Present the list of audio to select from - audioStreamsList = {} - audioStreams = [] - audioStreamsChannelsList = {} - subtitleStreamsList = {} - subtitleStreams = ['No subtitles'] - downloadableStreams = [] - selectAudioIndex = "" - selectSubsIndex = "" - playurlprefs = "%s" % url - - try: - mediasources = self.item['MediaSources'][0] - mediastreams = mediasources['MediaStreams'] - except (TypeError, KeyError, IndexError): - return - - for stream in mediastreams: - # Since Emby returns all possible tracks together, have to sort them. - index = stream['Index'] - - if 'Audio' in stream['Type']: - codec = stream['Codec'] - channelLayout = stream.get('ChannelLayout', "") - - try: - track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) - except: - track = "%s - %s %s" % (index, codec, channelLayout) - - audioStreamsChannelsList[index] = stream['Channels'] - audioStreamsList[track] = index - audioStreams.append(track) - - elif 'Subtitle' in stream['Type']: - try: - track = "%s - %s" % (index, stream['Language']) - except: - track = "%s - %s" % (index, stream['Codec']) - - default = stream['IsDefault'] - forced = stream['IsForced'] - downloadable = stream['SupportsExternalStream'] - - if default: - track = "%s - Default" % track - if forced: - track = "%s - Forced" % track - if downloadable: - downloadableStreams.append(index) - - subtitleStreamsList[track] = index - subtitleStreams.append(track) - - - if len(audioStreams) > 1: - resp = dialog.select(lang(33013), audioStreams) - if resp > -1: - # User selected audio - selected = audioStreams[resp] - selectAudioIndex = audioStreamsList[selected] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - else: # User backed out of selection - playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] - else: # There's only one audiotrack. - selectAudioIndex = audioStreamsList[audioStreams[0]] - playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex - - if len(subtitleStreams) > 1: - resp = dialog.select(lang(33014), subtitleStreams) - if resp == 0: - # User selected no subtitles - pass - elif resp > -1: - # User selected subtitles - selected = subtitleStreams[resp] - selectSubsIndex = subtitleStreamsList[selected] - settings = self.emby.get_server_transcoding_settings() - - # Load subtitles in the listitem if downloadable - if settings['EnableSubtitleExtraction'] and selectSubsIndex in downloadableStreams: - - itemid = self.item['Id'] - url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" - % (self.server, itemid, itemid, selectSubsIndex))] - log.info("Set up subtitles: %s %s" % (selectSubsIndex, url)) - listitem.setSubtitles(url) - else: - # Burn subtitles - playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex - - else: # User backed out of selection - playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") - - # Get number of channels for selected audio track - audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) - if audioChannels > 2: - playurlprefs += "&AudioBitrate=384000" - else: - 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): + def get_device_profile(self): return { + "Name": "Kodi", - "MaxStreamingBitrate": self.getBitrate()*1000, + "MaxStreamingBitrate": self.get_bitrate() * 1000, "MusicStreamingTranscodingBitrate": 1280000, "TimelineOffsetSeconds": 5, - + "Identification": { - "ModelName": "Kodi", - "Headers": [ - { - "Name": "User-Agent", - "Value": "Kodi", - "Match": 2 - } - ] + "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 - } + { + "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 - } + { + "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": "" - } + { + "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": "" + } ] } - def resolutionConflict(self): - if settings('limitResolution') == "true": - screenRes = self.getScreenResolution() - videoRes = self.getVideoResolution() - - if not videoRes: - return False - - return videoRes['width'] > screenRes['width'] or videoRes['height'] > screenRes['height'] - else: - return False - - def getScreenResolution(self): - wind = xbmcgui.Window() - return {'width' : wind.getWidth(), - 'height' : wind.getHeight()} - - def getVideoResolution(self): - try: - return {'width' : self.item['MediaStreams'][0]['Width'], - 'height' : self.item['MediaStreams'][0]['Height']} - except (KeyError, IndexError) as error: - log.debug(error) - log.debug(self.item) - return False + def get_resolution(self): + window = xbmcgui.Window() + return window.getWidth(), window.getHeight() diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index 1474f287..3b7ada3e 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -151,7 +151,7 @@ class Read_EmbyServer(): "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" + "MediaSources,VoteCount,ItemCounts" ) } queue.put({'url': url, 'params': params}) @@ -182,7 +182,7 @@ class Read_EmbyServer(): "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,ItemCounts" ) } return self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) @@ -198,7 +198,7 @@ class Read_EmbyServer(): "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,ItemCounts" ) } url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" @@ -219,7 +219,7 @@ class Read_EmbyServer(): "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" + "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,ItemCounts" ) } url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" @@ -285,7 +285,7 @@ class Read_EmbyServer(): "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" + "MediaSources,VoteCount,ItemCounts" ) queue.put({'url': url, 'params': params}) if not self._add_worker_thread(queue, items['Items']): @@ -472,7 +472,7 @@ class Read_EmbyServer(): "Etag,Genres,SortName,Studios,Writer,ProductionYear," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," - "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" + "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview,ItemCounts" ) } queue.put({'url': url, 'params': params}) @@ -614,7 +614,7 @@ class Read_EmbyServer(): "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" + "MediaSources,VoteCount,ItemCounts" ) }) return params @@ -649,6 +649,39 @@ class Read_EmbyServer(): return library['LibraryOptions'] def get_server_transcoding_settings(self): + return self.doUtils.downloadUrl(self.get_emby_url('System/Configuration/encoding')) - url = self.get_emby_url('/System/Configuration/encoding') - return self.doUtils.downloadUrl(url) + def get_intros(self, item_id): + return self.doUtils.downloadUrl(self.get_emby_url('Users/{UserId}/Items/%s/Intros' % item_id)) + + def get_additional_parts(self, item_id): + return self.doUtils.downloadUrl(self.get_emby_url('Videos/%s/AdditionalParts' % item_id)) + + def get_playback_info(self, item_id, profile, offset=0, audio=None, subtitles=None): + + url = self.get_emby_url('Items/%s/PlaybackInfo' % item_id) + return self.doUtils.downloadUrl(url, action_type="POST", postBody={ + + 'UserId': self.userId, + 'DeviceProfile': profile, + 'StartTimeTicks': offset, #TODO + 'AudioStreamIndex': audio, #TODO + 'SubtitleStreamIndex': subtitles, #TODO + 'MediaSourceId': None, + 'LiveStreamId': None + }) + + def get_live_stream(self, item_id, profile, session_id, token, offset=0, audio=None, subtitles=None): + + url = self.get_emby_url('/LiveStreams/Open') + return self.doUtils.downloadUrl(url, action_type="POST", postBody={ + + 'UserId': self.userId, + 'DeviceProfile': profile, + 'ItemId': item_id, + 'PlaySessionId': session_id, + 'OpenToken': token, + 'StartTimeTicks': offset, #TODO + 'AudioStreamIndex': audio, #TODO + 'SubtitleStreamIndex': subtitles #TODO + }) diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 8d469819..ca40edfa 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -10,6 +10,7 @@ from datetime import datetime import platform import xbmc +import xbmcgui import userclient import clientinfo @@ -52,10 +53,11 @@ class Service(object): self.addon_name = self.client_info.get_addon_name() log_level = settings('logLevel') + # General settings which are used by other entrypoints 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) + window('emby_context', value="true" if settings('enableContext') == "true" else "") + window('emby_context_transcode', value="true" if settings('enableContextTranscode') == "true" else "") # Initial logging log.warn("======== START %s ========", self.addon_name) @@ -72,7 +74,8 @@ class Service(object): "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" + "emby_initialScan", "emby_customplaylist", "emby_playbackProps", + "emby.external_check", "emby.external", "emby.resume" ] for prop in properties: window(prop, clear=True) @@ -319,6 +322,9 @@ class Service(object): #ga = GoogleAnalytics() #ga.sendEventData("Application", "Shutdown") + if self.monitor.special_monitor: + self.monitor.special_monitor.stop_monitor() + if self.userclient_running: self.userclient_thread.stop_client() diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 878dda15..17b7fcd9 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -313,7 +313,7 @@ class UserClient(threading.Thread): log.info("Username found: %s", username) self._auth = True - if monitor.waitForAbort(1): + if monitor.waitForAbort(2): # Abort was requested while waiting. We should exit break diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 71e5922c..759b04f1 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -78,7 +78,7 @@ def dialog(type_, *args, **kwargs): } return types[type_](*args, **kwargs) -def plugin_path(plugin, params): +def urllib_path(plugin, params): return "%s?%s" % (plugin, urllib.urlencode(params)) diff --git a/resources/lib/views.py b/resources/lib/views.py index 63fb5039..6d1aeb46 100644 --- a/resources/lib/views.py +++ b/resources/lib/views.py @@ -14,7 +14,7 @@ import xbmcvfs import read_embyserver as embyserver import embydb_functions as embydb -from utils import window, language as lang, indent as xml_indent, plugin_path +from utils import window, language as lang, indent as xml_indent, urllib_path ################################################################################################# @@ -641,7 +641,7 @@ class VideoNodes(object): 'mode': "browsecontent", 'type': mediatype } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) elif (mediatype == "homevideos" or mediatype == "photos"): params = { @@ -651,7 +651,7 @@ class VideoNodes(object): 'type': mediatype, 'folderid': nodetype } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) elif nodetype == "nextepisodes": params = { @@ -660,7 +660,7 @@ class VideoNodes(object): 'mode': "nextup", 'limit': 25 } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) elif KODI == 14 and nodetype == "recentepisodes": params = { @@ -669,7 +669,7 @@ class VideoNodes(object): 'mode': "recentepisodes", 'limit': 25 } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) elif KODI == 14 and nodetype == "inprogressepisodes": params = { @@ -678,7 +678,7 @@ class VideoNodes(object): 'mode': "inprogressepisodes", 'limit': 25 } - path = plugin_path("plugin://plugin.video.emby/", params) + path = urllib_path("plugin://plugin.video.emby/", params) else: path = "library://video/emby/%s/%s.xml" % (viewid, nodetype) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 7b7ffb66..f3899b1b 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -14,6 +14,7 @@ import downloadutils import librarysync import playlist import userclient +import playbackutils from utils import window, settings, dialog, language as lang, JSONRPC from ga_client import log_error @@ -112,19 +113,31 @@ class WebSocketClient(threading.Thread): @classmethod def _play(cls, data): + ''' {"Id":"e2c106a953bfc0d9c191a49cda6561de", + "ItemIds":["52f0c57e2133e1c11d36c59edcd835fc"], + "PlayCommand":"PlayNow","ControllingUserId":"d40000000000000000000000000000000", + "SubtitleStreamIndex":3,"AudioStreamIndex":1, + "MediaSourceId":"ba83c549ac5c0e4180ae33ebdf813c51"}} + ''' + item_ids = data['ItemIds'] + kwargs = { + + 'SubtitleStreamIndex': data.get('SubtitleStreamIndex'), + 'AudioStreamIndex': data.get('AudioStreamIndex'), + 'MediaSourceId': data.get('MediaSourceId') + } 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) + if playbackutils.PlaybackUtils(None, item_ids[0]).play_all(item_ids, data.get('StartPositionTicks', 0), **kwargs): + 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) @@ -214,7 +227,7 @@ class WebSocketClient(threading.Thread): elif command == 'SetSubtitleStreamIndex': emby_index = int(arguments['Index']) current_file = player.getPlayingFile() - mapping = window('emby_%s.indexMapping' % current_file) + mapping = window('emby_%s.indexMapping.json' % current_file) if emby_index == -1: player.showSubtitles(False) diff --git a/resources/settings.xml b/resources/settings.xml index 24a1951b..0ed30b50 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -49,13 +49,17 @@ + - - - + + + + + + + - @@ -65,7 +69,8 @@ - + + diff --git a/resources/skins/default/1080i/script-emby-connect-login-manual.xml b/resources/skins/default/1080i/script-emby-connect-login-manual.xml index 57e4a88a..35f48010 100644 --- a/resources/skins/default/1080i/script-emby-connect-login-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-login-manual.xml @@ -1,142 +1,185 @@ 200 - 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 600 - 35% - 20% - Background box - white.png - 600 - 480 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 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 + + + + + + + + + 50% + 50% + 470 + 420 + + -30 + + 20 + 100% + 25 + logo-white.png + keep + + + 100% - 100 + 420 + dialogs/dialog_back.png - - 150 - - Username - - ffa6a6a6 - font10 - top + 50% + 10 + 460 + 400 + + vertical + 0 + + 100% + 75 + center + 20 + font10 + white + 66000000 + + + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 205 + - + - + + + 20 + 20 + 1 + 80 + white.png + + + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 204 + 200 + - + - + true + + + separator + 20 + 20 + 1 + 80 + white.png + + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + 205 + Conditional + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional + - + + + 420 + False - 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 + Error box 100% - 50 - 201 + 70 + dialogs/dialog_back.png - - - Cancel - box.png - box.png - - font10 - ffa6a6a6 - white - center - 100% + + 10 50 - 55 - 200 + ffe1e1e1 + true + 66000000 + font10 + 20 + center + center diff --git a/resources/skins/default/1080i/script-emby-connect-login.xml b/resources/skins/default/1080i/script-emby-connect-login.xml index cb69387c..4ac5ab04 100644 --- a/resources/skins/default/1080i/script-emby-connect-login.xml +++ b/resources/skins/default/1080i/script-emby-connect-login.xml @@ -1,176 +1,221 @@ - 200 - 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 600 - 35% - 15% - Background box - white.png - 600 - 700 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 705 - False - - Error box - white.png - 100% - 50 - - - - Error message - white - font10 - center - center - 50 - - - - - Emby logo - logo-white.png - 160 - 49 - 30 - 25 - - - 500 - 50 - - Sign in emby connect - - white - font12 - top - 115 - - + + + + + + + + + 50% + 50% + 470 + 620 - 190 - - Username email - - ffa6a6a6 - font10 - top - - + -30 - separator - 102% - 0.5 - 66 - -10 - emby-separator.png - - - - - Password - 275 - - Password label - - ffa6a6a6 - font10 - top - - - - separator - 102% - 0.5 - 66 - -10 - emby-separator.png - - - - - Buttons - 385 - - Sign in - box.png - box.png - - font10 - ffa6a6a6 - white - center + 20 100% - 50 - 201 - - - - Cancel - box.png - box.png - - font10 - ffa6a6a6 - white - center - 100% - 50 - 55 - 200 + 25 + logo-white.png + keep - + + 100% + 610 + dialogs/dialog_back.png + - Disclaimer - 510 - - Disclaimer label - - font10 - ff464646 - true - top - 340 - 100% - - - + 50% + 10 + 460 + 590 + + vertical + 0 - Scan me - - font12 - ff0b8628 - top - 200 - 120 - 230 + 100% + 75 + center + 20 + font10 + white + 66000000 + - - - qrcode - qrcode_disclaimer.png - 140 - 140 - 10 - 360 + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 205 + - + - + + + 20 + 20 + 1 + 80 + white.png + + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 204 + 200 + - + - + true + + + separator + 20 + 20 + 1 + 80 + white.png + + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional + + + spacer + 20 + + + + + font10 + ff464646 + 66000000 + true + true + top + 130 + 20 + 160 + + + 10 + 20 + 130 + + 130 + 130 + qrcode + qrcode_disclaimer.png + + + 135 + center + + font10 + FF52b54b + 66000000 + top + + + + + + + 610 + False + + Error box + 100% + 70 + dialogs/dialog_back.png + + + 10 + 50 + ffe1e1e1 + true + 66000000 + font10 + 20 + center + center diff --git a/resources/skins/default/1080i/script-emby-connect-server-manual.xml b/resources/skins/default/1080i/script-emby-connect-server-manual.xml index 27e50037..fdc45b0e 100644 --- a/resources/skins/default/1080i/script-emby-connect-server-manual.xml +++ b/resources/skins/default/1080i/script-emby-connect-server-manual.xml @@ -2,150 +2,184 @@ 200 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 600 - 35% - 20% - Background box - white.png - 600 - 525 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 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 + + + + + + + + + 50% + 50% + 470 + 420 + + -30 + + 20 + 100% + 25 + logo-white.png + keep + + + 100% - 100 + 420 + dialogs/dialog_back.png - - 150 - - Host - - ffa6a6a6 - font10 - top + 50% + 10 + 460 + 400 + + vertical + 0 + + 100% + 75 + center + 20 + font10 + white + 66000000 + + + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 205 + [COLOR ff464646]IP or https://myserver.com[/COLOR] + - + - + + + 20 + 20 + 1 + 80 + white.png + + + + 100 + + + ffe1e1e1 + 66000000 + font10 + top + 20 + + + 35 + 20 + 20 + 50 + font10 + FF888888 + FF52b54b + 66000000 + 204 + 200 + - + - + + + separator + 20 + 20 + 1 + 80 + white.png + + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional + + + + 426 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional + - + + + 420 + False - 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 + Error box 100% - 50 - 201 + 70 + dialogs/dialog_back.png - - - Cancel - box.png - box.png - - font10 - ffa6a6a6 - white - center - 100% + + 10 50 - 55 - 200 + ffe1e1e1 + true + 66000000 + font10 + 20 + center + center diff --git a/resources/skins/default/1080i/script-emby-connect-server.xml b/resources/skins/default/1080i/script-emby-connect-server.xml index bc751e8c..65e10181 100644 --- a/resources/skins/default/1080i/script-emby-connect-server.xml +++ b/resources/skins/default/1080i/script-emby-connect-server.xml @@ -1,277 +1,239 @@ 205 - 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 450 - 38% - 15% - Background box - white.png - 450 - 710 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 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 - - + + + + + + + + + 50% + 50% + 470 + 480 - 100% - 220 - - 45 - 150 + -30 + + 20 + 100% + 25 + logo-white.png + keep + + + 20 + 100% + 25 + keep + userflyoutdefault.png + + + + 100% + 480 + dialogs/dialog_back.png + + + 50% + 10 + 460 + 460 + + vertical + 0 + + 100% + 75 + center + 20 + font10 + white + 66000000 + + + + 200 + + 50% + 460 + 200 + noop + close + close + 205 + + + 20 + 5 + 40 + + keep + network.png + String.IsEqual(ListItem.Property(server_type),network) + + + keep + wifi.png + String.IsEqual(ListItem.Property(server_type),wifi) + + + + 50 + 50 + center + 20 + font10 + ffe1e1e1 + 66000000 + + + + + + 100% + 50 + white.png + !Control.HasFocus(155) + + + 100% + 50 + white.png + Control.HasFocus(155) + + + 20 + 40 + + Network + keep + network.png + String.IsEqual(ListItem.Property(server_type),network) + + + Wifi + keep + wifi.png + String.IsEqual(ListItem.Property(server_type),wifi) + + + + 50 + 50 + center + 20 + font10 + true + ffe1e1e1 + 66000000 + + + + + + 50% + Busy animation + center + 120 + 200 + false + spinner.gif + keep + + + + spacer + 20 + - True - Sign in Connect - box.png - box.png - - font10 - ffa6a6a6 - white - center - 350 + + 426 50 - 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no 155 - 206 + Conditional - - Manually add server - box.png - box.png - - font10 - ffa6a6a6 - white - center - 55 - 350 + + 426 50 - 50 - 205 - 155 - 201 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + 155 + Conditional - - Cancel - box.png - box.png - - font10 - ffa6a6a6 - white - center - 110 - 350 + + 426 50 - 50 - 206 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + 155 + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + Conditional - - - 100% - False - - Message box - white.png - 100% - 50 - 20 - - - - Message - white - font10 - center - center - 50 - 20 - + + + 480 + False + + 100% + 70 + dialogs/dialog_back.png + + + 10 + 50 + ffe1e1e1 + true + 66000000 + font10 + 20 + center + center> diff --git a/resources/skins/default/1080i/script-emby-connect-users.xml b/resources/skins/default/1080i/script-emby-connect-users.xml index 0acd2b32..264acbbb 100644 --- a/resources/skins/default/1080i/script-emby-connect-users.xml +++ b/resources/skins/default/1080i/script-emby-connect-users.xml @@ -1,194 +1,226 @@ 155 - 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 715 - 32% - 20% - Background box - white.png - 100% - 525 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 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 - - + + + + + + + + + 50% + 50% + 920 + 570 - 615 - 325 - 100% - - - Manual Login button - box.png - box.png - - center + -30 + + 20 + 100% + 25 + logo-white.png + keep + + + + 100% + 570 + dialogs/dialog_back.png + + + 50% + 10 + 908 + 550 + + vertical + 0 + 100% - 50 - 35 + 75 + center + 20 font10 - ffa6a6a6 - white - 201 - 155 + white + 66000000 + - - - Cancel - box.png - box.png - - font10 - ffa6a6a6 - white - center - 100% + + Conditional + Conditional + 50% + 908 + 362 + noop + noop + noop + 200 + horizontal + + + 20 + 10 + + -2 + -2 + 282 + 282 + items/shadow_square.png + + + 276 + 322 + white.png + stretch + + + 276 + 276 + white.png + stretch + String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png) + + + 276 + 276 + $INFO[ListItem.Icon] + stretch + + + 285 + 276 + + 100% + 30 + + font10 + ffe1e1e1 + 66000000 + Control.HasFocus(155) + center + + + + + + + 20 + 10 + + -2 + -2 + 282 + 282 + items/shadow_square.png + + + 276 + 322 + white.png + stretch + + + 276 + 276 + white.png + stretch + String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png) + + + 276 + 276 + $INFO[ListItem.Icon] + stretch + + + -12 + -7 + 300 + 300 + items/focus_square.png + scale + Focus + UnFocus + Control.HasFocus(155) + + + 285 + 276 + + 100% + 30 + + font10 + white + 66000000 + Control.HasFocus(155) + center + Control.HasFocus(155) + + + 100% + 30 + + font10 + ffe1e1e1 + 66000000 + false + center + !Control.HasFocus(155) + + + + + + + + 874 50 - 90 - 200 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + 155 + Conditional + + + + 874 + 50 + font10 + ffe1e1e1 + white + ffe1e1e1 + 66000000 + 20 + center + center + buttons/shadow_smallbutton.png + buttons/shadow_smallbutton.png + no + 155 + Conditional diff --git a/resources/skins/default/1080i/script-emby-context.xml b/resources/skins/default/1080i/script-emby-context.xml index df4f0588..355eee9d 100644 --- a/resources/skins/default/1080i/script-emby-context.xml +++ b/resources/skins/default/1080i/script-emby-context.xml @@ -1,105 +1,97 @@ 155 - 0 - dialogeffect - - Background fade - 100% - 100% - emby-bg-fade.png - - - 450 - 38% - 36% - Background box - white.png - 90 + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose - - - 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 - + + + + + + + + + 50% + 50% + 370 + 220 + + -30 + + 20 + 100% + 25 + logo-white.png + keep + + + 20 + 100% + 25 + keep + userflyoutdefault.png + + + 100% - 100% - center - 155 - 155 - 155 - 155 - 200 - - - Background box - 450 - white.png - + 220 + dialogs/dialog_back.png + + + 50% + 10 + 360 + 200 + noop + close + close + noop + - 400 - font11 - ff525252 - 25 - center + 100% + 50 center - ListItem.Label + 20 + font10 + ffe1e1e1 + 66000000 + - - + - Background box - 450 - white.png + 100% + 50 + white.png + !Control.HasFocus(155) - 400 - 25 - center - white.png + 100% + 50 + white.png Control.HasFocus(155) - Visible - Hidden - 400 - font11 - white - 25 - center + 100% + 50 center - ListItem.Label + 20 + font10 + true + ffe1e1e1 + 66000000 + diff --git a/resources/skins/default/1080i/script-emby-resume.xml b/resources/skins/default/1080i/script-emby-resume.xml new file mode 100644 index 00000000..77402250 --- /dev/null +++ b/resources/skins/default/1080i/script-emby-resume.xml @@ -0,0 +1,110 @@ + + + 100 + + + + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose + + + Conditional + + + + + + + + + 50% + 50% + 20% + 90% + + vertical + 0 + 0 + auto + center + 0 + close + close + true + + 30 + + 20 + 100% + 25 + logo-white.png + keep + + + 20 + 100% + 25 + keep + $INFO[Window(Home).Property(EmbyUserImage)] + !String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + 20 + 100% + 25 + keep + userflyoutdefault.png + String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + + 100% + 10 + dialogs/menu_top.png + + + 100% + 50 + left + center + 20 + font10 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 50 + left + center + 20 + font10 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 10 + dialogs/menu_bottom.png + + + + + + diff --git a/resources/skins/default/media/buttons/shadow_smallbutton.png b/resources/skins/default/media/buttons/shadow_smallbutton.png new file mode 100644 index 0000000000000000000000000000000000000000..2ec35c5674935a12ea027f36bea700be1232ffc7 GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^Z9r_m!3HF+ykh#vz`z*i>EaktG3V`F!>q>+3~mqA zeXprK5ZT$7$Ul|)NnCaN6OP+I3Q-bQ4^xsb2XvFokmcM|1O_ao6*YG9To7lX#56$;6wSQ2& z+`cwuL7QL?qjk4hN)AK`@XzdCv>25?{B3KOBp%Rh>Ng zf{W)c!w0_0eTGb53oLA;j~DSCK9INlH}9T8yOezl+n*O6FEM&E>*XHz{>qrPiJ8kQ vnBy9*|9s1wyXHrgYwN!Kg=X^`KFZhLa%B8^a`9qdfHQcy`njxgN@xNAi>0ZN literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/dialog_back.png b/resources/skins/default/media/dialogs/dialog_back.png new file mode 100644 index 0000000000000000000000000000000000000000..6422ff5a20ab4177b062c58afb805a7254d76689 GIT binary patch literal 15141 zcmeI3e{37&8OPsHC|$Z_Xsjg-K^!J6AZp*89VhnHap>YUafy?taT`}7YtMJ*^E)B_@!*Qu^xH~8e?-bmkrOjLINpUbiOfq;X6^kl5muj`- z@^UaXn`sM`lNdW&E$hvKR4C9(wX1Q7a@v}$0_$*5Ep8j@bi3?LYbiU!Hq(roW}B=G z%Q0?_VX4B$;;n{}CoV>~&AyF=a`0EHWymlzj;52zq%G;NsqsOYb-Ud(W2fzQD^ytZ zv|{intD@hWPcoOsC+R|5)(lxyC^Ih~Rue|6#bOpJehT*$(~5-@y}%Aqq*J^`vo?mV zWDE%u@!yVkZP#yz%D-#XV3m2+p3#>aKZ+;Odz zxh+>b#ENH>>B;R}ju*_+%qy51LV|$jwU&lebQWy#|2u*C{D^(=8p$C^xzvWrr^=}o zPok?4Bgx05^@DItT+Uw4XPs{=Pw%14(?2TDpNM?x{P~$%u?y$ZV;*W8Tnlhqd~Oa{ z551tRR5`B?nR6OVZ~j>5 zgAWOkx7q1*I6ZWEbGf5ePD#;_Plo$fH&>+--s1fNX81r+5{6Ei4 zWxf?YEx-pAy7;6rf0m0o;VTYGck~q}M$?fZC=F~(aA6Ul0)h*rfsF|+EFx4uaG^A? zF~NmJgbD~Qlm<2?xUh&&0l|gRz{Ug@77;2SxKJ9{nBc-9LIngDN&_1cTv$Y?fZ#%D zU}J&{iwG4ETqq4}OmJZlp#p*nrGbqJE-WHcKyaZnura}fMT80nE|dl~Cb+POPyxY( z(!j<97ZwpJAh=K(*qGqLB0>cO7fJ&g6I@tCsDR)?X<%c53yTO95L_q?Y)o)r5upNt z3#EaL2`(%mR6uZ{G_Wzjg++u42riTcHohdT>e9P^k^*1&OTst%w%($ggKq^=Lg(fH z0K=;R7`+#O|IEVcM*t)k0RFiH0B#=u*QuH9FK-0kiebNReK0kBc6z-seMjBy_fD|> z`UigY>V^N_@}Xo|u?hUu_F2!W-jlc9ckbsM^*?4liL5-hn{D5<>e%mY1he56>il;4 z*wJ?rABUcpt<9=VvCOMk@Ol5(l4Z4&3z(PDZ0SO|a=&sHQviPzb&JU_S2wn# zm~Sp^|I!aa!lAXtYq|#dr^fUy``b0^&RqHK@n`qjk9>GId-&AZtFAuEYo81lu($L4 z!=f-bdi|+jU+suCeeNGW6(__PYzX!D)y5uiG_KmaHse{dZB4rSd`8^z*p1(N@U3s8 zI@Zmeesd}^`8VgEKHYWTxj%k%$AKAb`lX3IuaCU^hue*8>rBn3FT8&!aQ=yR-fP(V zn$@MBKGk;gnaLCT#A_O|_hv7=;+X0BcW-R`ot~#2U%qML6{JNRRGM+E&<=V@X#J==JgHci>&?)-M(kPy<_Zul(o7- literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_back.png b/resources/skins/default/media/dialogs/menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..0747aa019a1033df1e6a7ffc65746320b717a520 GIT binary patch literal 15358 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 aE?oy~P-4!)mf6+|mf`uGp`G)VJ@FsOJ0TVT literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_top.png b/resources/skins/default/media/dialogs/menu_top.png new file mode 100644 index 0000000000000000000000000000000000000000..26bc7d15bafd4e48db9f30f0ee2bbbb9612eb996 GIT binary patch literal 14768 zcmeI3Z)_7~9LFEP2~;pB$Uq@EOAt`oyKB3yceW*C1vazJu@%|ig}dE7-7d6u$K8!~ z3&tU8LeLkY5fEYwMgvCh1;IZd3^4k}pdtE7GyzPMh!|f;NX852bJwnY+I2i&c==qi z_Sfh6et*B`KF^=so7>&dzJ9LfQ4auMZfi@T6M$(;Q0$p?Kl)71x$*=0nr*c7SO7dQ z-}##c-aWJkfXH$+)opht+eBH<`Xxp0gZ_NhKxzP@tMi5=Z-q9~2m4hm=KcHB884$M zG4G~^B$qUr;egsQY{IVL_LMxlRTdQQ>bNJG7g2&Nv?V5=&1jaGk9pm^B8r`6*2}mO zduzDOlO0U6ZbGKPANI+7Fv2tne!fA71VT?U0gex|oWSxSAIFQFAaXoYyu5J_ zilU~H7CRH`isjI+n0LUo4UuJYxtu>2^y_9n%L{_Qasf6F@F9iI8q#bj@6)Vhg(MSs z63~)O)v#4vW1PHFpFU{Eyk4hJ=_=k=)+iOytRg!^kN8_RU@-Ej|kt}(?oIO{RUKg9(^j>D1^ zlvO#Bx}v6sDsu3V6pyT7F;I~MQd`$v&%>5j7CS}czui)r1=N_Je$O8t;EwEiet zZp4(ORV6m-8Qny~g7ILKt<{XV+?6J_YL+c&GHgx6QG;Jq6|`}sa9^;I_l2d9;FJ17 zoUbn(lzmbN%1{vmE)+u9C)p>ZPU!NWvs;R(mAg!h*(arb&Q#G!CuJ~=Gy1zNQ6tTy zQZ1^5*5?qmZqe#>&$2GKt+I8ZBcr-!k|CKEbaq_KJF(?Thgj*1ay;(N6(!j@&Em4N zA)pdvD_YCLDmjZb_5V&_AwOoHx<+mQYPZ`^g;cI;>Liw)wsVpRoBGk3xRbqJ$XaQu zP9J5R(?6qDPDG)#a(*UO?Bw~3I)|Ew)&d%riq)!`F_&}l7LJ83^pF7K;f97_LzJyar$LMGwSae2z`{y`1B@Rvhx_8k=7d>FkI$jB|7NdShI12D1+fU$A({RaSp901pP z0TA~CP_OTO;hS~n6~wyML{ln%@%qIk?JI8Ht5+_4{mS0duYuF!vxg7(H#eQzR{!Gh znW;yPDr0dqw0Fj1?0c`x-mR~?(Vb3@Z_qvO|31%u@WRZEJJRW!`2r`$Fa+2`4i9V>{{SoarwwE zk!xd%TZA^Y=kb5$EE!q6f5Dm$Ml!SlPu>q5PJcT&>rnX0`OznXTh4vbHuh89=cn5avwK!tdh5Wk-os}`p1n|iF8i&z zM-HA?|Hh4d?VEPae`&K|oX;FP`{k`I-_;{pXrVnr8l3SiGG6Cwe5-HYkFV) E58UV(?EnA( literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/white.jpg b/resources/skins/default/media/dialogs/white.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1206155018137636cf6c1c658768acce3283c22 GIT binary patch literal 8060 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 VJoC;o|NMQa{Mros_ze8yzX3F67cu|< literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/focus_square.png b/resources/skins/default/media/items/focus_square.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c1f377e40bfe04244d732fd45da5d31fac7380 GIT binary patch literal 1970 zcmeHI>raz+6n?AREEIH3-B3_I2q94}(G~%vY^caKE)#yx054YNionXHw542nSx0Ie zIGx3jl{Jj9;do063&qlMt&kC9447*>i?(*1!o&_4&|2ta^?%q$Px2%u=XpMzoaa1O zGt!bV=s+|CL70?tr_VzW>c}hjc)2Gv%r9sV9P#)uWp@}K9eh|% zM(cwYEtX5Vo9BjC*P!h;`=-^1?IP(MooUbl8mxtFB#u}?X;3DqN=#H1%c-hQaP=bk z9%L}mQ48&J!hj7+vfCo|HqHudQ&DD5F-F~2G|OU9sq2eK?FwxuJkd10INQdi*4Nvu zq_VJ#cvT?TrrfA?$fe7A=1k;{3;>u0Hnw$!_2FQy>GQsev{20G+hNr0vmVI3M6T#lD80lEvVht&uAtf5C z17B+8ILyc<-41bDsB*-qu6K~NG`s|%!Jl|vIeHQAuj%z`-C(kDwMzwNsNl;-;~UaJ z(4xu>u*&l8(!a*_1CN1-@jr`;N>BJ}qQVGcsU`8p-|jYY_w22>uiK;16jrgYSg7pY zC4s-B*TZ)pMQ}83y^e6F!L4LhQi~|W_kD)KL+fPKX#5E3u~$<(^@X4B*K=a$kwZKpD%!)BA3{j0E#_QYo z|LqXHr`xGBY$F;98r>e=bbqI|`G=2kTnW%1`GKs6Vm$7zv2;G6*ea;z>tI78X2f%@ z$)A09G_S+*yrDEY`auqG{WT?+{l_rs5|f;!8Hhm+#eJ z=P3&Nv?Nc?q7V1@+6f|&$YHXW7e>30#n#FKAVzX?0DEbU34>*UK#-o6o<6F$Bll#< zUUjjtv9|AVj;^a!-K*%*@>>8*1{_SV&kWaDS#^uQimIQk+0v!Uc*xGqlVrP?o9lmT zuLqynw5vYzzL2Hv6BfV{F|x8b(j#5&J)EQC>W%7=7g(QlVKEKt# zUL|~W%`rN+|HO+qQA-itiWqSaG5dJ?b9TG_=#vZFj32;EKQ7B131GhjM=*gL zys~<$a_DZLA!aD#lg9p_;ftDAIIq b*XBXZlib^7G3EcdvlvP_lXhDAE|L2mTJagg literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/logindefault.png b/resources/skins/default/media/items/logindefault.png new file mode 100644 index 0000000000000000000000000000000000000000..50407b14544752ed682233caf7fa20de8a920e1b GIT binary patch literal 17637 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 HKK1_qs&H1S literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/mask_square.png b/resources/skins/default/media/items/mask_square.png new file mode 100644 index 0000000000000000000000000000000000000000..912d133552895eee1289c4ef63fb0d7b89aad870 GIT binary patch literal 1075 zcmeAS@N?(olHy`uVBq!ia0y~yU=#si4mP03tAdl23=GV_JY5_^D(1YsaWHcVqe$yT z>)?hZoEL>2-Q!wwOep7=h{DZmPR7|Coxkt5iYYv4PCR5Odgm(p`?-COZ*1Fsdtc7) z9dFMv{5yYn+wa`@+yC0g9{-qkyFb@ld@gIu=Ht2Bmp``Hm-oBmrRjmIyRCte+2>>) zJguAGSMM*xIcfq}H8{U@PzPs&tw;$8G hv!DNMdt7$=u>4ME&ApA2L_viCgQu&X%Q~loCIEedKe7M- literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/items/shadow_square.png b/resources/skins/default/media/items/shadow_square.png new file mode 100644 index 0000000000000000000000000000000000000000..7c8606162ff76057c3894b8b0ce8ab9f80af198c GIT binary patch literal 1374 zcmeAS@N?(olHy`uVBq!ia0y~yV3Y!34mP03xy>_nF)*;Idb&7GHBn)$OxKW7ks!n1S7YWDjvpKhcd?=Jm( z)bxP-&OH(S@iFyXCyHb4E_|jqL2ldM zs+}?O;!<}X2!C$TXMX$L_Pw_yXUA`=y>oN&^5-Y+tvKKNg_Ge&Ov-usp}@4U}B{jr9vc>CMy*Kf;x;H}*M_IK|6w>hWJ zes(F&p2hf&=laTLb@J_Om7Lf%#A0p38i8wuP8 zKWSk*!O1&8SV}?N#KAA4Va^2x%TbBZAQ?>zII}_P>C#+l=i-~+N@R}D4!dU3_8wf$fjmbLF?&=KRjpclx~J?cG9|?dx|~KF}_gIevfJ-{jl+ z*2zHOqTR)_DtFxb@zJ91`|W%2dee_zzoqk`bf=x(-wnn_!0dnP+rwY~Uf!tmE8hKf zcW(XtZ+W-BTgxxK7iVw(@6%l7`0nSAr}tZ{&z!&QZ}IK(Kr<`e^qe@mZTH;7o%bc@ zZogiA{yyUm|9Ski@@2=K9k#Q#s-2aw?fk~d&Bl7=*30M2{>drxbP0l+XkK@USjW literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/network.png b/resources/skins/default/media/network.png index 7cd11ddfee5bc27524e5757f00a32e37185c113f..2f0461bf6e98cac2b1a808b24cf54666f75d13a0 100644 GIT binary patch literal 3818 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 WPz6(-p}+nQ%V%O_f#V!=4EYB=8i&IG literal 727 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�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%0AD7PH8ZsJIU-t0a1 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