diff --git a/addon.xml b/addon.xml index 8d848b4a..ab62d8ca 100644 --- a/addon.xml +++ b/addon.xml @@ -15,6 +15,13 @@ + + + + Settings for the Plex Server + [!IsEmpty(ListItem.DBID) + !StringCompare(ListItem.DBID,-1) | !IsEmpty(ListItem.Property(plexid))] + !IsEmpty(Window(10000).Property(plex_context)) + + all en diff --git a/contextmenu.py b/contextmenu.py index d4549829..df6de3fd 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -1,175 +1,52 @@ # -*- coding: utf-8 -*- -################################################################################################# +############################################################################### import logging import os import sys -import urlparse import xbmc import xbmcaddon -import xbmcgui -################################################################################################# +############################################################################### -_addon = xbmcaddon.Addon(id='plugin.video.emby') -_addon_path = _addon.getAddonInfo('path').decode('utf-8') -_base_resource = xbmc.translatePath(os.path.join(_addon_path, 'resources', 'lib')).decode('utf-8') +_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') +try: + _addon_path = _addon.getAddonInfo('path').decode('utf-8') +except TypeError: + _addon_path = _addon.getAddonInfo('path').decode() +try: + _base_resource = xbmc.translatePath(os.path.join( + _addon_path, + 'resources', + 'lib')).decode('utf-8') +except TypeError: + _base_resource = xbmc.translatePath(os.path.join( + _addon_path, + 'resources', + 'lib')).decode() sys.path.append(_base_resource) -################################################################################################# - -import api -import artwork -import downloadutils -import librarysync -import read_embyserver as embyserver -import embydb_functions as embydb -import kodidb_functions as kodidb -import musicutils as musicutils -from utils import settings, language as lang, kodiSQL - -################################################################################################# +############################################################################### import loghandler +from context_entry import ContextMenu + +############################################################################### loghandler.config() -log = logging.getLogger("EMBY.contextmenu") +log = logging.getLogger("PLEX.contextmenu") -################################################################################################# +############################################################################### -# Kodi contextmenu item to configure the emby settings -if __name__ == '__main__': +if __name__ == "__main__": - kodiId = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') - itemType = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') - itemId = "" - - if not itemType: - - if xbmc.getCondVisibility("Container.Content(albums)"): - itemType = "album" - elif xbmc.getCondVisibility("Container.Content(artists)"): - itemType = "artist" - elif xbmc.getCondVisibility("Container.Content(songs)"): - itemType = "song" - elif xbmc.getCondVisibility("Container.Content(pictures)"): - itemType = "picture" - else: - log.info("ItemType is unknown.") - - if (not kodiId or kodiId == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"): - itemId = xbmc.getInfoLabel("ListItem.Property(embyid)") - - elif kodiId and itemType: - embyconn = kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(kodiId, itemType) - embycursor.close() - try: - itemId = item[0] - except TypeError: - pass - - - log.info("Found ItemId: %s ItemType: %s" % (itemId, itemType)) - if itemId: - - dialog = xbmcgui.Dialog() - - emby = embyserver.Read_EmbyServer() - item = emby.getItem(itemId) - API = api.API(item) - userdata = API.getUserData() - likes = userdata['Likes'] - favourite = userdata['Favorite'] - - options = [] - - if favourite: - # Remove from emby favourites - options.append(lang(30406)) - else: - # Add to emby favourites - options.append(lang(30405)) - - if itemType == "song": - # Set custom song rating - options.append(lang(30407)) - - # Refresh item - options.append(lang(30410)) - # Delete item - options.append(lang(30409)) - # Addon settings - options.append(lang(30408)) - - # Display select dialog and process results - resp = xbmcgui.Dialog().select(lang(30401), options) - if resp > -1: - selected = options[resp] - - if selected == lang(30410): - # Refresh item - emby.refreshItem(itemId) - elif selected == lang(30405): - # Add favourite - emby.updateUserRating(itemId, favourite=True) - elif selected == lang(30406): - # Delete favourite - emby.updateUserRating(itemId, favourite=False) - elif selected == lang(30407): - # Update song rating - kodiconn = kodiSQL('music') - kodicursor = kodiconn.cursor() - query = "SELECT rating FROM song WHERE idSong = ?" - kodicursor.execute(query, (kodiId,)) - try: - value = kodicursor.fetchone()[0] - current_value = int(round(float(value),0)) - except TypeError: - pass - else: - new_value = dialog.numeric(0, lang(30411), str(current_value)) - if new_value > -1: - - new_value = int(new_value) - if new_value > 5: - new_value = 5 - - if settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(new_value, API.getFilePath()) - - query = "UPDATE song SET rating = ? WHERE idSong = ?" - kodicursor.execute(query, (new_value, kodiId,)) - kodiconn.commit() - - '''if settings('enableExportSongRating') == "true": - like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(new_value) - emby.updateUserRating(itemId, like, favourite, deletelike)''' - finally: - kodicursor.close() - - elif selected == lang(30408): - # Open addon settings - xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)") - - elif selected == lang(30409): - # delete item from the server - delete = True - if settings('skipContextMenu') != "true": - resp = dialog.yesno( - heading=lang(29999), - line1=lang(33041)) - if not resp: - log.info("User skipped deletion for: %s." % itemId) - delete = False - - if delete: - log.info("Deleting request: %s" % itemId) - emby.deleteItem(itemId) - - xbmc.sleep(500) - xbmc.executebuiltin('Container.Refresh') \ No newline at end of file + try: + # Start the context menu + ContextMenu() + except Exception as error: + log.exception(error) + import traceback + log.exception("Traceback:\n%s" % traceback.format_exc()) + raise diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 913a3512..4a35817e 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -1,6 +1,7 @@  + PlexKodiConnect Server Address (IP) Preferred playback method Log level @@ -260,7 +261,11 @@ Remove from Plex favorites Set custom song rating Plex addon settings - Delete item from the server + Delete item from server + Refresh this item + Set custom song rating (0-5) + Force transcode + Enable Plex context menu in Kodi Verify Host SSL Certificate (more secure) @@ -283,7 +288,7 @@ [COLOR yellow]Enter network credentials[/COLOR] Enable Plex Trailers (Plexpass is needed) Ask to play trailers - Skip Emby delete confirmation for the context menu (use at your own risk) + Skip Plex delete confirmation for the context menu (use at your own risk) Jump back on resume (in seconds) Force transcode H265 Music metadata options (not compatible with direct stream) @@ -344,6 +349,7 @@ Comparing: Failed to generate a new device Id. See your logs for more information. Kodi will now restart to apply the changes. + Delete file(s) from Plex Server? This will also delete the file(s) from disk! - Number of trailers to play before a movie diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index 789debcf..6263abb0 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -1,5 +1,6 @@ + PlexKodiConnect IP-Adresse des Servers Automatisches Öffnen von Ordnern mit einem Eintrag Gewünschte Wiedergabe-Methode @@ -21,6 +22,7 @@ Netzwerk Credentials eingeben PlexKodiConnect Start Verzögerung (in Sekunden) Extras ignorieren, wenn Nächste Episode gespielt wird + Sicherheitsabfrage bei Löschen des Plex Elements deaktivieren Bei Wiederaufnahme zurückspulen (in Sekunden) [COLOR yellow]Anzahl Login-Versuche zurücksetzen[/COLOR] @@ -282,7 +284,24 @@ Musikstücke Kanäle + + Plex Optionen + Clear like for this item + Like this item + Dislike this item + Add to Plex favorites + Remove from Plex favorites + Set custom song rating + Plex Addon Optionen + Element in Plex Server löschen + Element neu laden + Set custom song rating (0-5) + Transkodieren erzwingen + Plex Kontextmenu in Kodi aktivieren + + Kodi wird jetzt neu gestartet um die Änderungen anzuwenden. + Eintrag vom Plex Server löschen? Dies wird die Dateien auch endgültig von der Festplatte löschen! - Anzahl abzuspielender Trailer vor einem Film diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index ba42e254..1d008e54 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -526,3 +526,22 @@ def scrobble(ratingKey, state): return downloadutils.DownloadUtils().downloadUrl(url) log.info("Toggled watched state for Plex item %s" % ratingKey) + + +def delete_item_from_pms(plexid): + """ + Deletes the item plexid from the Plex Media Server (and the harddrive!). + Do make sure that the currently logged in user has the credentials + + Returns True if successful, False otherwise + """ + xml = downloadutils.DownloadUtils().downloadUrl( + '{server}/library/metadata/%s' % plexid, + action_type="DELETE") + try: + xml.attrib + except AttributeError: + log.error('Could not delete Plex id %s' % plexid) + return False + log.info(xml.dump) + return True diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py new file mode 100644 index 00000000..65c386ee --- /dev/null +++ b/resources/lib/context_entry.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +############################################################################### + +import logging + +import xbmc +import xbmcaddon + +import PlexAPI +from PlexFunctions import GetPlexMetadata, delete_item_from_pms +import embydb_functions as embydb +from utils import settings, dialog, language as lang, kodiSQL +from dialogs import context + +############################################################################### + +log = logging.getLogger("PLEX."+__name__) +addonName = 'PlexKodiConnect' + +OPTIONS = { + 'Refresh': lang(30410), + 'Delete': lang(30409), + 'Addon': lang(30408), + # 'AddFav': lang(30405), + # 'RemoveFav': lang(30406), + # 'RateSong': lang(30407), + 'Transcode': lang(30412) +} + +############################################################################### + + +class ContextMenu(object): + + _selected_option = None + + def __init__(self): + self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8') + self.item_type = self._get_item_type() + self.item_id = self._get_item_id(self.kodi_id, self.item_type) + + log.info("Found item_id: %s item_type: %s" + % (self.item_id, self.item_type)) + + if not self.item_id: + return + + self.item = GetPlexMetadata(self.item_id) + self.api = PlexAPI.API(self.item) + + if self._select_menu(): + self._action_menu() + + if self._selected_option in (OPTIONS['Delete'], + OPTIONS['Refresh']): + log.info("refreshing container") + xbmc.sleep(500) + xbmc.executebuiltin('Container.Refresh') + + @classmethod + def _get_item_type(cls): + item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8') + + if not item_type: + if xbmc.getCondVisibility('Container.Content(albums)'): + item_type = "album" + elif xbmc.getCondVisibility('Container.Content(artists)'): + item_type = "artist" + elif xbmc.getCondVisibility('Container.Content(songs)'): + item_type = "song" + elif xbmc.getCondVisibility('Container.Content(pictures)'): + item_type = "picture" + else: + log.info("item_type is unknown") + + return item_type + + @classmethod + def _get_item_id(cls, kodi_id, item_type): + item_id = xbmc.getInfoLabel('ListItem.Property(plexid)') + if not item_id and kodi_id and item_type: + with embydb.GetEmbyDB() as emby_db: + item = emby_db.getItem_byKodiId(kodi_id, item_type) + try: + item_id = item[0] + except TypeError: + log.error('Could not get the Plex id for context menu') + return item_id + + def _select_menu(self): + # Display select dialog + options = [] + + if self.item_type in ("movie", "episode", "song"): + options.append(OPTIONS['Transcode']) + + # userdata = self.api.getUserData() + # if userdata['Favorite']: + # # Remove from emby favourites + # options.append(OPTIONS['RemoveFav']) + # else: + # # Add to emby favourites + # options.append(OPTIONS['AddFav']) + + # if self.item_type == "song": + # # Set custom song rating + # options.append(OPTIONS['RateSong']) + + # Refresh item + options.append(OPTIONS['Refresh']) + # Delete item, only if the Plex Home main user is logged in + if settings('plex_restricteduser') != 'true': + options.append(OPTIONS['Delete']) + # Addon settings + options.append(OPTIONS['Addon']) + + addon = xbmcaddon.Addon('plugin.video.plexkodiconnect') + context_menu = context.ContextMenu("script-emby-context.xml", + addon.getAddonInfo('path'), + "default", "1080i") + context_menu.set_options(options) + context_menu.doModal() + + if context_menu.is_selected(): + self._selected_option = context_menu.get_selected() + + return self._selected_option + + def _action_menu(self): + + selected = self._selected_option + + if selected == OPTIONS['Transcode']: + pass + + elif selected == OPTIONS['Refresh']: + self.emby.refreshItem(self.item_id) + + # elif selected == OPTIONS['AddFav']: + # self.emby.updateUserRating(self.item_id, favourite=True) + + # elif selected == OPTIONS['RemoveFav']: + # self.emby.updateUserRating(self.item_id, favourite=False) + + # elif selected == OPTIONS['RateSong']: + # self._rate_song() + + elif selected == OPTIONS['Addon']: + xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') + + elif selected == OPTIONS['Delete']: + self._delete_item() + + def _rate_song(self): + + conn = kodiSQL('music') + cursor = conn.cursor() + query = "SELECT rating FROM song WHERE idSong = ?" + cursor.execute(query, (self.kodi_id,)) + try: + value = cursor.fetchone()[0] + current_value = int(round(float(value), 0)) + except TypeError: + pass + else: + new_value = dialog("numeric", 0, lang(30411), str(current_value)) + if new_value > -1: + + new_value = int(new_value) + if new_value > 5: + new_value = 5 + + if settings('enableUpdateSongRating') == "true": + musicutils.updateRatingToFile(new_value, self.api.get_file_path()) + + query = "UPDATE song SET rating = ? WHERE idSong = ?" + cursor.execute(query, (new_value, self.kodi_id,)) + conn.commit() + finally: + cursor.close() + + def _delete_item(self): + + delete = True + if settings('skipContextMenu') != "true": + + if not dialog(type_="yesno", heading=addonName, line1=lang(33041)): + log.info("User skipped deletion for: %s", self.item_id) + delete = False + + if delete: + log.info("Deleting Plex item with id %s", self.item_id) + delete_item_from_pms(self.item_id) diff --git a/resources/lib/dialog/__init__.py b/resources/lib/dialog/__init__.py deleted file mode 100644 index b93054b3..00000000 --- a/resources/lib/dialog/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Dummy file to make this directory a package. diff --git a/resources/lib/dialog/loginconnect.py b/resources/lib/dialog/loginconnect.py deleted file mode 100644 index f12eab0f..00000000 --- a/resources/lib/dialog/loginconnect.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################## - -import os - -import xbmcgui -import xbmcaddon - -################################################################################################## - -addon = xbmcaddon.Addon('plugin.video.plexkodiconnect') - -ACTION_BACK = 92 -SIGN_IN = 200 -REMIND_LATER = 201 - - -class LoginConnect(xbmcgui.WindowXMLDialog): - - - def __init__(self, *args, **kwargs): - - xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - - def __add_editcontrol(self, x, y, height, width, password=0): - - media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') - control = xbmcgui.ControlEdit(0,0,0,0, - label="User", - font="font10", - textColor="ff464646", - focusTexture=os.path.join(media, "button-focus.png"), - noFocusTexture=os.path.join(media, "button-focus.png"), - isPassword=password) - - control.setPosition(x,y) - control.setHeight(height) - control.setWidth(width) - - self.addControl(control) - return control - - def onInit(self): - - self.user_field = self.__add_editcontrol(685,385,40,500) - self.setFocus(self.user_field) - self.password_field = self.__add_editcontrol(685,470,40,500, password=1) - self.signin_button = self.getControl(SIGN_IN) - self.remind_button = self.getControl(REMIND_LATER) - - self.user_field.controlUp(self.remind_button) - self.user_field.controlDown(self.password_field) - self.password_field.controlUp(self.user_field) - self.password_field.controlDown(self.signin_button) - self.signin_button.controlUp(self.password_field) - self.remind_button.controlDown(self.user_field) - - def onClick(self, control): - - if control == SIGN_IN: - # Sign in to emby connect - self.user = self.user_field.getText() - __password = self.password_field.getText() - - ### REVIEW ONCE CONNECT MODULE IS MADE - self.close() - - elif control == REMIND_LATER: - # Remind me later - self.close() - - def onAction(self, action): - - if action == ACTION_BACK: - self.close() \ No newline at end of file diff --git a/resources/lib/dialogs/__init__.py b/resources/lib/dialogs/__init__.py new file mode 100644 index 00000000..92c6f42a --- /dev/null +++ b/resources/lib/dialogs/__init__.py @@ -0,0 +1,6 @@ +# Dummy file to make this directory a package. +# from serverconnect import ServerConnect +# from usersconnect import UsersConnect +# from loginconnect import LoginConnect +# from loginmanual import LoginManual +# from servermanual import ServerManual diff --git a/resources/lib/dialogs/context.py b/resources/lib/dialogs/context.py new file mode 100644 index 00000000..4ee9f038 --- /dev/null +++ b/resources/lib/dialogs/context.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +############################################################################### + +import logging +import os + +import xbmcgui +import xbmcaddon + +from utils import window + +############################################################################### + +log = logging.getLogger("PLEX."+__name__) +addon = xbmcaddon.Addon('plugin.video.plexkodiconnect') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +LIST = 155 +USER_IMAGE = 150 + +############################################################################### + + +class ContextMenu(xbmcgui.WindowXMLDialog): + + _options = [] + selected_option = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_options(self, options=[]): + self._options = options + + def is_selected(self): + return True if self.selected_option else False + + def get_selected(self): + return self.selected_option + + def onInit(self): + + if window('PlexUserImage'): + self.getControl(USER_IMAGE).setImage(window('PlexUserImage')) + + height = 479 + (len(self._options) * 55) + log.info("options: %s", self._options) + self.list_ = self.getControl(LIST) + + for option in self._options: + self.list_.addItem(self._add_listitem(option)) + + self.background = self._add_editcontrol(730, height, 30, 450) + self.setFocus(self.list_) + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + option = self.list_.getSelectedItem() + self.selected_option = option.getLabel() + log.info('option selected: %s', self.selected_option) + + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlImage(0, 0, 0, 0, + filename=os.path.join(media, "white.png"), + aspectRatio=0, + colorDiffuse="ff111111") + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + @classmethod + def _add_listitem(cls, label): + return xbmcgui.ListItem(label) diff --git a/resources/lib/dialogs/loginconnect.py b/resources/lib/dialogs/loginconnect.py new file mode 100644 index 00000000..db7c39cc --- /dev/null +++ b/resources/lib/dialogs/loginconnect.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +SIGN_IN = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class LoginConnect(xbmcgui.WindowXMLDialog): + + _user = None + error = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_connect_manager(self, connect_manager): + self.connect_manager = connect_manager + + def is_logged_in(self): + return True if self._user else False + + def get_user(self): + return self._user + + + def onInit(self): + + self.user_field = self._add_editcontrol(725, 385, 40, 500) + self.setFocus(self.user_field) + self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1) + self.signin_button = self.getControl(SIGN_IN) + self.remind_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + + self.user_field.controlUp(self.remind_button) + self.user_field.controlDown(self.password_field) + self.password_field.controlUp(self.user_field) + self.password_field.controlDown(self.signin_button) + self.signin_button.controlUp(self.password_field) + self.remind_button.controlDown(self.user_field) + + def onClick(self, control): + + if control == SIGN_IN: + # Sign in to emby connect + self._disable_error() + + user = self.user_field.getText() + password = self.password_field.getText() + + if not user or not password: + # Display error + self._error(ERROR['Empty'], lang(30608)) + log.error("Username or password cannot be null") + + elif self._login(user, password): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if (self.error == ERROR['Empty'] + and self.user_field.getText() and self.password_field.getText()): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ff525252", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png"), + isPassword=password) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _login(self, username, password): + + result = self.connect_manager.loginToConnect(username, password) + if result is False: + self._error(ERROR['Invalid'], lang(33009)) + return False + else: + self._user = result + return True + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/loginmanual.py b/resources/lib/dialogs/loginmanual.py new file mode 100644 index 00000000..7db1adcf --- /dev/null +++ b/resources/lib/dialogs/loginmanual.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +import read_embyserver as embyserver +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +SIGN_IN = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class LoginManual(xbmcgui.WindowXMLDialog): + + _user = None + error = None + username = None + + + def __init__(self, *args, **kwargs): + + self.emby = embyserver.Read_EmbyServer() + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def is_logged_in(self): + return True if self._user else False + + def set_server(self, server): + self.server = server + + def set_user(self, user): + self.username = user or {} + + def get_user(self): + return self._user + + def onInit(self): + + self.signin_button = self.getControl(SIGN_IN) + self.cancel_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + self.user_field = self._add_editcontrol(725, 400, 40, 500) + self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1) + + if self.username: + self.user_field.setText(self.username) + self.setFocus(self.password_field) + else: + self.setFocus(self.user_field) + + self.user_field.controlUp(self.cancel_button) + self.user_field.controlDown(self.password_field) + self.password_field.controlUp(self.user_field) + self.password_field.controlDown(self.signin_button) + self.signin_button.controlUp(self.password_field) + self.cancel_button.controlDown(self.user_field) + + def onClick(self, control): + + if control == SIGN_IN: + # Sign in to emby connect + self._disable_error() + + user = self.user_field.getText() + password = self.password_field.getText() + + if not user: + # Display error + self._error(ERROR['Empty'], lang(30613)) + log.error("Username cannot be null") + + elif self._login(user, password): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if self.error == ERROR['Empty'] and self.user_field.getText(): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width, password=0): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ff525252", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png"), + isPassword=password) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _login(self, username, password): + + result = self.emby.loginUser(self.server, username, password) + if not result: + self._error(ERROR['Invalid'], lang(33009)) + return False + else: + self._user = result + return True + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/serverconnect.py b/resources/lib/dialogs/serverconnect.py new file mode 100644 index 00000000..541ca6f9 --- /dev/null +++ b/resources/lib/dialogs/serverconnect.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui + +import connect.connectionmanager as connectionmanager +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +CONN_STATE = connectionmanager.ConnectionState +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +USER_IMAGE = 150 +USER_NAME = 151 +LIST = 155 +CANCEL = 201 +MESSAGE_BOX = 202 +MESSAGE = 203 +BUSY = 204 +EMBY_CONNECT = 205 +MANUAL_SERVER = 206 + +################################################################################################## + + +class ServerConnect(xbmcgui.WindowXMLDialog): + + username = "" + user_image = None + servers = [] + + _selected_server = None + _connect_login = False + _manual_server = False + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_args(self, **kwargs): + # connect_manager, username, user_image, servers, emby_connect + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + def is_server_selected(self): + return True if self._selected_server else False + + def get_server(self): + return self._selected_server + + def is_connect_login(self): + return self._connect_login + + def is_manual_server(self): + return self._manual_server + + + def onInit(self): + + self.message = self.getControl(MESSAGE) + self.message_box = self.getControl(MESSAGE_BOX) + self.busy = self.getControl(BUSY) + self.list_ = self.getControl(LIST) + + for server in self.servers: + server_type = "wifi" if server.get('ExchangeToken') else "network" + self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) + + self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8'))) + + if self.user_image is not None: + self.getControl(USER_IMAGE).setImage(self.user_image) + + if not self.emby_connect: # Change connect user + self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]") + + if self.servers: + self.setFocus(self.list_) + + @classmethod + def _add_listitem(cls, label, server_id, server_type): + + item = xbmcgui.ListItem(label) + item.setProperty('id', server_id) + item.setProperty('server_type', server_type) + + return item + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + server = self.list_.getSelectedItem() + selected_id = server.getProperty('id') + log.info('Server Id selected: %s', selected_id) + + if self._connect_server(selected_id): + self.message_box.setVisibleCondition('False') + self.close() + + def onClick(self, control): + + if control == EMBY_CONNECT: + self.connect_manager.clearData() + self._connect_login = True + self.close() + + elif control == MANUAL_SERVER: + self._manual_server = True + self.close() + + elif control == CANCEL: + self.close() + + def _connect_server(self, server_id): + + server = self.connect_manager.getServerInfo(server_id) + self.message.setLabel("%s %s..." % (lang(30610), server['Name'])) + self.message_box.setVisibleCondition('True') + self.busy.setVisibleCondition('True') + result = self.connect_manager.connectToServer(server) + + if result['State'] == CONN_STATE['Unavailable']: + self.busy.setVisibleCondition('False') + self.message.setLabel(lang(30609)) + return False + else: + xbmc.sleep(1000) + self._selected_server = result['Servers'][0] + return True diff --git a/resources/lib/dialogs/servermanual.py b/resources/lib/dialogs/servermanual.py new file mode 100644 index 00000000..d54199eb --- /dev/null +++ b/resources/lib/dialogs/servermanual.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging +import os + +import xbmcgui +import xbmcaddon + +import connect.connectionmanager as connectionmanager +from utils import language as lang + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) +addon = xbmcaddon.Addon('plugin.video.emby') + +CONN_STATE = connectionmanager.ConnectionState +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +CONNECT = 200 +CANCEL = 201 +ERROR_TOGGLE = 202 +ERROR_MSG = 203 +ERROR = { + 'Invalid': 1, + 'Empty': 2 +} + +################################################################################################## + + +class ServerManual(xbmcgui.WindowXMLDialog): + + _server = None + error = None + + + def __init__(self, *args, **kwargs): + + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_connect_manager(self, connect_manager): + self.connect_manager = connect_manager + + def is_connected(self): + return True if self._server else False + + def get_server(self): + return self._server + + def onInit(self): + + self.connect_button = self.getControl(CONNECT) + self.cancel_button = self.getControl(CANCEL) + self.error_toggle = self.getControl(ERROR_TOGGLE) + self.error_msg = self.getControl(ERROR_MSG) + self.host_field = self._add_editcontrol(725, 400, 40, 500) + self.port_field = self._add_editcontrol(725, 525, 40, 500) + + self.port_field.setText('8096') + self.setFocus(self.host_field) + + self.host_field.controlUp(self.cancel_button) + self.host_field.controlDown(self.port_field) + self.port_field.controlUp(self.host_field) + self.port_field.controlDown(self.connect_button) + self.connect_button.controlUp(self.port_field) + self.cancel_button.controlDown(self.host_field) + + def onClick(self, control): + + if control == CONNECT: + # Sign in to emby connect + self._disable_error() + + server = self.host_field.getText() + port = self.port_field.getText() + + if not server or not port: + # Display error + self._error(ERROR['Empty'], lang(30617)) + log.error("Server or port cannot be null") + + elif self._connect_to_server(server, port): + self.close() + + elif control == CANCEL: + # Remind me later + self.close() + + def onAction(self, action): + + if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText(): + self._disable_error() + + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def _add_editcontrol(self, x, y, height, width): + + media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media') + control = xbmcgui.ControlEdit(0, 0, 0, 0, + label="User", + font="font10", + textColor="ffc2c2c2", + focusTexture=os.path.join(media, "button-focus.png"), + noFocusTexture=os.path.join(media, "button-focus.png")) + control.setPosition(x, y) + control.setHeight(height) + control.setWidth(width) + + self.addControl(control) + return control + + def _connect_to_server(self, server, port): + + server_address = "%s:%s" % (server, port) + self._message("%s %s..." % (lang(30610), server_address)) + result = self.connect_manager.connectToAddress(server_address) + + if result['State'] == CONN_STATE['Unavailable']: + self._message(lang(30609)) + return False + else: + self._server = result['Servers'][0] + return True + + def _message(self, message): + + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _error(self, state, message): + + self.error = state + self.error_msg.setLabel(message) + self.error_toggle.setVisibleCondition('True') + + def _disable_error(self): + + self.error = None + self.error_toggle.setVisibleCondition('False') diff --git a/resources/lib/dialogs/usersconnect.py b/resources/lib/dialogs/usersconnect.py new file mode 100644 index 00000000..770b0a2c --- /dev/null +++ b/resources/lib/dialogs/usersconnect.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +################################################################################################## + +import logging + +import xbmc +import xbmcgui + +################################################################################################## + +log = logging.getLogger("EMBY."+__name__) + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +ACTION_SELECT_ITEM = 7 +ACTION_MOUSE_LEFT_CLICK = 100 +LIST = 155 +MANUAL = 200 +CANCEL = 201 + +################################################################################################## + + +class UsersConnect(xbmcgui.WindowXMLDialog): + + _user = None + _manual_login = False + + + def __init__(self, *args, **kwargs): + + self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_server(self, server): + self.server = server + + def set_users(self, users): + self.users = users + + def is_user_selected(self): + return True if self._user else False + + def get_user(self): + return self._user + + def is_manual_login(self): + return self._manual_login + + + def onInit(self): + + self.list_ = self.getControl(LIST) + for user in self.users: + user_image = ("userflyoutdefault2.png" if 'PrimaryImageTag' not in user + else self._get_user_artwork(user['Id'], 'Primary')) + self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) + + self.setFocus(self.list_) + + def _add_listitem(self, label, user_id, user_image): + + item = xbmcgui.ListItem(label) + item.setProperty('id', user_id) + if self.kodi_version > 15: + item.setArt({'Icon': user_image}) + else: + item.setIconImage(user_image) + + return item + + def onAction(self, action): + + if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): + self.close() + + if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK): + + if self.getFocusId() == LIST: + user = self.list_.getSelectedItem() + selected_id = user.getProperty('id') + log.info('User Id selected: %s', selected_id) + + for user in self.users: + if user['Id'] == selected_id: + self._user = user + break + + self.close() + + def onClick(self, control): + + if control == MANUAL: + self._manual_login = True + self.close() + + elif control == CANCEL: + self.close() + + def _get_user_artwork(self, user_id, item_type): + # Load user information set by UserClient + return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 5cac1315..589115d6 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -42,28 +42,18 @@ class KodiMonitor(xbmc.Monitor): window('plex_kodiScan', clear=True) def onSettingsChanged(self): - # Monitor emby settings - # Review reset setting at a later time, need to be adjusted to account for initial setup - # changes. - '''currentPath = settings('useDirectPaths') - if window('plex_pluginpath') != currentPath: - # Plugin path value changed. Offer to reset - log.info("Changed to playback mode detected") - window('plex_pluginpath', value=currentPath) - resp = xbmcgui.Dialog().yesno( - heading="Playback mode change detected", - line1=( - "Detected the playback mode has changed. The database " - "needs to be recreated for the change to be applied. " - "Proceed?")) - if resp: - utils.reset()''' - + """ + Monitor the PKC settings for changes made by the user + """ currentLog = settings('logLevel') if window('plex_logLevel') != currentLog: # The log level changed, set new prop log.debug("New log level: %s" % currentLog) window('plex_logLevel', value=currentLog) + current_context = "true" if settings('enableContext') == "true" else "" + if window('plex_context') != current_context: + log.info("New context setting: %s", current_context) + window('plex_context', value=current_context) @CatchExceptions(warnuser=False) def onNotification(self, sender, method, data): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index ceae438a..61666041 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -73,6 +73,29 @@ def language(stringid): return ADDON.getLocalizedString(stringid) +def dialog(type_, *args, **kwargs): + + d = xbmcgui.Dialog() + + if "icon" in kwargs: + kwargs['icon'] = kwargs['icon'].replace( + "{plex}", + "special://home/addons/plugin.video.plexkodiconnect/icon.png") + if "heading" in kwargs: + kwargs['heading'] = kwargs['heading'].replace("{plex}", + language(29999)) + + types = { + 'yesno': d.yesno, + 'ok': d.ok, + 'notification': d.notification, + 'input': d.input, + 'select': d.select, + 'numeric': d.numeric + } + return types[type_](*args, **kwargs) + + def tryEncode(uniString, encoding='utf-8'): """ Will try to encode uniString (in unicode) to encoding. This possibly diff --git a/resources/settings.xml b/resources/settings.xml index 9c18a978..bcaca8be 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -38,6 +38,8 @@ + + @@ -117,7 +119,6 @@ - - 320 - 30 - left - 40 - 100% - font16 - ffeeeeee - - - - separator - -10 - 103 - 101% - 1 - separator.png - - - 50 - 600 - vertical - left - 20 - - - 8 - 8012 - 8012 - 100 - 785 - left - 70 - radio-on.png - radio-off.png - white.png - 750 - 32 - 32 - 30 - B3dddddd - - - - - - - separator - 70 - -10 - 101% - 1 - separator.png - - - Save - 150 - 20 - 230 - 8011 - 8013 - 8013 - font14 - - FFededed - B3dddddd - FF000000 - 40 - ff333333 - box.png - box.png - - - Cancel - 430 - 20 - 230 - 8011 - 8012 - 8012 - font14 - - FFededed - B3dddddd - FF000000 - 40 - ff333333 - box.png - box.png - - - - - \ No newline at end of file diff --git a/resources/skins/default/media/emby-icon.png b/resources/skins/default/media/emby-icon.png new file mode 100644 index 00000000..52a30539 Binary files /dev/null and b/resources/skins/default/media/emby-icon.png differ diff --git a/resources/skins/default/media/separator.png b/resources/skins/default/media/emby-separator.png similarity index 100% rename from resources/skins/default/media/separator.png rename to resources/skins/default/media/emby-separator.png diff --git a/resources/skins/default/media/fading_circle.png b/resources/skins/default/media/fading_circle.png new file mode 100644 index 00000000..56aea399 Binary files /dev/null and b/resources/skins/default/media/fading_circle.png differ diff --git a/resources/skins/default/media/network.png b/resources/skins/default/media/network.png new file mode 100644 index 00000000..7cd11ddf Binary files /dev/null and b/resources/skins/default/media/network.png differ diff --git a/resources/skins/default/media/user_image.png b/resources/skins/default/media/user_image.png new file mode 100644 index 00000000..194a78b1 Binary files /dev/null and b/resources/skins/default/media/user_image.png differ diff --git a/resources/skins/default/media/userflyoutdefault.png b/resources/skins/default/media/userflyoutdefault.png new file mode 100644 index 00000000..046676e5 Binary files /dev/null and b/resources/skins/default/media/userflyoutdefault.png differ diff --git a/resources/skins/default/media/userflyoutdefault2.png b/resources/skins/default/media/userflyoutdefault2.png new file mode 100644 index 00000000..24d771eb Binary files /dev/null and b/resources/skins/default/media/userflyoutdefault2.png differ diff --git a/resources/skins/default/media/wifi.png b/resources/skins/default/media/wifi.png new file mode 100644 index 00000000..2a646c55 Binary files /dev/null and b/resources/skins/default/media/wifi.png differ diff --git a/service.py b/service.py index efdbd2d8..799b91cd 100644 --- a/service.py +++ b/service.py @@ -50,7 +50,7 @@ import PlexCompanion import loghandler loghandler.config() -log = logging.getLogger("PLEX.default") +log = logging.getLogger("PLEX.service") addonName = 'PlexKodiConnect' ############################################################################### @@ -77,6 +77,8 @@ class Service(): window('plex_logLevel', value=str(logLevel)) window('plex_kodiProfile', value=xbmc.translatePath("special://profile")) + window('plex_context', + value='true' if settings('enableContext') == "true" else "") # Initial logging log.warn("======== START %s ========" % addonName)