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)
This commit is contained in:
angelblue05 2018-01-07 20:13:11 -06:00 committed by GitHub
parent 6005555b37
commit 9ac37a1c40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2679 additions and 2378 deletions

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.emby" <addon id="plugin.video.emby"
name="Emby" name="Emby"
version="2.3.61" version="3.0.0"
provider-name="Emby.media"> provider-name="Emby.media">
<requires> <requires>
<import addon="xbmc.python" version="2.19.0"/> <import addon="xbmc.python" version="2.25.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
<import addon="plugin.video.emby.movies" version="0.11" /> <import addon="plugin.video.emby.movies" version="0.11" />
<import addon="plugin.video.emby.tvshows" version="0.11" /> <import addon="plugin.video.emby.tvshows" version="0.11" />
@ -16,12 +16,17 @@
</extension> </extension>
<extension point="xbmc.service" library="service.py" start="login"> <extension point="xbmc.service" library="service.py" start="login">
</extension> </extension>
<extension point="kodi.context.item" library="contextmenu.py"> <extension point="kodi.context.item">
<item> <menu id="kodi.core.main">
<item library="contextmenu.py">
<label>30401</label> <label>30401</label>
<description>Settings for the Emby Server</description> <visible>[!String.IsEmpty(ListItem.DBID) + !String.IsEqual(ListItem.DBID,-1) | !String.IsEmpty(ListItem.Property(embyid))] + !String.IsEmpty(Window(10000).Property(emby_context))</visible>
<visible>[!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(embyid))] + !IsEmpty(Window(10000).Property(emby_context))</visible>
</item> </item>
<item library="contextmenu_play.py">
<label>30402</label>
<visible>[[!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))</visible>
</item>
</menu>
</extension> </extension>
<extension point="xbmc.addon.metadata"> <extension point="xbmc.addon.metadata">
<platform>all</platform> <platform>all</platform>
@ -32,8 +37,9 @@
<source>https://github.com/MediaBrowser/plugin.video.emby</source> <source>https://github.com/MediaBrowser/plugin.video.emby</source>
<summary lang="en"></summary> <summary lang="en"></summary>
<description lang="en">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.&#10;&#10;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!</description> <description lang="en">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.&#10;&#10;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!</description>
<news>v2.3.56 <news>v3.0.0
- Add support for Kodi Leia (Alpha) MusicDatabase v67 - Minimum version supported is now Kodi Krypton.
- For more info, check the emby Kodi forum thread.
</news> </news>
</extension> </extension>
</addon> </addon>

37
contextmenu_play.py Normal file
View file

@ -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)

View file

@ -102,7 +102,13 @@ class Main(object):
'deviceid': entrypoint.resetDeviceId, 'deviceid': entrypoint.resetDeviceId,
'delete': entrypoint.deleteItem, 'delete': entrypoint.deleteItem,
'connect': entrypoint.emby_connect, '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: if mode in modes:
# Simple functions # Simple functions

View file

@ -5,7 +5,7 @@
<string id="29999">Emby for Kodi</string> <string id="29999">Emby for Kodi</string>
<string id="30000">Server address</string> <string id="30000">Server address</string>
<string id="30001">Server name</string> <string id="30001">Server name</string>
<string id="30002">Play from HTTP instead of SMB</string> <string id="30002">Enable HTTP playback</string>
<string id="30004">Log level</string> <string id="30004">Log level</string>
<string id="30016">Device Name</string> <string id="30016">Device Name</string>
<string id="30022">Advanced</string> <string id="30022">Advanced</string>
@ -175,7 +175,8 @@
<string id="30312">Channels</string> <string id="30312">Channels</string>
<!-- contextmenu --> <!-- contextmenu -->
<string id="30401">Emby options</string> <string id="30401">Emby Options</string>
<string id="30402">Emby Force transcode</string>
<string id="30405">Add to Emby favorites</string> <string id="30405">Add to Emby favorites</string>
<string id="30406">Remove from Emby favorites</string> <string id="30406">Remove from Emby favorites</string>
<string id="30407">Set custom song rating</string> <string id="30407">Set custom song rating</string>
@ -206,7 +207,7 @@
<string id="30517">Network credentials</string> <string id="30517">Network credentials</string>
<string id="30518">Enable Emby cinema mode</string> <string id="30518">Enable Emby cinema mode</string>
<string id="30519">Ask to play trailers</string> <string id="30519">Ask to play trailers</string>
<string id="30520">Skip Emby delete confirmation for the context menu (use at your own risk)</string> <string id="30520">Skip Emby delete confirmation (use at your own risk)</string>
<string id="30521">Jump back on resume (in seconds)</string> <string id="30521">Jump back on resume (in seconds)</string>
<string id="30522">Force transcode H265</string> <string id="30522">Force transcode H265</string>
<string id="30523">Music metadata options (not compatible with direct stream)</string> <string id="30523">Music metadata options (not compatible with direct stream)</string>

View file

@ -7,6 +7,8 @@
import logging import logging
from utils import settings from utils import settings
import artwork
################################################################################################## ##################################################################################################
log = logging.getLogger("EMBY."+__name__) log = logging.getLogger("EMBY."+__name__)
@ -19,6 +21,7 @@ class API(object):
def __init__(self, item): def __init__(self, item):
# item is the api response # item is the api response
self.item = item self.item = item
self.artwork = artwork.Artwork()
def get_userdata(self): def get_userdata(self):
# Default # Default
@ -71,12 +74,8 @@ class API(object):
writer = [] writer = []
cast = [] cast = []
try: if 'People' in self.item:
people = self.item['People'] for person in self.item['People']:
except KeyError:
pass
else:
for person in people:
type_ = person['Type'] type_ = person['Type']
name = person['Name'] name = person['Name']
@ -95,6 +94,26 @@ class API(object):
'Cast': cast '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): def get_media_streams(self):
video_tracks = [] video_tracks = []

View file

@ -468,7 +468,7 @@ class Artwork(object):
image = "" image = ""
person_id = person['Id'] person_id = person['Id']
if "PrimaryImageTag" in person: if 'PrimaryImageTag' in person:
image = ( image = (
"%s/emby/Items/%s/Images/Primary?" "%s/emby/Items/%s/Images/Primary?"
"MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
@ -518,14 +518,14 @@ class Artwork(object):
tag, custom_query)) tag, custom_query))
all_artwork['Backdrop'].append(artwork) 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 if not tag: return
artwork = ("%s/emby/Items/%s/Images/%s/0?" artwork = ("%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s" "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, item_id, type_, max_width, max_height, tag, custom_query)) % (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 # Process backdrops
get_backdrops(item_id, backdrops) get_backdrops(item_id, backdrops)
@ -554,6 +554,9 @@ class Artwork(object):
get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork, get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork,
item['Parent%sImageTag' % 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 # Parent album works a bit differently
if not all_artwork['Primary']: if not all_artwork['Primary']:

View file

@ -78,22 +78,7 @@ class ClientInfo(object):
emby_guid = xbmc.translatePath("special://temp/emby_guid").decode('utf-8') 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): if reset and xbmcvfs.exists(emby_guid):
# Reset the file
xbmcvfs.delete(emby_guid) xbmcvfs.delete(emby_guid)
guid = xbmcvfs.File(emby_guid) guid = xbmcvfs.File(emby_guid)

View file

@ -4,21 +4,26 @@
import logging import logging
import sys import sys
from datetime import timedelta
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import api import api
import read_embyserver as embyserver import read_embyserver as embyserver
import playbackutils as pbutils
import embydb_functions as embydb import embydb_functions as embydb
import musicutils as musicutils import musicutils as musicutils
from utils import settings, dialog, language as lang from utils import settings, dialog, language as lang
from dialogs import context from dialogs import context, resume
from database import DatabaseConn from database import DatabaseConn
################################################################################################# #################################################################################################
log = logging.getLogger("EMBY."+__name__) log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
XML_PATH = (addon.getAddonInfo('path'), "default", "1080i")
OPTIONS = { OPTIONS = {
'Refresh': lang(30410), 'Refresh': lang(30410),
@ -38,7 +43,7 @@ class ContextMenu(object):
_selected_option = None _selected_option = None
def __init__(self): def __init__(self, force_transcode=False):
self.emby = embyserver.Read_EmbyServer() self.emby = embyserver.Read_EmbyServer()
@ -53,7 +58,10 @@ class ContextMenu(object):
self.item = self.emby.getItem(self.item_id) self.item = self.emby.getItem(self.item_id)
self.api = api.API(self.item) self.api = api.API(self.item)
if self._select_menu(): if force_transcode:
self._force_transcode()
elif self._select_menu():
self._action_menu() self._action_menu()
if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'],
@ -104,10 +112,6 @@ class ContextMenu(object):
userdata = self.api.get_userdata() userdata = self.api.get_userdata()
options = [] options = []
if self.item_type in ("movie", "episode", "song"):
#options.append(OPTIONS['Transcode'])
pass
if userdata['Favorite']: if userdata['Favorite']:
# Remove from emby favourites # Remove from emby favourites
options.append(OPTIONS['RemoveFav']) options.append(OPTIONS['RemoveFav'])
@ -126,9 +130,7 @@ class ContextMenu(object):
# Addon settings # Addon settings
options.append(OPTIONS['Addon']) options.append(OPTIONS['Addon'])
addon = xbmcaddon.Addon('plugin.video.emby') context_menu = context.ContextMenu("script-emby-context.xml", *XML_PATH)
context_menu = context.ContextMenu("script-emby-context.xml", addon.getAddonInfo('path'),
"default", "1080i")
context_menu.set_options(options) context_menu.set_options(options)
context_menu.doModal() context_menu.doModal()
@ -141,10 +143,7 @@ class ContextMenu(object):
selected = self._selected_option.decode('utf-8') selected = self._selected_option.decode('utf-8')
if selected == OPTIONS['Transcode']: if selected == OPTIONS['Refresh']:
pass
elif selected == OPTIONS['Refresh']:
self.emby.refreshItem(self.item_id) self.emby.refreshItem(self.item_id)
elif selected == OPTIONS['AddFav']: elif selected == OPTIONS['AddFav']:
@ -198,3 +197,24 @@ class ContextMenu(object):
if delete: if delete:
log.info("Deleting request: %s", self.item_id) log.info("Deleting request: %s", self.item_id)
self.emby.deleteItem(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)

View file

@ -57,7 +57,6 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
for option in self._options: for option in self._options:
self.list_.addItem(self._add_listitem(option)) self.list_.addItem(self._add_listitem(option))
self.background = self._add_editcontrol(730, height, 30, 450)
self.setFocus(self.list_) self.setFocus(self.list_)
def onAction(self, action): def onAction(self, action):

View file

@ -22,6 +22,8 @@ SIGN_IN = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
USER = 204
PASSWORD = 205
ERROR = { ERROR = {
'Invalid': 1, 'Invalid': 1,
'Empty': 2 'Empty': 2
@ -52,21 +54,14 @@ class LoginConnect(xbmcgui.WindowXMLDialog):
def onInit(self): 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.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.signin_button = self.getControl(SIGN_IN)
self.remind_button = self.getControl(CANCEL) self.remind_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG) 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): def onClick(self, control):
if control == SIGN_IN: if control == SIGN_IN:
@ -97,23 +92,6 @@ class LoginConnect(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close() 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): def _login(self, username, password):
result = self.connect_manager.loginToConnect(username, password) result = self.connect_manager.loginToConnect(username, password)

View file

@ -23,6 +23,8 @@ SIGN_IN = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
USER = 204
PASSWORD = 205
ERROR = { ERROR = {
'Invalid': 1, 'Invalid': 1,
'Empty': 2 'Empty': 2
@ -50,7 +52,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
self.server = server self.server = server
def set_user(self, user): def set_user(self, user):
self.username = user or {} self.username = user or None
def get_user(self): def get_user(self):
return self._user return self._user
@ -61,8 +63,8 @@ class LoginManual(xbmcgui.WindowXMLDialog):
self.cancel_button = self.getControl(CANCEL) self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG) self.error_msg = self.getControl(ERROR_MSG)
self.user_field = self._add_editcontrol(725, 400, 40, 500) self.user_field = self.getControl(USER)
self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1) self.password_field = self.getControl(PASSWORD)
if self.username: if self.username:
self.user_field.setText(self.username) self.user_field.setText(self.username)
@ -70,13 +72,6 @@ class LoginManual(xbmcgui.WindowXMLDialog):
else: else:
self.setFocus(self.user_field) 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): def onClick(self, control):
if control == SIGN_IN: if control == SIGN_IN:
@ -106,23 +101,6 @@ class LoginManual(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close() 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): def _login(self, username, password):
try: try:

View file

@ -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()

View file

@ -21,7 +21,6 @@ ACTION_BACK = 92
ACTION_SELECT_ITEM = 7 ACTION_SELECT_ITEM = 7
ACTION_MOUSE_LEFT_CLICK = 100 ACTION_MOUSE_LEFT_CLICK = 100
USER_IMAGE = 150 USER_IMAGE = 150
USER_NAME = 151
LIST = 155 LIST = 155
CANCEL = 201 CANCEL = 201
MESSAGE_BOX = 202 MESSAGE_BOX = 202
@ -35,7 +34,6 @@ MANUAL_SERVER = 206
class ServerConnect(xbmcgui.WindowXMLDialog): class ServerConnect(xbmcgui.WindowXMLDialog):
username = ""
user_image = None user_image = None
servers = [] servers = []
@ -49,7 +47,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_args(self, **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(): for key, value in kwargs.iteritems():
setattr(self, key, value) setattr(self, key, value)
@ -77,13 +75,11 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
server_type = "wifi" if server.get('ExchangeToken') else "network" server_type = "wifi" if server.get('ExchangeToken') else "network"
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) 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: if self.user_image is not None:
self.getControl(USER_IMAGE).setImage(self.user_image) self.getControl(USER_IMAGE).setImage(self.user_image)
if not self.emby_connect: # Change connect user 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: if self.servers:
self.setFocus(self.list_) self.setFocus(self.list_)

View file

@ -24,6 +24,8 @@ CONNECT = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
HOST = 204
PORT = 205
ERROR = { ERROR = {
'Invalid': 1, 'Invalid': 1,
'Empty': 2 'Empty': 2
@ -57,19 +59,12 @@ class ServerManual(xbmcgui.WindowXMLDialog):
self.cancel_button = self.getControl(CANCEL) self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE) self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG) self.error_msg = self.getControl(ERROR_MSG)
self.host_field = self._add_editcontrol(725, 400, 40, 500) self.host_field = self.getControl(HOST)
self.port_field = self._add_editcontrol(725, 525, 40, 500) self.port_field = self.getControl(PORT)
self.port_field.setText('8096') self.port_field.setText('8096')
self.setFocus(self.host_field) 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): def onClick(self, control):
if control == CONNECT: if control == CONNECT:
@ -99,22 +94,6 @@ class ServerManual(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close() 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): def _connect_to_server(self, server, port):
server_address = "%s:%s" % (server, port) if port else server server_address = "%s:%s" % (server, port) if port else server

View file

@ -54,7 +54,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
self.list_ = self.getControl(LIST) self.list_ = self.getControl(LIST)
for user in self.users: 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')) else self._get_user_artwork(user['Id'], 'Primary'))
self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image))

View file

@ -110,7 +110,7 @@ class DownloadUtils(object):
"SetAudioStreamIndex,SetSubtitleStreamIndex," "SetAudioStreamIndex,SetSubtitleStreamIndex,"
"SetRepeatMode," "SetRepeatMode,"
"Mute,Unmute,SetVolume," "Mute,Unmute,SetVolume,"
"Play,Playstate,PlayNext" "Play,Playstate,PlayNext,PlayMediaSource"
) )
} }

View file

@ -30,12 +30,15 @@ import playbackutils as pbutils
import playutils import playutils
import api import api
from views import Playlist, VideoNodes 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__) 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)) log.info(window('emby_server%s.name' % server))
addDirectoryItem(window('emby_server%s.name' % server), "plugin://plugin.video.emby/?mode=%s" % 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(30517), "plugin://plugin.video.emby/?mode=passwords")
addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings") addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings")
addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser") addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser")
@ -123,6 +132,140 @@ def doMainListing():
xbmcplugin.endOfDirectory(int(sys.argv[1])) 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(): def emby_connect():
# Login user to emby connect # Login user to emby connect
@ -657,7 +800,7 @@ def BrowseContent(viewname, browse_type="", folderid=""):
'type': browse_type, 'type': browse_type,
'folderid': item['Id'] '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) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True)
else: #playable item, set plugin path and mediastreams else: #playable item, set plugin path and mediastreams
xbmcplugin.setContent(int(sys.argv[1]), 'episodes' if folderid else 'files') xbmcplugin.setContent(int(sys.argv[1]), 'episodes' if folderid else 'files')

View file

@ -4,6 +4,7 @@
import json import json
import logging import logging
import threading
import xbmc import xbmc
import xbmcgui import xbmcgui
@ -26,11 +27,15 @@ KODI = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
class KodiMonitor(xbmc.Monitor): class KodiMonitor(xbmc.Monitor):
retry = True retry = True
special_monitor = None
def __init__(self): def __init__(self):
xbmc.Monitor.__init__(self) xbmc.Monitor.__init__(self)
if settings('useDirectPaths') == "0":
self.special_monitor = SpecialMonitor().start()
self.download = downloadutils.DownloadUtils().downloadUrl self.download = downloadutils.DownloadUtils().downloadUrl
log.info("Kodi monitor started") log.info("Kodi monitor started")
@ -60,6 +65,11 @@ class KodiMonitor(xbmc.Monitor):
log.info("New context setting: %s", current_context) log.info("New context setting: %s", current_context)
window('emby_context', value=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() @log_error()
def onNotification(self, sender, method, data): def onNotification(self, sender, method, data):
@ -142,7 +152,7 @@ class KodiMonitor(xbmc.Monitor):
else: else:
window('emby_%s.playmethod' % playurl, value="DirectPlay") window('emby_%s.playmethod' % playurl, value="DirectPlay")
# Set properties for player.py # Set properties for player.py
playback.setProperties(playurl, listitem) playback.set_properties(playurl, listitem)
def _video_update(self, data): def _video_update(self, data):
# Manually marking as watched/unwatched # Manually marking as watched/unwatched
@ -199,3 +209,58 @@ class KodiMonitor(xbmc.Monitor):
log.info("Could not retrieve item Id") log.info("Could not retrieve item Id")
return 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

View file

@ -63,18 +63,6 @@ class KodiMovies(KodiItems):
return kodi_id return kodi_id
def add_movie(self, *args): 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 # Create the movie entry
query = ( query = (
''' '''
@ -88,17 +76,6 @@ class KodiMovies(KodiItems):
self.cursor.execute(query, (args)) self.cursor.execute(query, (args))
def update_movie(self, *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(( query = ' '.join((
"UPDATE movie", "UPDATE movie",
@ -177,8 +154,6 @@ class KodiMovies(KodiItems):
def add_countries(self, kodi_id, countries): def add_countries(self, kodi_id, countries):
if self.kodi_version > 14:
for country in countries: for country in countries:
country_id = self._get_country(country) country_id = self._get_country(country)
@ -189,36 +164,6 @@ class KodiMovies(KodiItems):
''' '''
) )
self.cursor.execute(query, (country_id, kodi_id, "movie")) self.cursor.execute(query, (country_id, kodi_id, "movie"))
else:
# TODO: Remove Helix code when Krypton is RC
for country in countries:
query = ' '.join((
"SELECT idCountry",
"FROM country",
"WHERE strCountry = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (country,))
try:
country_id = self.cursor.fetchone()[0]
except TypeError:
# Create a new entry
self.cursor.execute("select coalesce(max(idCountry),0) from country")
country_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO country(idCountry, strCountry) values(?, ?)"
self.cursor.execute(query, (country_id, country))
log.debug("Add country to media, processing: %s", country)
query = (
'''
INSERT OR REPLACE INTO countrylinkmovie(idCountry, idMovie)
VALUES (?, ?)
'''
)
self.cursor.execute(query, (country_id, kodi_id))
def _add_country(self, country): def _add_country(self, country):

View file

@ -193,52 +193,11 @@ class KodiMusic(KodiItems):
"UPDATE album", "UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", "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 = ?", "iUserrating = ?, lastScraped = ?, strReleaseType = ?",
"WHERE idAlbum = ?" "WHERE idAlbum = ?"
)) ))
self.cursor.execute(query, (args)) self.cursor.execute(query, (args))
def update_album_17(self, *args):
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iUserrating = ?, lastScraped = ?, strReleaseType = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def update_album_15(self, *args):
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def update_album_14(self, *args):
# TODO: Remove Helix code when Krypton is RC
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iRating = ?, lastScraped = ?, dateAdded = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def get_album_artist(self, album_id, artists): def get_album_artist(self, album_id, artists):
query = ' '.join(( query = ' '.join((

View file

@ -174,7 +174,6 @@ class KodiTVShows(KodiItems):
except TypeError: except TypeError:
season_id = self._add_season(show_id, number) 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 = ?" query = "UPDATE seasons SET name = ? WHERE idSeason = ?"
self.cursor.execute(query, (name, season_id)) self.cursor.execute(query, (name, season_id))
@ -189,18 +188,6 @@ class KodiTVShows(KodiItems):
return season_id return season_id
def add_episode(self, *args): 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 = ( query = (
''' '''
INSERT INTO episode( INSERT INTO episode(
@ -213,16 +200,6 @@ class KodiTVShows(KodiItems):
self.cursor.execute(query, (args)) self.cursor.execute(query, (args))
def update_episode(self, *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(( query = ' '.join((
"UPDATE episode", "UPDATE episode",

View file

@ -250,28 +250,18 @@ class Movies(Items):
if update_item: if update_item:
log.info("UPDATE movie itemid: %s - Title: %s", itemid, title) log.info("UPDATE movie itemid: %s - Title: %s", itemid, title)
# update new ratings Kodi 17 # update ratings
if self.kodi_version >= 17:
ratingid = self.kodi_db.get_ratingid(movieid) 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 # update uniqueid
if self.kodi_version >= 17:
uniqueid = self.kodi_db.get_uniqueid(movieid) uniqueid = self.kodi_db.get_uniqueid(movieid)
self.kodi_db.update_uniqueid(movieid, "movie", imdb, "imdb", uniqueid) self.kodi_db.update_uniqueid(movieid, "movie", imdb, "imdb", uniqueid)
# Update the movie entry # Update the movie entry
if self.kodi_version >= 17: self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, uniqueid,
self.kodi_db.update_movie_17(title, plot, shortplot, tagline, votecount, uniqueid,
writer, year, uniqueid, sorttitle, runtime, mpaa, genre, writer, year, uniqueid, sorttitle, runtime, mpaa, genre,
director, title, studio, trailer, country, year, director, title, studio, trailer, country, year, movieid)
movieid)
else:
self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, rating,
writer, year, imdb, sorttitle, runtime, mpaa, genre,
director, title, studio, trailer, country, movieid)
# Update the checksum in emby table # Update the checksum in emby table
emby_db.updateReference(itemid, checksum) emby_db.updateReference(itemid, checksum)
@ -280,16 +270,12 @@ class Movies(Items):
else: else:
log.info("ADD movie itemid: %s - Title: %s", itemid, title) log.info("ADD movie itemid: %s - Title: %s", itemid, title)
# add new ratings Kodi 17 # Add ratings
if self.kodi_version >= 17:
ratingid = self.kodi_db.create_entry_rating() 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 # Add uniqueid
if self.kodi_version >= 17:
uniqueid = self.kodi_db.create_entry_uniqueid() uniqueid = self.kodi_db.create_entry_uniqueid()
self.kodi_db.add_uniqueid(uniqueid, movieid, "movie", imdb, "imdb") self.kodi_db.add_uniqueid(uniqueid, movieid, "movie", imdb, "imdb")
# Add path # Add path
@ -298,16 +284,10 @@ class Movies(Items):
fileid = self.kodi_db.add_file(filename, pathid) fileid = self.kodi_db.add_file(filename, pathid)
# Create the movie entry # Create the movie entry
if self.kodi_version >= 17: self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline,
self.kodi_db.add_movie_17(movieid, fileid, title, plot, shortplot, tagline,
votecount, uniqueid, writer, year, uniqueid, sorttitle, votecount, uniqueid, writer, year, uniqueid, sorttitle,
runtime, mpaa, genre, director, title, studio, trailer, runtime, mpaa, genre, director, title, studio, trailer,
country, year) country, year)
else:
self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline,
votecount, rating, writer, year, imdb, sorttitle,
runtime, mpaa, genre, director, title, studio, trailer,
country)
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None,

View file

@ -303,22 +303,8 @@ class Music(Items):
emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum) emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum)
# Process the album info # 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, self.kodi_db.update_album(artistname, year, genre, bio, thumb, rating, lastScraped,
"album", albumid) "album", albumid)
elif self.kodi_version == 15:
# Kodi Isengard
self.kodi_db.update_album_15(artistname, year, genre, bio, thumb, rating, lastScraped,
dateadded, "album", albumid)
else:
# TODO: Remove Helix code when Krypton is RC
self.kodi_db.update_album_14(artistname, year, genre, bio, thumb, rating, lastScraped,
dateadded, albumid)
# Assign main artists to album # Assign main artists to album
for artist in item['AlbumArtists']: for artist in item['AlbumArtists']:

View file

@ -9,7 +9,7 @@ import api
import embydb_functions as embydb import embydb_functions as embydb
import _kodi_tvshows import _kodi_tvshows
from _common import Items, catch_except 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 artwork = self.artwork
API = api.API(item) API = api.API(item)
# Server api changed or something? RecursiveItemCount always returns 0 # If the show is empty, try to remove it.
"""if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'): if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'):
if item.get('Name', None) is not None: log.info("Skipping empty show: %s", item.get('Name', item['Id']))
log.info("Skipping empty show: %s", item['Name']) return self.remove(item['Id'])
return"""
# If the item already exist in the local Kodi DB we'll perform a full item update # 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 # If the item doesn't exist, we'll add it to the database
update_item = True update_item = True
@ -308,12 +308,8 @@ class TVShows(Items):
if self.emby_db.get_view_grouped_series(viewid) and tvdb: if self.emby_db.get_view_grouped_series(viewid) and tvdb:
# search kodi db for same provider id # search kodi db for same provider id
if self.kodi_version > 16:
query = "SELECT idShow FROM tvshow_view WHERE uniqueid_value = ?" query = "SELECT idShow FROM tvshow_view WHERE uniqueid_value = ?"
kodicursor.execute(query, (tvdb,)) kodicursor.execute(query, (tvdb,))
else:
query = "SELECT idShow FROM tvshow WHERE C12 = ?"
kodicursor.execute(query, (tvdb,))
try: try:
temps_showid = kodicursor.fetchall() temps_showid = kodicursor.fetchall()
@ -342,51 +338,22 @@ class TVShows(Items):
force_episodes = True 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 ##### ##### UPDATE THE TVSHOW #####
if update_item: if update_item:
log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title) log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title)
# update new ratings Kodi 17 # update ratings
if self.kodi_version > 16:
ratingid = self.kodi_db.get_ratingid("tvshow", showid) 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 # update uniqueid
if self.kodi_version > 16:
uniqueid = self.kodi_db.get_uniqueid("tvshow", showid) uniqueid = self.kodi_db.get_uniqueid("tvshow", showid)
self.kodi_db.update_uniqueid(showid, "tvshow", tvdb, "unknown", uniqueid) self.kodi_db.update_uniqueid(showid, "tvshow", tvdb, "unknown", uniqueid)
# Update the tvshow entry # Update the tvshow entry
if self.kodi_version > 16:
self.kodi_db.update_tvshow(title, plot, uniqueid, premieredate, genre, title, self.kodi_db.update_tvshow(title, plot, uniqueid, premieredate, genre, title,
uniqueid, mpaa, studio, sorttitle, showid) uniqueid, mpaa, studio, sorttitle, showid)
else:
self.kodi_db.update_tvshow(title, plot, rating, premieredate, genre, title,
tvdb, mpaa, studio, sorttitle, showid)
# Update the checksum in emby table # Update the checksum in emby table
emby_db.updateReference(itemid, checksum) emby_db.updateReference(itemid, checksum)
@ -394,16 +361,12 @@ class TVShows(Items):
else: else:
log.info("ADD tvshow itemid: %s - Title: %s", itemid, title) log.info("ADD tvshow itemid: %s - Title: %s", itemid, title)
# add new ratings Kodi 17 # add ratings
if self.kodi_version > 16:
ratingid = self.kodi_db.create_entry_rating() 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 # add uniqueid
if self.kodi_version > 16:
uniqueid = self.kodi_db.create_entry_uniqueid() uniqueid = self.kodi_db.create_entry_uniqueid()
self.kodi_db.add_uniqueid(uniqueid, showid, "tvshow", tvdb, "unknown") self.kodi_db.add_uniqueid(uniqueid, showid, "tvshow", tvdb, "unknown")
# Add top path # Add top path
@ -414,12 +377,8 @@ class TVShows(Items):
pathid = self.kodi_db.add_path(path) pathid = self.kodi_db.add_path(path)
# Create the tvshow entry # Create the tvshow entry
if self.kodi_version > 16:
self.kodi_db.add_tvshow(showid, title, plot, uniqueid, premieredate, genre, self.kodi_db.add_tvshow(showid, title, plot, uniqueid, premieredate, genre,
title, uniqueid, mpaa, studio, sorttitle) title, uniqueid, mpaa, studio, sorttitle)
else:
self.kodi_db.add_tvshow(showid, title, plot, rating, premieredate, genre,
title, tvdb, mpaa, studio, sorttitle)
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid, emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
@ -629,39 +588,24 @@ class TVShows(Items):
'dbid': episodeid, 'dbid': episodeid,
'mode': "play" 'mode': "play"
} }
filename = plugin_path(path, params) filename = urllib_path(path, params)
##### UPDATE THE EPISODE ##### ##### UPDATE THE EPISODE #####
if update_item: if update_item:
log.info("UPDATE episode itemid: %s - Title: %s", itemid, title) log.info("UPDATE episode itemid: %s - Title: %s", itemid, title)
# update new ratings Kodi 17 # update ratings
if self.kodi_version >= 17:
ratingid = self.kodi_db.get_ratingid("episode", episodeid) 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 # update uniqueid
if self.kodi_version >= 17:
uniqueid = self.kodi_db.get_uniqueid("episode", episodeid) uniqueid = self.kodi_db.get_uniqueid("episode", episodeid)
self.kodi_db.update_uniqueid(episodeid, "episode", tvdb, "tvdb", uniqueid) self.kodi_db.update_uniqueid(episodeid, "episode", tvdb, "tvdb", uniqueid)
# Update the episode entry # Update the episode entry
if self.kodi_version >= 17: self.kodi_db.update_episode(title, plot, uniqueid, writer, premieredate, runtime,
# Kodi Krypton
self.kodi_db.update_episode_16(title, plot, uniqueid, writer, premieredate, runtime,
director, season, episode, title, airsBeforeSeason, director, season, episode, title, airsBeforeSeason,
airsBeforeEpisode, seasonid, showid, episodeid) 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)
# Update the checksum in emby table # Update the checksum in emby table
emby_db.updateReference(itemid, checksum) emby_db.updateReference(itemid, checksum)
@ -672,16 +616,12 @@ class TVShows(Items):
else: else:
log.info("ADD episode itemid: %s - Title: %s", itemid, title) log.info("ADD episode itemid: %s - Title: %s", itemid, title)
# add new ratings Kodi 17 # add ratings
if self.kodi_version >= 17:
ratingid = self.kodi_db.create_entry_rating() 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 # add uniqueid
if self.kodi_version >= 17:
uniqueid = self.kodi_db.create_entry_uniqueid() uniqueid = self.kodi_db.create_entry_uniqueid()
self.kodi_db.add_uniqueid(uniqueid, episodeid, "episode", tvdb, "tvdb") self.kodi_db.add_uniqueid(uniqueid, episodeid, "episode", tvdb, "tvdb")
# Add path # Add path
@ -690,20 +630,9 @@ class TVShows(Items):
fileid = self.kodi_db.add_file(filename, pathid) fileid = self.kodi_db.add_file(filename, pathid)
# Create the episode entry # Create the episode entry
if self.kodi_version >= 17: self.kodi_db.add_episode(episodeid, fileid, title, plot, uniqueid, writer,
# Kodi Krypton
self.kodi_db.add_episode_16(episodeid, fileid, title, plot, uniqueid, writer,
premieredate, runtime, director, season, episode, title, premieredate, runtime, director, season, episode, title,
showid, airsBeforeSeason, airsBeforeEpisode, seasonid) 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)
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid, emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid,

View file

@ -21,6 +21,8 @@ import playutils as putils
import playlist import playlist
import read_embyserver as embyserver import read_embyserver as embyserver
import shutil import shutil
import embydb_functions as embydb
from database import DatabaseConn
from utils import window, settings, language as lang from utils import window, settings, language as lang
################################################################################################# #################################################################################################
@ -30,329 +32,147 @@ log = logging.getLogger("EMBY."+__name__)
################################################################################################# #################################################################################################
class PlaybackUtils(): class PlaybackUtils(object):
def __init__(self, item): def __init__(self, item=None, item_id=None):
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)
self.artwork = artwork.Artwork() self.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer() 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() listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(self.item)
log.info("Play called.") log.info("Play called: %s", self.item['Name'])
playurl = playutils.getPlayUrl()
if not playurl: 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) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
if dbid is None: seektime = 0 if resume == "true" else self.API.adjust_resume(self.API.get_userdata()['Resume'])
# Item is not in Kodi database
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
# TODO: Review once Krypton is RC, no need for workaround. if force_transcode:
log.info("Clear the playlist.")
self.playlist.clear()
############### ORGANIZE CURRENT PLAYLIST ################ self.set_playlist(play_url, item_id, listitem, seektime, dbid)
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') ##### SETUP PLAYBACK
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
startPos = max(playlist.getposition(), 0) # Can return -1
sizePlaylist = playlist.size()
currentPosition = startPos
propertiesPlayback = window('emby_playbackProps') == "true" ''' To get everything to work together, play the first item in the stack with setResolvedUrl,
introsPlaylist = False add the rest to the regular playlist.
dummyPlaylist = False '''
log.debug("Playlist start position: %s" % startPos) index = max(self.playlist.getposition(), 0) + 1 # Can return -1
log.debug("Playlist plugin position: %s" % currentPosition) force_play = False
log.debug("Playlist size: %s" % sizePlaylist)
############### RESUME POINT ################ # Stack: [(url, listitem), (url, ...), ...]
self.stack[0][1].setPath(self.stack[0][0])
try:
#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
userdata = self.API.get_userdata() xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, self.stack[0][1])
seektime = self.API.adjust_resume(userdata['Resume']) 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))
# We need to ensure we add the intro and additional parts only once. for stack in self.stack:
# Otherwise we get a loop. self.playlist.add(url=stack[0], listitem=stack[1], index=index)
if not propertiesPlayback: index += 1
window('emby_playbackProps', value="true") if force_play:
log.info("Setting up properties in playlist.") xbmc.Player().play(self.playlist)
if not homeScreen and not seektime and window('emby_customPlaylist') != "true": def set_playlist(self, play_url, item_id, listitem, seektime=None, db_id=None):
log.debug("Adding dummy file to playlist.") ##### CHECK FOR INTROS
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 settings('enableCinema') == "true" and not seektime:
# if we have any play them when the movie/show is not being resumed self._set_intros(item_id)
url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
intros = self.doUtils(url)
if intros['TotalRecordCount'] != 0: ##### ADD MAIN ITEM
getTrailers = True
self.set_properties(play_url, listitem)
self.set_listitem(listitem, db_id)
self.stack.append([play_url, listitem])
##### ADD ADDITIONAL PARTS
if self.item.get('PartCount'):
self._set_additional_parts(item_id)
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)
if intros['Items']:
enabled = True
if settings('askCinema') == "true": if settings('askCinema') == "true":
resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016)) resp = xbmcgui.Dialog().yesno("Emby for Kodi", lang(33016))
if not resp: if not resp:
# User selected to not play trailers # User selected to not play trailers
getTrailers = False enabled = False
log.info("Skip trailers.") log.info("Skip trailers.")
if getTrailers: if enabled:
for intro in intros['Items']: 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 listitem = xbmcgui.ListItem()
pbutils = PlaybackUtils(intro) url = putils.PlayUtils(intro, listitem).get_play_url()
pbutils.setProperties(introPlayurl, introListItem) log.info("Adding Intro: %s" % url)
self.pl.insert_to_playlist(currentPosition, url=introPlayurl) self.stack.append([url, listitem])
introsPlaylist = True
currentPosition += 1
def _set_additional_parts(self, item_id):
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### parts = self.emby.get_additional_parts(item_id)
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']: for part in parts['Items']:
additionalListItem = xbmcgui.ListItem() listitem = xbmcgui.ListItem()
additionalPlayurl = putils.PlayUtils(part).getPlayUrl() url = putils.PlayUtils(part, listitem).get_play_url()
log.info("Adding additional part: %s" % partcount) log.info("Adding additional part: %s" % url)
# Set listitem and properties for each additional parts # Set listitem and properties for each additional parts
pbutils = PlaybackUtils(part) pb = PlaybackUtils(part)
pbutils.setProperties(additionalPlayurl, additionalListItem) pb.set_properties(url, listitem)
pbutils.setArtwork(additionalListItem) pb.setArtwork(listitem)
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) self.stack.append([url, listitem])
self.pl.verify_playlist()
currentPosition += 1
if dummyPlaylist: def set_listitem(self, listitem, dbid=None):
# 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']
try:
mediastreams = self.item['MediaSources'][0]['MediaStreams']
except (TypeError, KeyError, IndexError):
return
temp = xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8')
kodiindex = 0
for stream in mediastreams:
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']):
# Direct stream
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.%s"
% (self.server, itemid, itemid, index, stream['Codec']))
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)
return externalsubs
def _download_external_subs(self, src, dst, filename):
if not xbmcvfs.exists(dst):
xbmcvfs.mkdir(dst)
path = os.path.join(dst, filename)
try:
response = requests.get(src, stream=True)
response.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
return path
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 = {
'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:
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])
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})
def setListItem(self, listItem, dbid=None):
people = self.API.get_people() people = self.API.get_people()
studios = self.API.get_studios() mediatype = self.item['Type']
metadata = { metadata = {
'title': self.item.get('Name', "Missing name"), 'title': self.item.get('Name', "Missing name"),
'year': self.item.get('ProductionYear'), 'year': self.item.get('ProductionYear'),
'plot': self.API.get_overview(), 'plot': self.API.get_overview(),
@ -360,33 +180,151 @@ class PlaybackUtils():
'writer': people.get('Writer'), 'writer': people.get('Writer'),
'mpaa': self.API.get_mpaa(), 'mpaa': self.API.get_mpaa(),
'genre': " / ".join(self.item['Genres']), 'genre': " / ".join(self.item['Genres']),
'studio': " / ".join(studios), 'studio': " / ".join(self.API.get_studios()),
'aired': self.API.get_premiere_date(), 'aired': self.API.get_premiere_date(),
'rating': self.item.get('CommunityRating'), 'rating': self.item.get('CommunityRating'),
'votes': self.item.get('VoteCount') 'votes': self.item.get('VoteCount')
} }
if "Episode" in self.item['Type']: if mediatype == "Episode":
# Only for tv shows # Only for tv shows
# For Kodi Krypton
metadata['mediatype'] = "episode" 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') elif mediatype == "Movie":
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
metadata['mediatype'] = "movie" metadata['mediatype'] = "movie"
elif mediatype == "MusicVideo":
metadata['mediatype'] = "musicvideo"
elif mediatype == "Audio":
metadata['mediatype'] = "song"
if dbid:
metadata['dbid'] = dbid metadata['dbid'] = dbid
listItem.setProperty('IsPlayable', 'true') listitem.setProperty('IsPlayable', 'true')
listItem.setProperty('IsFolder', 'false') listitem.setProperty('IsFolder', 'false')
listItem.setLabel(metadata['title']) listitem.setLabel(metadata['title'])
listItem.setInfo('video', infoLabels=metadata) 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

View file

@ -12,7 +12,7 @@ import xbmcgui
import clientinfo import clientinfo
import downloadutils import downloadutils
import websocket_client as wsc 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 from ga_client import GoogleAnalytics, log_error
################################################################################################# #################################################################################################
@ -43,6 +43,39 @@ class Player(xbmc.Player):
log.debug("Starting playback monitor.") log.debug("Starting playback monitor.")
xbmc.Player.__init__(self) 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() @log_error()
def onPlayBackStarted(self): def onPlayBackStarted(self):
# Will be called when xbmc starts playing a file # Will be called when xbmc starts playing a file
@ -74,28 +107,31 @@ class Player(xbmc.Player):
self.currentFile = currentFile self.currentFile = currentFile
# We may need to wait for info to be set in kodi monitor # 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 tryCount = 0
while not itemId: while not item:
xbmc.sleep(200) 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 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 break
else: tryCount += 1 else: tryCount += 1
else: 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. # Only proceed if an itemId was found.
embyitem = "emby_%s" % currentFile runtime = item.get('runtime')
runtime = window("%s.runtime" % embyitem) refresh_id = item.get('refreshid')
refresh_id = window("%s.refreshid" % embyitem) play_method = item.get('playmethod')
playMethod = window("%s.playmethod" % embyitem) item_type = item.get('type')
itemType = window("%s.type" % embyitem)
window('emby_skipWatched%s' % itemId, value="true")
#self.set_audio_subs(item.get('forcedaudio'), item.get('forcedsubs'))
window('emby_skipWatched%s' % item_id, value="true")
customseek = window('emby_customPlaylist.seektime') customseek = window('emby_customPlaylist.seektime')
if window('emby_customPlaylist') == "true" and customseek: if window('emby_customPlaylist') == "true" and customseek:
# Start at, when using custom playlist (play to Kodi from webclient) # Start at, when using custom playlist (play to Kodi from webclient)
@ -110,18 +146,7 @@ class Player(xbmc.Player):
return return
# Get playback volume # Get playback volume
volume_query = { result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]})
"jsonrpc": "2.0",
"id": 1,
"method": "Application.GetProperties",
"params": {
"properties": ["volume", "muted"]
}
}
result = xbmc.executeJSONRPC(json.dumps(volume_query))
result = json.loads(result)
result = result.get('result') result = result.get('result')
volume = result.get('volume') volume = result.get('volume')
@ -133,33 +158,26 @@ class Player(xbmc.Player):
'QueueableMediaTypes': "Video", 'QueueableMediaTypes': "Video",
'CanSeek': True, 'CanSeek': True,
'ItemId': itemId, 'ItemId': item_id,
'MediaSourceId': itemId, 'MediaSourceId': item_id,
'PlayMethod': playMethod, 'PlayMethod': play_method,
'VolumeLevel': volume, 'VolumeLevel': volume,
'PositionTicks': int(seekTime * 10000000), 'PositionTicks': int(seekTime * 10000000),
'IsMuted': muted 'IsMuted': muted
} }
# Get the current audio track and subtitles # Get the current audio track and subtitles
if playMethod == "Transcode": if play_method == "Transcode":
# property set in PlayUtils.py # property set in PlayUtils.py
postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile) postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile) postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
else: else:
# Get the current kodi audio and subtitles and convert to Emby equivalent # Get the current kodi audio and subtitles and convert to Emby equivalent
tracks_query = { params = {
'playerid': 1,
"jsonrpc": "2.0", 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"]
"id": 1,
"method": "Player.GetProperties",
"params": {
"playerid": 1,
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
} }
} result = JSONRPC('Player.GetProperties').execute(params)
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
tracks_data = None tracks_data = None
try: try:
tracks_data = json.loads(result) tracks_data = json.loads(result)
@ -190,7 +208,7 @@ class Player(xbmc.Player):
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = window("%s.indexMapping" % embyitem) mapping = window("emby_%s.indexMapping" % currentFile)
if mapping: # Set in playbackutils.py if mapping: # Set in playbackutils.py
@ -230,13 +248,13 @@ class Player(xbmc.Player):
data = { data = {
'runtime': runtime, 'runtime': runtime,
'item_id': itemId, 'item_id': item_id,
'refresh_id': refresh_id, 'refresh_id': refresh_id,
'currentfile': currentFile, 'currentfile': currentFile,
'AudioStreamIndex': postdata['AudioStreamIndex'], 'AudioStreamIndex': postdata['AudioStreamIndex'],
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
'playmethod': playMethod, 'playmethod': play_method,
'Type': itemType, 'Type': item_type,
'currentPosition': int(seekTime) 'currentPosition': int(seekTime)
} }
@ -244,8 +262,8 @@ class Player(xbmc.Player):
log.info("ADDING_FILE: %s" % self.played_info) log.info("ADDING_FILE: %s" % self.played_info)
ga = GoogleAnalytics() ga = GoogleAnalytics()
ga.sendEventData("PlayAction", itemType, playMethod) ga.sendEventData("PlayAction", item_type, play_method)
ga.sendScreenView(itemType) ga.sendScreenView(item_type)
def reportPlayback(self): def reportPlayback(self):
@ -267,18 +285,7 @@ class Player(xbmc.Player):
# Get playback volume # Get playback volume
volume_query = { result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]})
"jsonrpc": "2.0",
"id": 1,
"method": "Application.GetProperties",
"params": {
"properties": ["volume", "muted"]
}
}
result = xbmc.executeJSONRPC(json.dumps(volume_query))
result = json.loads(result)
result = result.get('result') result = result.get('result')
volume = result.get('volume') volume = result.get('volume')
@ -305,19 +312,11 @@ class Player(xbmc.Player):
else: else:
# Get current audio and subtitles track # Get current audio and subtitles track
tracks_query = { params = {
'playerid': 1,
"jsonrpc": "2.0", 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"]
"id": 1,
"method": "Player.GetProperties",
"params": {
"playerid": 1,
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
} }
} result = JSONRPC('Player.GetProperties').execute(params)
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
result = json.loads(result)
result = result.get('result') result = result.get('result')
try: # Audio tracks try: # Audio tracks
@ -413,10 +412,15 @@ class Player(xbmc.Player):
def onPlayBackStopped(self): def onPlayBackStopped(self):
# Will be called when user stops xbmc playing a file # Will be called when user stops xbmc playing a file
log.debug("ONPLAYBACK_STOPPED") log.debug("ONPLAYBACK_STOPPED")
window('emby_customPlaylist', clear=True)
window('emby_customPlaylist.seektime', 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() self.stopAll()
@log_error() @log_error()
@ -456,10 +460,16 @@ class Player(xbmc.Player):
if currentPosition and runtime: if currentPosition and runtime:
try: try:
if window('emby.external'):
window('emby.external', clear=True)
raise ValueError
percentComplete = (currentPosition * 10000000) / int(runtime) percentComplete = (currentPosition * 10000000) / int(runtime)
except ZeroDivisionError: except ZeroDivisionError:
# Runtime is 0. # Runtime is 0.
percentComplete = 0 percentComplete = 0
except ValueError:
percentComplete = 100
markPlayedAt = float(settings('markPlayed')) / 100 markPlayedAt = float(settings('markPlayed')) / 100
log.info("Percent complete: %s Mark played at: %s" log.info("Percent complete: %s Mark played at: %s"
@ -486,6 +496,8 @@ class Player(xbmc.Player):
else: else:
log.info("User skipped deletion.") log.info("User skipped deletion.")
window('emby.external_check', clear=True)
# Stop transcoding # Stop transcoding
if playMethod == "Transcode": if playMethod == "Transcode":
log.info("Transcoding for %s terminated." % itemid) log.info("Transcoding for %s terminated." % itemid)

View file

@ -145,7 +145,7 @@ class Playlist(object):
@classmethod @classmethod
def verify_playlist(cls): def verify_playlist(cls):
log.debug(JSONRPC('Playlist.GetItems').execute({'playlistid': 1})) log.info(JSONRPC('Playlist.GetItems').execute({'playlistid': 1}))
@classmethod @classmethod
def remove_from_playlist(cls, position): def remove_from_playlist(cls, position):
@ -156,3 +156,171 @@ class Playlist(object):
'position': position 'position': position
} }
log.debug(JSONRPC('Playlist.Remove').execute(params)) 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))
'''

View file

@ -2,8 +2,8 @@
################################################################################################# #################################################################################################
import collections
import logging import logging
import sys
import urllib import urllib
import xbmc import xbmc
@ -13,7 +13,7 @@ import xbmcvfs
import clientinfo import clientinfo
import downloadutils import downloadutils
import read_embyserver as embyserver 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(): class PlayUtils():
method = "DirectPlay"
force_transcode = False
def __init__(self, item):
def __init__(self, item, listitem, **kwargs):
self.info = kwargs
self.item = item self.item = item
self.listitem = listitem
self.clientInfo = clientinfo.ClientInfo() self.clientInfo = clientinfo.ClientInfo()
self.emby = embyserver.Read_EmbyServer() self.emby = embyserver.Read_EmbyServer()
self.userid = window('emby_currUser') self.server = window('emby_server%s' % window('emby_currUser'))
self.server = window('emby_server%s' % self.userid)
self.doUtils = downloadutils.DownloadUtils().downloadUrl def get_play_url(self, force_transcode=False):
def getPlayUrlNew(self): ''' New style to retrieve the best playback method based on sending
the profile to the server. Based on capabilities the correct path is returned,
including livestreams that need to be opened by the server
''' '''
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 self.force_transcode = force_transcode
TODO: Close livestream if needed (RequiresClosing in livestream source) 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:
log.info("No media source selected.")
return False
return self.get_optimal_source(selected_source)
def get_optimal_source(self, source):
''' 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.
''' '''
playurl = None
pbinfo = self.getPlaybackInfo()
if pbinfo:
xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo))
if pbinfo["Protocol"] == "SupportsDirectPlay": if (not self.force_transcode and self.is_h265(source) or self.is_strm(source) or
playmethod = "DirectPlay" (source['SupportsDirectPlay'] and settings('playFromStream') == "false" and self.is_file_exists(source))):
elif pbinfo["Protocol"] == "SupportsDirectStream": # Do nothing, path is updated with our verification if applies.
playmethod = "DirectStream" pass
elif pbinfo.get('LiveStreamId'):
playmethod = "LiveStream"
else: else:
playmethod = "Transcode" source['Path'] = self.get_http_path(source, self.force_transcode or source['SupportsDirectStream'] == False)
playurl = pbinfo["Path"] log.debug('get source: %s', source)
xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl)) return source
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 is_file_exists(self, source):
path = self.get_direct_path(source)
def getPlayUrl(self): if xbmcvfs.exists(path) or ":" not in path:
log.info("Path exists or assumed linux or web.")
# log filename, used by other addons eg subtitles which require the file name self.method = "DirectPlay"
try: source['Path'] = path
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:
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
# Verify screen resolution
if self.resolutionConflict():
log.info("Can't direct play, resolution limit is enabled")
return False
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()
return True return True
def directPlay(self):
try:
playurl = self.item['MediaSources'][0]['Path']
except (IndexError, KeyError):
playurl = self.item['Path']
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 "apple.com" in playurl:
USER_AGENT = "QuickTime/7.7.4"
playurl += "?|User-Agent=%s" % USER_AGENT
# Strm
if playurl.endswith('.strm'):
playurl = urllib.urlencode(playurl)
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.") log.info("Failed to find file.")
return False return False
def isDirectStream(self): def is_strm(self, source):
videotrack = self.item['MediaSources'][0]['Name'] if source['Container'] == "strm":
transcodeH265 = settings('transcodeH265') log.info('Strm detected.')
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: self.method = "DirectPlay"
return False source['Path'] = self.get_direct_path(source)
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 return True
def directStream(self): return False
if 'Path' in self.item and self.item['Path'].endswith('.strm'): def is_h265(self, source):
# Allow strm loading when direct streaming
playurl = self.directPlay() if source['MediaStreams']:
elif self.item['Type'] == "Audio": force_transcode = False
playurl = "%s/emby/Audio/%s/stream.mp3?" % (self.server, self.item['Id'])
else: for stream in source['MediaStreams']:
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id']) 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 path.startswith('\\\\'):
path = path.replace('\\\\', "smb://")
path = path.replace('\\', "/")
return path
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(',')
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() user_token = downloadutils.DownloadUtils().get_token()
playurl += "&api_key=" + str(user_token) play_url += "&api_key=" + user_token
return playurl
def isNetworkSufficient(self): return play_url
settings = self.getBitrate()*1000 def get_direct_url(self, source):
try: self.method = "DirectStream"
sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
except (KeyError, TypeError): if self.item['Type'] == "Audio":
log.info("Bitrate value is missing.") play_url = "%s/emby/Audio/%s/stream.%s?static=true" % (self.server, self.item['Id'], self.item['MediaSources'][0]['Container'])
else: else:
log.info("The add-on settings bitrate is: %s, the video bitrate required is: %s" play_url = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id'])
% (settings, sourceBitrate))
if settings < sourceBitrate:
return False
return True # Append external subtitles
if settings('enableExternalSubs') == "true":
self.set_external_subs(source, play_url)
def isTranscoding(self): return play_url
# Make sure the server supports it
if not self.item['MediaSources'][0]['SupportsTranscoding']:
return False
return True def get_transcode_url(self, source):
def transcoding(self): self.method = "Transcode"
if 'Path' in self.item and self.item['Path'].endswith('.strm'): item_id = self.item['Id']
# Allow strm loading when transcoding play_url = urllib_path("%s/emby/Videos/%s/master.m3u8" % (self.server, item_id), {
playurl = self.directPlay() 'MediaSourceId': source['Id'],
else: 'VideoCodec': "h264",
itemid = self.item['Id'] 'AudioCodec': "ac3",
deviceId = self.clientInfo.get_device_id() 'MaxAudioChannels': 6,
playurl = ( 'deviceId': self.clientInfo.get_device_id(),
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s" 'VideoBitrate': self.get_bitrate() * 1000
% (self.server, itemid, itemid) })
)
playurl = (
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
% (playurl, deviceId, self.getBitrate()*1000))
# Limit to 8 bit if user selected transcode Hi10P # Limit to 8 bit if user selected transcode Hi10P
transcodeHi10P = settings('transcodeHi10P') if settings('transcodeHi10P') == "true":
if transcodeHi10P == "true": play_url += "&MaxVideoBitDepth=8"
playurl = "%s&MaxVideoBitDepth=8" % playurl
# Adjust video resolution # Adjust the video resolution
if self.resolutionConflict(): play_url += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
screenRes = self.getScreenResolution() # Select audio and subtitles
playurl = "%s&maxWidth=%s&maxHeight=%s" % (playurl, screenRes['width'], screenRes['height']) play_url += self.get_audio_subs(source)
user_token = downloadutils.DownloadUtils().get_token() return play_url
playurl += "&api_key=" + str(user_token)
return playurl def set_external_subs(self, source, play_url):
def getBitrate(self): 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:
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
return path
def get_audio_subs(self, source):
''' For transcoding only
Present the list of audio/subs to select from, before playback starts.
Returns part of the url to append.
'''
prefs = ""
streams = source['MediaStreams']
audio_streams = collections.OrderedDict()
subs_streams = collections.OrderedDict()
if streams:
''' Since Emby returns all possible tracks together, sort them.
IsTextSubtitleStream if true, is available to download from server.
'''
for stream in streams:
index = stream['Index']
stream_type = stream['Type']
if stream_type == "Audio":
codec = stream['Codec']
channel = stream.get('ChannelLayout', "")
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 # get the addon video quality
bitrate = { bitrate = {
'0': 664, '0': 664,
@ -381,202 +448,14 @@ class PlayUtils():
'17': 100000, '17': 100000,
'18': 1000000 '18': 1000000
} }
# max bit rate supported by server (max signed 32bit integer) # max bit rate supported by server (max signed 32bit integer)
return bitrate.get(settings('videoBitrate'), 2147483) return bitrate.get(settings('videoBitrate'), 2147483)
def audioSubsPref(self, url, listitem): def get_device_profile(self):
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):
return { return {
"Name": "Kodi", "Name": "Kodi",
"MaxStreamingBitrate": self.getBitrate()*1000, "MaxStreamingBitrate": self.get_bitrate() * 1000,
"MusicStreamingTranscodingBitrate": 1280000, "MusicStreamingTranscodingBitrate": 1280000,
"TimelineOffsetSeconds": 5, "TimelineOffsetSeconds": 5,
@ -679,29 +558,7 @@ class PlayUtils():
] ]
} }
def resolutionConflict(self): def get_resolution(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
window = xbmcgui.Window()
return window.getWidth(), window.getHeight()

View file

@ -151,7 +151,7 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount" "MediaSources,VoteCount,ItemCounts"
) )
} }
queue.put({'url': url, 'params': params}) queue.put({'url': url, 'params': params})
@ -182,7 +182,7 @@ class Read_EmbyServer():
"CommunityRating,OfficialRating,CumulativeRunTimeTicks," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "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) return self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
@ -198,7 +198,7 @@ class Read_EmbyServer():
"CommunityRating,OfficialRating,CumulativeRunTimeTicks," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "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" url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json"
@ -219,7 +219,7 @@ class Read_EmbyServer():
"CommunityRating,OfficialRating,CumulativeRunTimeTicks," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "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" url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
@ -285,7 +285,7 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount" "MediaSources,VoteCount,ItemCounts"
) )
queue.put({'url': url, 'params': params}) queue.put({'url': url, 'params': params})
if not self._add_worker_thread(queue, items['Items']): if not self._add_worker_thread(queue, items['Items']):
@ -472,7 +472,7 @@ class Read_EmbyServer():
"Etag,Genres,SortName,Studios,Writer,ProductionYear," "Etag,Genres,SortName,Studios,Writer,ProductionYear,"
"CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore,"
"AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview,ItemCounts"
) )
} }
queue.put({'url': url, 'params': params}) queue.put({'url': url, 'params': params})
@ -614,7 +614,7 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount" "MediaSources,VoteCount,ItemCounts"
) )
}) })
return params return params
@ -649,6 +649,39 @@ class Read_EmbyServer():
return library['LibraryOptions'] return library['LibraryOptions']
def get_server_transcoding_settings(self): 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') def get_intros(self, item_id):
return self.doUtils.downloadUrl(url) 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
})

View file

@ -10,6 +10,7 @@ from datetime import datetime
import platform import platform
import xbmc import xbmc
import xbmcgui
import userclient import userclient
import clientinfo import clientinfo
@ -52,10 +53,11 @@ class Service(object):
self.addon_name = self.client_info.get_addon_name() self.addon_name = self.client_info.get_addon_name()
log_level = settings('logLevel') log_level = settings('logLevel')
# General settings which are used by other entrypoints
window('emby_logLevel', value=str(log_level)) window('emby_logLevel', value=str(log_level))
window('emby_kodiProfile', value=xbmc.translatePath('special://profile')) window('emby_kodiProfile', value=xbmc.translatePath('special://profile'))
context_menu = "true" if settings('enableContext') == "true" else "" window('emby_context', value="true" if settings('enableContext') == "true" else "")
window('emby_context', value=context_menu) window('emby_context_transcode', value="true" if settings('enableContextTranscode') == "true" else "")
# Initial logging # Initial logging
log.warn("======== START %s ========", self.addon_name) log.warn("======== START %s ========", self.addon_name)
@ -72,7 +74,8 @@ class Service(object):
"emby_online", "emby_state.json", "emby_serverStatus", "emby_onWake", "emby_online", "emby_state.json", "emby_serverStatus", "emby_onWake",
"emby_syncRunning", "emby_dbCheck", "emby_kodiScan", "emby_syncRunning", "emby_dbCheck", "emby_kodiScan",
"emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId", "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: for prop in properties:
window(prop, clear=True) window(prop, clear=True)
@ -319,6 +322,9 @@ class Service(object):
#ga = GoogleAnalytics() #ga = GoogleAnalytics()
#ga.sendEventData("Application", "Shutdown") #ga.sendEventData("Application", "Shutdown")
if self.monitor.special_monitor:
self.monitor.special_monitor.stop_monitor()
if self.userclient_running: if self.userclient_running:
self.userclient_thread.stop_client() self.userclient_thread.stop_client()

View file

@ -313,7 +313,7 @@ class UserClient(threading.Thread):
log.info("Username found: %s", username) log.info("Username found: %s", username)
self._auth = True self._auth = True
if monitor.waitForAbort(1): if monitor.waitForAbort(2):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
break break

View file

@ -78,7 +78,7 @@ def dialog(type_, *args, **kwargs):
} }
return types[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)) return "%s?%s" % (plugin, urllib.urlencode(params))

View file

@ -14,7 +14,7 @@ import xbmcvfs
import read_embyserver as embyserver import read_embyserver as embyserver
import embydb_functions as embydb 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", 'mode': "browsecontent",
'type': mediatype 'type': mediatype
} }
path = plugin_path("plugin://plugin.video.emby/", params) path = urllib_path("plugin://plugin.video.emby/", params)
elif (mediatype == "homevideos" or mediatype == "photos"): elif (mediatype == "homevideos" or mediatype == "photos"):
params = { params = {
@ -651,7 +651,7 @@ class VideoNodes(object):
'type': mediatype, 'type': mediatype,
'folderid': nodetype 'folderid': nodetype
} }
path = plugin_path("plugin://plugin.video.emby/", params) path = urllib_path("plugin://plugin.video.emby/", params)
elif nodetype == "nextepisodes": elif nodetype == "nextepisodes":
params = { params = {
@ -660,7 +660,7 @@ class VideoNodes(object):
'mode': "nextup", 'mode': "nextup",
'limit': 25 'limit': 25
} }
path = plugin_path("plugin://plugin.video.emby/", params) path = urllib_path("plugin://plugin.video.emby/", params)
elif KODI == 14 and nodetype == "recentepisodes": elif KODI == 14 and nodetype == "recentepisodes":
params = { params = {
@ -669,7 +669,7 @@ class VideoNodes(object):
'mode': "recentepisodes", 'mode': "recentepisodes",
'limit': 25 'limit': 25
} }
path = plugin_path("plugin://plugin.video.emby/", params) path = urllib_path("plugin://plugin.video.emby/", params)
elif KODI == 14 and nodetype == "inprogressepisodes": elif KODI == 14 and nodetype == "inprogressepisodes":
params = { params = {
@ -678,7 +678,7 @@ class VideoNodes(object):
'mode': "inprogressepisodes", 'mode': "inprogressepisodes",
'limit': 25 'limit': 25
} }
path = plugin_path("plugin://plugin.video.emby/", params) path = urllib_path("plugin://plugin.video.emby/", params)
else: else:
path = "library://video/emby/%s/%s.xml" % (viewid, nodetype) path = "library://video/emby/%s/%s.xml" % (viewid, nodetype)

View file

@ -14,6 +14,7 @@ import downloadutils
import librarysync import librarysync
import playlist import playlist
import userclient import userclient
import playbackutils
from utils import window, settings, dialog, language as lang, JSONRPC from utils import window, settings, dialog, language as lang, JSONRPC
from ga_client import log_error from ga_client import log_error
@ -112,14 +113,26 @@ class WebSocketClient(threading.Thread):
@classmethod @classmethod
def _play(cls, data): def _play(cls, data):
''' {"Id":"e2c106a953bfc0d9c191a49cda6561de",
"ItemIds":["52f0c57e2133e1c11d36c59edcd835fc"],
"PlayCommand":"PlayNow","ControllingUserId":"d40000000000000000000000000000000",
"SubtitleStreamIndex":3,"AudioStreamIndex":1,
"MediaSourceId":"ba83c549ac5c0e4180ae33ebdf813c51"}}
'''
item_ids = data['ItemIds'] item_ids = data['ItemIds']
kwargs = {
'SubtitleStreamIndex': data.get('SubtitleStreamIndex'),
'AudioStreamIndex': data.get('AudioStreamIndex'),
'MediaSourceId': data.get('MediaSourceId')
}
command = data['PlayCommand'] command = data['PlayCommand']
playlist_ = playlist.Playlist() playlist_ = playlist.Playlist()
if command == 'PlayNow': if command == 'PlayNow':
startat = data.get('StartPositionTicks', 0) if playbackutils.PlaybackUtils(None, item_ids[0]).play_all(item_ids, data.get('StartPositionTicks', 0), **kwargs):
playlist_.play_all(item_ids, startat)
dialog(type_="notification", dialog(type_="notification",
heading="{emby}", heading="{emby}",
message="%s %s" % (len(item_ids), lang(33004)), message="%s %s" % (len(item_ids), lang(33004)),
@ -214,7 +227,7 @@ class WebSocketClient(threading.Thread):
elif command == 'SetSubtitleStreamIndex': elif command == 'SetSubtitleStreamIndex':
emby_index = int(arguments['Index']) emby_index = int(arguments['Index'])
current_file = player.getPlayingFile() current_file = player.getPlayingFile()
mapping = window('emby_%s.indexMapping' % current_file) mapping = window('emby_%s.indexMapping.json' % current_file)
if emby_index == -1: if emby_index == -1:
player.showSubtitles(False) player.showSubtitles(False)

View file

@ -49,13 +49,17 @@
<setting id="deleteTV" type="bool" label="30115" visible="eq(-1,true)" default="false" subsetting="true" /> <setting id="deleteTV" type="bool" label="30115" visible="eq(-1,true)" default="false" subsetting="true" />
<setting id="deleteMovies" type="bool" label="30116" visible="eq(-2,true)" default="false" subsetting="true" /> <setting id="deleteMovies" type="bool" label="30116" visible="eq(-2,true)" default="false" subsetting="true" />
<setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" /> <setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" />
<setting label="HTTP Playback (Direct play or transcode)" type="lsep"/>
<setting type="sep" /> <setting type="sep" />
<setting id="playFromStream" type="bool" label="30002" default="true" /> <setting id="playFromStream" type="bool" label="30002" default="true" />
<setting id="videoBitrate" type="enum" label="30160" values="664 Kbps SD|996 Kbps HD|1.3 Mbps HD|2.0 Mbps HD|3.2 Mbps HD|4.7 Mbps HD|6.2 Mbps HD|7.7 Mbps HD|9.2 Mbps HD|10.7 Mbps HD|12.2 Mbps HD|13.7 Mbps HD|15.2 Mbps HD|16.7 Mbps HD|18.2 Mbps HD|20.0 Mbps HD|25.0 Mbps HD|30.0 Mbps HD|35.0 Mbps HD|40.0 Mbps HD|100.0 Mbps HD [default]|1000.0 Mbps HD" visible="true" default="20" subsetting="true" /> <setting id="videoBitrate" type="enum" label="30160" values="664 Kbps SD|996 Kbps HD|1.3 Mbps HD|2.0 Mbps HD|3.2 Mbps HD|4.7 Mbps HD|6.2 Mbps HD|7.7 Mbps HD|9.2 Mbps HD|10.7 Mbps HD|12.2 Mbps HD|13.7 Mbps HD|15.2 Mbps HD|16.7 Mbps HD|18.2 Mbps HD|20.0 Mbps HD|25.0 Mbps HD|30.0 Mbps HD|35.0 Mbps HD|40.0 Mbps HD|100.0 Mbps HD [default]|1000.0 Mbps HD" visible="true" default="20" />
<setting id="enableExternalSubs" type="bool" label="Enable external subs for direct stream" default="true" /> <setting label="Direct play (HTTP)" type="lsep"/>
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled|480p(and higher)|720p(and higher)|1080p" /> <setting id="enableExternalSubs" type="bool" label="Enable external subtitles" default="true" />
<setting label="Transcode" type="lsep"/>
<setting id="skipDialogTranscode" type="enum" label="Enable audio/subtitles selection" values="Enabled|Audio only|Subtitles only|Disabled" visible="true" default="0" />
<setting id="ignoreTranscode" type="text" label="Never transcode codecs (i.e. h264,h265,...)" visible="true" />
<setting id="transcode_h265" type="bool" label="30522" default="false" />
<setting id="transcodeHi10P" type="bool" label="30537" default="false"/> <setting id="transcodeHi10P" type="bool" label="30537" default="false"/>
<setting id="limitResolution" type="bool" label="33096" default="false"/>
<setting id="markPlayed" type="number" visible="false" default="90" /> <setting id="markPlayed" type="number" visible="false" default="90" />
<setting id="failedCount" type="number" visible="false" default="0" /> <setting id="failedCount" type="number" visible="false" default="0" />
<setting id="networkCreds" type="text" visible="false" default="" /> <setting id="networkCreds" type="text" visible="false" default="" />
@ -65,7 +69,8 @@
<setting id="enableCoverArt" type="bool" label="30157" default="true" /> <setting id="enableCoverArt" type="bool" label="30157" default="true" />
<setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" /> <setting id="ignoreSpecialsNextEpisodes" type="bool" label="30527" default="false" />
<setting id="enableContext" type="bool" label="Enable the Emby context menu" default="true" /> <setting id="enableContext" type="bool" label="Enable the Emby context menu" default="true" />
<setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-1,true)" subsetting="true" /> <setting id="enableContextTranscode" type="bool" label="Enable Force Transcode" visible="eq(-1,true)" default="true" subsetting="true" />
<setting id="skipContextMenu" type="bool" label="30520" default="false" visible="eq(-2,true)" subsetting="true" />
<setting id="additionalUsers" type="text" label="30528" default="" /> <setting id="additionalUsers" type="text" label="30528" default="" />
<setting type="lsep" label="30534" /> <setting type="lsep" label="30534" />
<setting id="connectMsg" type="bool" label="30249" default="true" /> <setting id="connectMsg" type="bool" label="30249" default="true" />

View file

@ -1,142 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<window> <window>
<defaultcontrol always="true">200</defaultcontrol> <defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>600</width>
<left>35%</left>
<top>20%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<width>600</width> <left>0</left>
<height>480</height> <right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>470</width>
<height>420</height>
<control type="group">
<top>-30</top>
<control type="image">
<left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture>
<aspectratio align="left">keep</aspectratio>
</control>
</control>
<control type="image">
<width>100%</width>
<height>420</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control>
<control type="group">
<centerleft>50%</centerleft>
<top>10</top>
<width>460</width>
<height>400</height>
<control type="grouplist" id="100">
<orientation>vertical</orientation>
<itemgap>0</itemgap>
<control type="label">
<width>100%</width>
<height>75</height>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>white</textcolor>
<textshadow>66000000</textshadow>
<label>[B]$ADDON[plugin.video.emby 30612][/B]</label>
</control>
<control type="group" id="101">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30024]</label>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="204" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<ondown>205</ondown>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
</control>
<control type="image">
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="group" id="102">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffe1e1e1</textcolor>
<textshadow>66000000</textshadow>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="205" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<onup>204</onup>
<ondown>200</ondown>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
<password>true</password>
</control>
<control type="image">
<description>separator</description>
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="button" id="200">
<label>[B]$ADDON[plugin.video.emby 30605][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<onup>205</onup>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
<control type="button" id="201">
<label>[B]$ADDON[plugin.video.emby 30606][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
</control>
</control> </control>
<control type="group" id="202"> <control type="group" id="202">
<top>485</top> <top>420</top>
<visible>False</visible> <visible>False</visible>
<control type="image"> <control type="image">
<description>Error box</description> <description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width> <width>100%</width>
<height>50</height> <height>70</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control> </control>
<control type="label" id="203"> <control type="label" id="203">
<description>Error message</description> <top>10</top>
<textcolor>white</textcolor> <height>50</height>
<textcolor>ffe1e1e1</textcolor>
<scroll>true</scroll>
<shadowcolor>66000000</shadowcolor>
<font>font10</font> <font>font10</font>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny> <aligny>center</aligny>
<align>center</align> <align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Please sign in</description>
<label>$ADDON[plugin.video.emby 30612]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<width>100%</width>
<top>100</top>
</control>
<control type="group">
<top>150</top>
<control type="label">
<description>Username</description>
<label>$ADDON[plugin.video.emby 30024]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Password</description>
<top>225</top>
<control type="label">
<description>Password label</description>
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>335</top>
<control type="button" id="200">
<description>Sign in</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control> </control>
</control> </control>
</control> </control>

View file

@ -1,176 +1,221 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<window> <window>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>600</width>
<left>35%</left>
<top>15%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<width>600</width> <left>0</left>
<height>700</height> <right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control> </control>
<control type="group">
<control type="group" id="202"> <animation type="WindowOpen" reversible="false">
<top>705</top> <effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<visible>False</visible> <effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>470</width>
<height>620</height>
<control type="group">
<top>-30</top>
<control type="image"> <control type="image">
<description>Error box</description> <left>20</left>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width> <width>100%</width>
<height>50</height> <height>25</height>
<texture>logo-white.png</texture>
<aspectratio align="left">keep</aspectratio>
</control> </control>
</control>
<control type="label" id="203"> <control type="image">
<description>Error message</description> <width>100%</width>
<textcolor>white</textcolor> <height>610</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control>
<control type="group">
<centerleft>50%</centerleft>
<top>10</top>
<width>460</width>
<height>590</height>
<control type="grouplist" id="100">
<orientation>vertical</orientation>
<itemgap>0</itemgap>
<control type="label">
<width>100%</width>
<height>75</height>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font10</font> <font>font10</font>
<textcolor>white</textcolor>
<textshadow>66000000</textshadow>
<label>[B]$ADDON[plugin.video.emby 30612][/B]</label>
</control>
<control type="group" id="101">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30024]</label>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="204" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<ondown>205</ondown>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
</control>
<control type="image">
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="group" id="102">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffe1e1e1</textcolor>
<textshadow>66000000</textshadow>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="205" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<onup>204</onup>
<ondown>200</ondown>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
<password>true</password>
</control>
<control type="image">
<description>separator</description>
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="button" id="200">
<label>[B]$ADDON[plugin.video.emby 30605][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny> <aligny>center</aligny>
<align>center</align> <align>center</align>
<height>50</height> <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control> </control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<width>160</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Sign in emby connect</description>
<label>$ADDON[plugin.video.emby 30600]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<top>115</top>
</control>
<control type="group">
<top>190</top>
<control type="label">
<description>Username email</description>
<label>$ADDON[plugin.video.emby 30543]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Password</description>
<top>275</top>
<control type="label">
<description>Password label</description>
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>385</top>
<control type="button" id="200">
<description>Sign in</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201"> <control type="button" id="201">
<description>Cancel</description> <label>[B]$ADDON[plugin.video.emby 30606][/B]</label>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> <width>426</width>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height> <height>50</height>
<top>55</top> <font>font10</font>
<onup>200</onup> <textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control> </control>
</control>
<control type="group">
<description>Disclaimer</description>
<top>510</top>
<control type="label"> <control type="label">
<description>Disclaimer label</description> <description>spacer</description>
<height>20</height>
</control>
<control type="group">
<control type="label">
<label>$ADDON[plugin.video.emby 30603]</label> <label>$ADDON[plugin.video.emby 30603]</label>
<font>font10</font> <font>font10</font>
<textcolor>ff464646</textcolor> <textcolor>ff464646</textcolor>
<shadowcolor>66000000</shadowcolor>
<wrapmultiline>true</wrapmultiline> <wrapmultiline>true</wrapmultiline>
<scroll>true</scroll>
<aligny>top</aligny> <aligny>top</aligny>
<width>340</width> <height>130</height>
<height>100%</height> <left>20</left>
<right>160</right>
</control> </control>
<control type="group"> <control type="group">
<control type="label"> <top>10</top>
<description>Scan me</description> <right>20</right>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label> <width>130</width>
<font>font12</font>
<textcolor>ff0b8628</textcolor>
<aligny>top</aligny>
<width>200</width>
<top>120</top>
<left>230</left>
</control>
<control type="image"> <control type="image">
<width>130</width>
<height>130</height>
<description>qrcode</description> <description>qrcode</description>
<texture>qrcode_disclaimer.png</texture> <texture>qrcode_disclaimer.png</texture>
<width>140</width>
<height>140</height>
<top>10</top>
<left>360</left>
</control> </control>
<control type="label">
<top>135</top>
<align>center</align>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label>
<font>font10</font>
<textcolor>FF52b54b</textcolor>
<shadowcolor>66000000</shadowcolor>
<aligny>top</aligny>
</control>
</control>
</control>
</control>
</control>
<control type="group" id="202">
<top>610</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<width>100%</width>
<height>70</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control>
<control type="label" id="203">
<top>10</top>
<height>50</height>
<textcolor>ffe1e1e1</textcolor>
<scroll>true</scroll>
<shadowcolor>66000000</shadowcolor>
<font>font10</font>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
</control> </control>
</control> </control>
</control> </control>

View file

@ -2,150 +2,184 @@
<window> <window>
<defaultcontrol always="true">200</defaultcontrol> <defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder> <zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>600</width>
<left>35%</left>
<top>20%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<width>600</width> <left>0</left>
<height>525</height> <right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>470</width>
<height>420</height>
<control type="group">
<top>-30</top>
<control type="image">
<left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture>
<aspectratio align="left">keep</aspectratio>
</control>
</control>
<control type="image">
<width>100%</width>
<height>420</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control>
<control type="group">
<centerleft>50%</centerleft>
<top>10</top>
<width>460</width>
<height>400</height>
<control type="grouplist" id="100">
<orientation>vertical</orientation>
<itemgap>0</itemgap>
<control type="label">
<width>100%</width>
<height>75</height>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>white</textcolor>
<textshadow>66000000</textshadow>
<label>[B]$ADDON[plugin.video.emby 30614][/B]</label>
</control>
<control type="group" id="101">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30615]</label>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="204" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<ondown>205</ondown>
<hinttext>[COLOR ff464646]IP or https://myserver.com[/COLOR]</hinttext>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
</control>
<control type="image">
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="group" id="102">
<height>100</height>
<control type="label">
<label>$ADDON[plugin.video.emby 30030]</label>
<textcolor>ffe1e1e1</textcolor>
<textshadow>66000000</textshadow>
<font>font10</font>
<aligny>top</aligny>
<textoffsetx>20</textoffsetx>
</control>
<control id="205" type="edit">
<top>35</top>
<left>20</left>
<right>20</right>
<height>50</height>
<font>font10</font>
<textcolor>FF888888</textcolor>
<focusedcolor>FF52b54b</focusedcolor>
<shadowcolor>66000000</shadowcolor>
<onup>204</onup>
<ondown>200</ondown>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
</control>
<control type="image">
<description>separator</description>
<left>20</left>
<right>20</right>
<height>1</height>
<top>80</top>
<texture colordiffuse="ff525252">white.png</texture>
</control>
</control>
<control type="button" id="200">
<label>[B]$ADDON[plugin.video.emby 30616][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
<control type="button" id="201">
<label>[B]$ADDON[plugin.video.emby 30606][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
</control>
</control> </control>
<control type="group" id="202"> <control type="group" id="202">
<top>530</top> <top>420</top>
<visible>False</visible> <visible>False</visible>
<control type="image"> <control type="image">
<description>Error box</description> <description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width> <width>100%</width>
<height>50</height> <height>70</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control> </control>
<control type="label" id="203"> <control type="label" id="203">
<description>Error message</description> <top>10</top>
<textcolor>white</textcolor> <height>50</height>
<textcolor>ffe1e1e1</textcolor>
<scroll>true</scroll>
<shadowcolor>66000000</shadowcolor>
<font>font10</font> <font>font10</font>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny> <aligny>center</aligny>
<align>center</align> <align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Connect to server</description>
<label>$ADDON[plugin.video.emby 30614]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<width>100%</width>
<top>100</top>
</control>
<control type="group">
<top>150</top>
<control type="label">
<description>Host</description>
<label>$ADDON[plugin.video.emby 30615]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
<control type="label">
<description>Host example</description>
<label>192.168.1.100 or https://myserver.com</label>
<textcolor>ff464646</textcolor>
<font>font10</font>
<aligny>top</aligny>
<top>70</top>
</control>
</control>
<control type="group">
<description>Port</description>
<top>275</top>
<control type="label">
<description>Port label</description>
<label>$ADDON[plugin.video.emby 30030]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>380</top>
<control type="button" id="200">
<description>Connect</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30616][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control> </control>
</control> </control>
</control> </control>

View file

@ -1,277 +1,239 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<window> <window>
<defaultcontrol always="true">205</defaultcontrol> <defaultcontrol always="true">205</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>450</width>
<left>38%</left>
<top>15%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<width>450</width> <left>0</left>
<height>710</height> <right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control> </control>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>470</width>
<height>480</height>
<control type="group">
<top>-30</top>
<control type="image"> <control type="image">
<description>Emby logo</description> <left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture> <texture>logo-white.png</texture>
<aspectratio>keep</aspectratio> <aspectratio align="left">keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control> </control>
<control type="group">
<description>User info</description>
<top>70</top>
<width>350</width>
<left>50</left>
<control type="image" id="150"> <control type="image" id="150">
<description>User image</description> <right>20</right>
<width>100%</width>
<height>25</height>
<aspectratio align="right">keep</aspectratio>
<texture diffuse="user_image.png">userflyoutdefault.png</texture> <texture diffuse="user_image.png">userflyoutdefault.png</texture>
<aspectratio>keep</aspectratio>
<align>center</align>
<width>100%</width>
<height>70</height>
<top>40</top>
</control> </control>
<control type="image" id="204">
<description>Busy animation</description>
<align>center</align>
<top>23</top>
<width>100%</width>
<height>105</height>
<visible>False</visible>
<texture colordiffuse="ff13a134">fading_circle.png</texture>
<aspectratio>keep</aspectratio>
<animation effect="rotate" start="360" end="0" center="auto" time="2000" loop="true" condition="true">conditional</animation>
</control> </control>
<control type="label" id="151">
<description>Welcome user</description>
<textcolor>white</textcolor>
<font>font12</font>
<align>center</align>
<aligny>top</aligny>
<top>120</top>
<width>100%</width>
<height>50</height>
</control>
<control type="image"> <control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>165</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
<control type="label">
<description>Select server</description>
<textcolor>ffa6a6a6</textcolor>
<label>$ADDON[plugin.video.emby 30607]</label>
<font>font10</font>
<align>center</align>
<aligny>top</aligny>
<top>170</top>
<width>100%</width> <width>100%</width>
<height>50</height> <height>480</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control> </control>
</control>
<control type="group"> <control type="group">
<top>290</top> <centerleft>50%</centerleft>
<width>100%</width>
<height>184</height>
<control type="list" id="155">
<description>Connect servers</description>
<focusposition>0</focusposition>
<width>100%</width>
<height>100%</height>
<top>10</top> <top>10</top>
<left>55</left> <width>460</width>
<onup>155</onup> <height>460</height>
<ondown condition="Control.IsVisible(205)">205</ondown> <control type="grouplist" id="100">
<ondown condition="!Control.IsVisible(205)">206</ondown> <orientation>vertical</orientation>
<onleft condition="Control.IsVisible(205)">205</onleft> <itemgap>0</itemgap>
<onleft condition="!Control.IsVisible(205)">206</onleft> <control type="label">
<onright>155</onright> <width>100%</width>
<pagecontrol>60</pagecontrol> <height>75</height>
<scrolltime tween="sine" easing="out">250</scrolltime> <aligny>center</aligny>
<itemlayout height="46"> <textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>white</textcolor>
<textshadow>66000000</textshadow>
<label>[B]$ADDON[plugin.video.emby 30607][/B]</label>
</control>
<control type="group" id="101">
<height>200</height>
<control type="list" id="155">
<centerleft>50%</centerleft>
<width>460</width>
<height>200</height>
<onup>noop</onup>
<onleft>close</onleft>
<onright>close</onright>
<ondown>205</ondown>
<itemlayout width="460" height="50">
<control type="group"> <control type="group">
<width>45</width> <left>20</left>
<height>45</height> <top>5</top>
<width>40</width>
<control type="image"> <control type="image">
<description>Network</description>
<aspectratio>keep</aspectratio> <aspectratio>keep</aspectratio>
<texture>network.png</texture> <texture>network.png</texture>
<visible>StringCompare(ListItem.Property(server_type),network)</visible> <visible>String.IsEqual(ListItem.Property(server_type),network)</visible>
</control> </control>
<control type="image"> <control type="image">
<description>Wifi</description>
<aspectratio>keep</aspectratio> <aspectratio>keep</aspectratio>
<texture>wifi.png</texture> <texture>wifi.png</texture>
<visible>StringCompare(ListItem.Property(server_type),wifi)</visible> <visible>String.IsEqual(ListItem.Property(server_type),wifi)</visible>
</control> </control>
</control> </control>
<control type="label"> <control type="label">
<width>300</width> <left>50</left>
<height>40</height> <height>50</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny> <aligny>center</aligny>
<textcolor>ff838383</textcolor> <textoffsetx>20</textoffsetx>
<info>ListItem.Label</info> <font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<label>$INFO[ListItem.Label]</label>
</control> </control>
</itemlayout> </itemlayout>
<focusedlayout height="46"> <focusedlayout width="460" height="50">
<control type="image">
<width>100%</width>
<height>50</height>
<texture colordiffuse="ff222326">white.png</texture>
<visible>!Control.HasFocus(155)</visible>
</control>
<control type="image">
<width>100%</width>
<height>50</height>
<texture colordiffuse="ff303034">white.png</texture>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="group"> <control type="group">
<width>45</width> <left>20</left>
<height>45</height> <width>40</width>
<control type="image"> <control type="image">
<description>Network</description> <description>Network</description>
<aspectratio>keep</aspectratio> <aspectratio>keep</aspectratio>
<texture>network.png</texture> <texture>network.png</texture>
<visible>StringCompare(ListItem.Property(server_type),network)</visible> <visible>String.IsEqual(ListItem.Property(server_type),network)</visible>
</control> </control>
<control type="image"> <control type="image">
<description>Wifi</description> <description>Wifi</description>
<aspectratio>keep</aspectratio> <aspectratio>keep</aspectratio>
<texture>wifi.png</texture> <texture>wifi.png</texture>
<visible>StringCompare(ListItem.Property(server_type),wifi)</visible> <visible>String.IsEqual(ListItem.Property(server_type),wifi)</visible>
</control> </control>
</control> </control>
<control type="label">
<width>300</width>
<height>40</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<info>ListItem.Label</info>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="label"> <control type="label">
<width>300</width> <left>50</left>
<height>40</height> <height>50</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny> <aligny>center</aligny>
<textcolor>ff838383</textcolor> <textoffsetx>20</textoffsetx>
<info>ListItem.Label</info> <font>font10</font>
<visible>!Control.HasFocus(155)</visible> <scroll>true</scroll>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<label>$INFO[ListItem.Label]</label>
</control> </control>
</focusedlayout> </focusedlayout>
</control> </control>
<control type="image" id="204">
<control type="scrollbar" id="60"> <centerleft>50%</centerleft>
<left>395</left> <description>Busy animation</description>
<top>10</top> <align>center</align>
<width>5</width> <width>120</width>
<height>100%</height> <height>200</height>
<onleft>155</onleft> <visible>false</visible>
<onup>60</onup> <texture colordiffuse="ff52b54b">spinner.gif</texture>
<ondown>60</ondown> <aspectratio>keep</aspectratio>
<texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground> </control>
<texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar> </control>
<texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus> <control type="label" id="102">
<showonepage>false</showonepage> <description>spacer</description>
<height>20</height>
</control> </control>
<control type="group">
<top>100%</top>
<height>220</height>
<control type="group">
<top>45</top>
<height>150</height>
<control type="button" id="205"> <control type="button" id="205">
<visible>True</visible> <label>[B]$ADDON[plugin.video.emby 30600][/B]</label>
<description>Sign in Connect</description> <width>426</width>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus> <height>50</height>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30600][/B][/UPPERCASE]</label>
<font>font10</font> <font>font10</font>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor> <focusedcolor>white</focusedcolor>
<align>center</align> <selectedcolor>ffe1e1e1</selectedcolor>
<width>350</width> <shadowcolor>66000000</shadowcolor>
<height>50</height> <textoffsetx>20</textoffsetx>
<left>50</left>
<onup>155</onup>
<ondown>206</ondown>
</control>
<control type="button" id="206">
<description>Manually add server</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30611][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<top>55</top>
<width>350</width>
<height>50</height>
<left>50</left>
<onup condition="Control.IsVisible(205)">205</onup>
<onup condition="!Control.IsVisible(205)">155</onup>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<top>110</top>
<width>350</width>
<height>50</height>
<left>50</left>
<onup>206</onup>
</control>
</control>
<control type="group" id="202">
<top>100%</top>
<visible>False</visible>
<control type="image">
<description>Message box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
<top>20</top>
</control>
<control type="label" id="203">
<description>Message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny> <aligny>center</aligny>
<align>center</align> <align>center</align>
<height>50</height> <texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<top>20</top> <texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<onup>155</onup>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control> </control>
<control type="button" id="206">
<label>[B]$ADDON[plugin.video.emby 30611][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<onup>155</onup>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
<control type="button" id="201">
<label>[B]$ADDON[plugin.video.emby 30606][/B]</label>
<width>426</width>
<height>50</height>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<onup>155</onup>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control>
</control>
</control>
<control type="group" id="202">
<top>480</top>
<visible>False</visible>
<control type="image">
<width>100%</width>
<height>70</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control>
<control type="label" id="203">
<top>10</top>
<height>50</height>
<textcolor>ffe1e1e1</textcolor>
<scroll>true</scroll>
<shadowcolor>66000000</shadowcolor>
<font>font10</font>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>>
</control> </control>
</control> </control>
</control> </control>

View file

@ -1,194 +1,226 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<window> <window>
<defaultcontrol always="true">155</defaultcontrol> <defaultcontrol always="true">155</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>715</width>
<left>32%</left>
<top>20%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture border="6" colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<width>100%</width> <left>0</left>
<height>525</height> <right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control> </control>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>920</width>
<height>570</height>
<control type="group">
<top>-30</top>
<control type="image"> <control type="image">
<description>Emby logo</description> <left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture> <texture>logo-white.png</texture>
<aspectratio>keep</aspectratio> <aspectratio align="left">keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control> </control>
<control type="label">
<description>Please sign in</description>
<label>$ADDON[plugin.video.emby 30612]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<top>80</top>
<width>100%</width>
</control> </control>
<control type="group">
<top>100</top>
<width>620</width>
<height>245</height>
<left>50</left>
<control type="list" id="155">
<description>Select User</description>
<focusposition>0</focusposition>
<width>100%</width>
<top>40</top>
<onleft>155</onleft>
<onright>155</onright>
<ondown>200</ondown>
<pagecontrol>60</pagecontrol>
<orientation>horizontal</orientation>
<scrolltime tween="sine" easing="out">250</scrolltime>
<itemlayout width="155">
<control type="group">
<width>150</width>
<control type="image"> <control type="image">
<description>User image</description>
<colordiffuse>ff888888</colordiffuse>
<info>ListItem.Icon</info>
<aspectratio>keep</aspectratio>
<width>100%</width> <width>100%</width>
<height>150</height> <height>570</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control> </control>
<control type="group">
<control type="image"> <centerleft>50%</centerleft>
<description>Background label</description> <top>10</top>
<texture colordiffuse="ff222222">white.png</texture> <width>908</width>
<width>100%</width> <height>550</height>
<height>50</height> <control type="grouplist" id="100">
<top>150</top> <orientation>vertical</orientation>
</control> <itemgap>0</itemgap>
<control type="label"> <control type="label">
<width>100%</width> <width>100%</width>
<align>center</align> <height>75</height>
<height>50</height> <aligny>center</aligny>
<top>150</top> <textoffsetx>20</textoffsetx>
<font>font10</font> <font>font10</font>
<textcolor>white</textcolor> <textcolor>white</textcolor>
<info>ListItem.Label</info> <textshadow>66000000</textshadow>
<label>[B]$ADDON[plugin.video.emby 30612][/B]</label>
</control>
<control type="list" id="155">
<animation effect="slide" time="0" start="0,0" end="148,0" condition="Integer.IsEqual(Container(155).NumItems,2)">Conditional</animation>
<animation effect="slide" time="0" start="0,0" end="296,0" condition="Integer.IsEqual(Container(155).NumItems,1)">Conditional</animation>
<centerleft>50%</centerleft>
<width>908</width>
<height>362</height>
<onup>noop</onup>
<onleft>noop</onleft>
<onright>noop</onright>
<ondown>200</ondown>
<orientation>horizontal</orientation>
<itemlayout width="296">
<control type="group">
<left>20</left>
<top>10</top>
<control type="image">
<top>-2</top>
<left>-2</left>
<width>282</width>
<height>282</height>
<texture>items/shadow_square.png</texture>
</control>
<control type="image">
<width>276</width>
<height>322</height>
<texture colordiffuse="ff121314">white.png</texture>
<aspectratio scalediffuse="false">stretch</aspectratio>
</control>
<control type="image">
<width>276</width>
<height>276</height>
<texture colordiffuse="ff0288d1" diffuse="items/mask_square.png">white.png</texture>
<aspectratio>stretch</aspectratio>
<visible>String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png)</visible>
</control>
<control type="image">
<width>276</width>
<height>276</height>
<texture diffuse="items/mask_square.png" background="true">$INFO[ListItem.Icon]</texture>
<aspectratio scalediffuse="false">stretch</aspectratio>
</control>
<control type="group">
<top>285</top>
<width>276</width>
<control type="label">
<width>100%</width>
<height>30</height>
<label>$INFO[ListItem.Label]</label>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<scroll>Control.HasFocus(155)</scroll>
<align>center</align>
</control>
</control> </control>
</control> </control>
</itemlayout> </itemlayout>
<focusedlayout width="155"> <focusedlayout width="296">
<control type="group"> <control type="group">
<width>150</width> <left>20</left>
<top>10</top>
<control type="image"> <control type="image">
<description>User image</description> <top>-2</top>
<info>ListItem.Icon</info> <left>-2</left>
<aspectratio>keep</aspectratio> <width>282</width>
<width>100%</width> <height>282</height>
<height>150</height> <texture>items/shadow_square.png</texture>
</control>
<control type="image">
<width>276</width>
<height>322</height>
<texture colordiffuse="ff121314">white.png</texture>
<aspectratio scalediffuse="false">stretch</aspectratio>
</control>
<control type="image">
<width>276</width>
<height>276</height>
<texture colordiffuse="ff0288d1" diffuse="items/mask_square.png">white.png</texture>
<aspectratio>stretch</aspectratio>
<visible>String.IsEmpty(ListItem.Icon) | String.Contains(ListItem.Icon,logindefault.png)</visible>
</control>
<control type="image">
<width>276</width>
<height>276</height>
<texture diffuse="items/mask_square.png" background="true">$INFO[ListItem.Icon]</texture>
<aspectratio scalediffuse="false">stretch</aspectratio>
</control>
<control type="image">
<left>-12</left>
<top>-7</top>
<width>300</width>
<height>300</height>
<texture colordiffuse="FF388e3c">items/focus_square.png</texture>
<aspectratio>scale</aspectratio>
<animation effect="fade" start="0" end="100" time="200" tween="sine">Focus</animation>
<animation effect="fade" start="100" end="0" time="0">UnFocus</animation>
<visible>Control.HasFocus(155)</visible> <visible>Control.HasFocus(155)</visible>
</control> </control>
<control type="image"> <control type="group">
<description>User image</description> <top>285</top>
<colordiffuse>ff888888</colordiffuse> <width>276</width>
<info>ListItem.Icon</info>
<aspectratio>keep</aspectratio>
<width>100%</width>
<height>150</height>
<visible>!Control.HasFocus(155)</visible>
</control>
<control type="image">
<description>Background label</description>
<texture colordiffuse="ff333333">white.png</texture>
<width>100%</width>
<height>50</height>
<top>150</top>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="image">
<description>Background label</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
<top>150</top>
<visible>!Control.HasFocus(155)</visible>
</control>
<control type="label"> <control type="label">
<width>100%</width> <width>100%</width>
<align>center</align> <height>30</height>
<height>50</height> <label>$INFO[ListItem.Label]</label>
<top>150</top>
<font>font10</font> <font>font10</font>
<textcolor>white</textcolor> <textcolor>white</textcolor>
<info>ListItem.Label</info> <shadowcolor>66000000</shadowcolor>
<scroll>Control.HasFocus(155)</scroll>
<align>center</align>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="label">
<width>100%</width>
<height>30</height>
<label>$INFO[ListItem.Label]</label>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<scroll>false</scroll>
<align>center</align>
<visible>!Control.HasFocus(155)</visible>
</control>
</control> </control>
</control> </control>
</focusedlayout> </focusedlayout>
</control> </control>
<control type="scrollbar" id="60">
<top>100%</top>
<width>615</width>
<height>5</height>
<onleft>155</onleft>
<onleft>60</onleft>
<onright>60</onright>
<texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground>
<texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar>
<texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus>
<showonepage>false</showonepage>
<orientation>horizontal</orientation>
</control>
<control type="group">
<width>615</width>
<height>325</height>
<top>100%</top>
<control type="group">
<control type="button" id="200"> <control type="button" id="200">
<description>Manual Login button</description> <label>[B]$ADDON[plugin.video.emby 30540][/B]</label>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> <width>874</width>
<texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30540][/B][/UPPERCASE]</label>
<align>center</align>
<width>100%</width>
<height>50</height> <height>50</height>
<top>35</top>
<font>font10</font> <font>font10</font>
<textcolor>ffa6a6a6</textcolor> <textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor> <focusedcolor>white</focusedcolor>
<ondown>201</ondown> <selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<onup>155</onup> <onup>155</onup>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control> </control>
<control type="button" id="201"> <control type="button" id="201">
<description>Cancel</description> <label>[B]$ADDON[plugin.video.emby 30606][/B]</label>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus> <width>874</width>
<texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height> <height>50</height>
<top>90</top> <font>font10</font>
<onup>200</onup> <textcolor>ffe1e1e1</textcolor>
<focusedcolor>white</focusedcolor>
<selectedcolor>ffe1e1e1</selectedcolor>
<shadowcolor>66000000</shadowcolor>
<textoffsetx>20</textoffsetx>
<aligny>center</aligny>
<align>center</align>
<texturefocus border="10" colordiffuse="ff52b54b">buttons/shadow_smallbutton.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
<pulseonselect>no</pulseonselect>
<onup>155</onup>
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
</control> </control>
</control> </control>
</control> </control>

View file

@ -1,105 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<window> <window>
<defaultcontrol always="true">155</defaultcontrol> <defaultcontrol always="true">155</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls> <controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group"> <control type="group">
<width>450</width>
<left>38%</left>
<top>36%</top>
<control type="image"> <control type="image">
<description>Background box</description> <top>0</top>
<texture colordiffuse="ff111111">white.png</texture> <bottom>0</bottom>
<height>90</height> <left>0</left>
<right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control> </control>
<control type="group">
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>370</width>
<height>220</height>
<control type="group">
<top>-30</top>
<control type="image"> <control type="image">
<description>Emby logo</description> <left>20</left>
<texture>emby-icon.png</texture> <width>100%</width>
<aspectratio>keep</aspectratio> <height>25</height>
<height>30</height> <texture>logo-white.png</texture>
<top>20</top> <aspectratio align="left">keep</aspectratio>
<left>370</left>
</control> </control>
<control type="image" id="150"> <control type="image" id="150">
<description>User image</description> <right>20</right>
<texture diffuse="user_image.png"></texture>
<aspectratio>keep</aspectratio>
<height>34</height>
<top>20</top>
<left>285</left>
</control>
<control type="image">
<description>separator</description>
<width>100%</width> <width>100%</width>
<height>0.5</height> <height>25</height>
<top>70</top> <aspectratio align="right">keep</aspectratio>
<left>-5</left> <texture diffuse="user_image.png">userflyoutdefault.png</texture>
<texture colordiffuse="ff484848" border="90,3,90,3">emby-separator.png</texture> </control>
</control>
<control type="image">
<width>100%</width>
<height>220</height>
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
</control> </control>
<control type="group">
<width>450</width>
<top>90</top>
<control type="list" id="155"> <control type="list" id="155">
<width>100%</width> <centerleft>50%</centerleft>
<height>100%</height> <top>10</top>
<align>center</align> <width>360</width>
<onup>155</onup> <height>200</height>
<ondown>155</ondown> <onup>noop</onup>
<onleft>155</onleft> <onleft>close</onleft>
<onright>155</onright> <onright>close</onright>
<scrolltime>200</scrolltime> <ondown>noop</ondown>
<itemlayout height="55"> <itemlayout width="360" height="50">
<control type="image">
<description>Background box</description>
<width>450</width>
<texture colordiffuse="ff111111">white.png</texture>
</control>
<control type="label"> <control type="label">
<width>400</width> <width>100%</width>
<font>font11</font> <height>50</height>
<textcolor>ff525252</textcolor>
<left>25</left>
<align>center</align>
<aligny>center</aligny> <aligny>center</aligny>
<info>ListItem.Label</info> <textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<label>$INFO[ListItem.Label]</label>
</control> </control>
</itemlayout> </itemlayout>
<focusedlayout width="360" height="50">
<focusedlayout height="55">
<control type="image"> <control type="image">
<description>Background box</description> <width>100%</width>
<width>450</width> <height>50</height>
<texture colordiffuse="ff111111">white.png</texture> <texture colordiffuse="ff222326">white.png</texture>
<visible>!Control.HasFocus(155)</visible>
</control> </control>
<control type="image"> <control type="image">
<width>400</width> <width>100%</width>
<left>25</left> <height>50</height>
<align>center</align> <texture colordiffuse="ff303034">white.png</texture>
<texture border="5" colordiffuse="ff222222">white.png</texture>
<visible>Control.HasFocus(155)</visible> <visible>Control.HasFocus(155)</visible>
<animation effect="fade" time="300">Visible</animation>
<animation effect="fade" time="300">Hidden</animation>
</control> </control>
<control type="label"> <control type="label">
<width>400</width> <width>100%</width>
<font>font11</font> <height>50</height>
<textcolor>white</textcolor>
<left>25</left>
<align>center</align>
<aligny>center</aligny> <aligny>center</aligny>
<info>ListItem.Label</info> <textoffsetx>20</textoffsetx>
<font>font10</font>
<scroll>true</scroll>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<label>$INFO[ListItem.Label]</label>
</control> </control>
</focusedlayout> </focusedlayout>
</control> </control>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<window id="3301" type="dialog">
<defaultcontrol always="true">100</defaultcontrol>
<controls>
<control type="group">
<control type="image">
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
<texture colordiffuse="CC000000">white.png</texture>
<aspectratio>stretch</aspectratio>
<animation effect="fade" end="100" time="200">WindowOpen</animation>
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
</control>
<control type="group">
<animation effect="slide" time="0" end="0,-15" condition="true">Conditional</animation>
<animation type="WindowOpen" reversible="false">
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
<effect type="fade" delay="160" end="100" time="240" />
</animation>
<animation type="WindowClose" reversible="false">
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
<effect type="fade" start="100" end="0" time="240" />
</animation>
<centerleft>50%</centerleft>
<centertop>50%</centertop>
<width>20%</width>
<height>90%</height>
<control type="grouplist" id="100">
<orientation>vertical</orientation>
<left>0</left>
<right>0</right>
<height>auto</height>
<align>center</align>
<itemgap>0</itemgap>
<onright>close</onright>
<onleft>close</onleft>
<usecontrolcoords>true</usecontrolcoords>
<control type="group">
<height>30</height>
<control type="image">
<left>20</left>
<width>100%</width>
<height>25</height>
<texture>logo-white.png</texture>
<aspectratio align="left">keep</aspectratio>
</control>
<control type="image">
<right>20</right>
<width>100%</width>
<height>25</height>
<aspectratio align="right">keep</aspectratio>
<texture diffuse="user_image.png">$INFO[Window(Home).Property(EmbyUserImage)]</texture>
<visible>!String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
</control>
<control type="image">
<right>20</right>
<width>100%</width>
<height>25</height>
<aspectratio align="right">keep</aspectratio>
<texture diffuse="user_image.png">userflyoutdefault.png</texture>
<visible>String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
</control>
</control>
<control type="image">
<width>100%</width>
<height>10</height>
<texture border="5" colordiffuse="ff222326">dialogs/menu_top.png</texture>
</control>
<control type="button" id="3010">
<width>100%</width>
<height>50</height>
<align>left</align>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<disabledcolor>FF404040</disabledcolor>
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
</control>
<control type="button" id="3011">
<width>100%</width>
<height>50</height>
<align>left</align>
<aligny>center</aligny>
<textoffsetx>20</textoffsetx>
<font>font10</font>
<textcolor>ffe1e1e1</textcolor>
<shadowcolor>66000000</shadowcolor>
<disabledcolor>FF404040</disabledcolor>
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
</control>
<control type="image">
<width>100%</width>
<height>10</height>
<texture border="5" colordiffuse="ff222326">dialogs/menu_bottom.png</texture>
</control>
</control>
</control>
</control>
</controls>
</window>

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After