# -*- coding: utf-8 -*- import json import os import re import unicodedata import uuid import _strptime # Workaround for threads using datetime: _striptime is locked from datetime import datetime, timedelta from dateutil import tz, parser try: from urllib import quote_plus, quote except: from urllib.parse import quote_plus, quote unicode = str import xbmc import xbmcaddon import xbmcgui import xbmcvfs from . import xmls from . import settings from . import loghandler from . import jsonrpc class Utils(): def __init__(self): self.LOG = loghandler.LOG('EMBY.helper.utils.Utils') self.Addon = xbmcaddon.Addon("plugin.video.emby-next-gen") self.Settings = settings.Settings(self.Addon) self.WindowID = xbmcgui.Window(10000) self.KodiVersion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) self.STRINGS = { 'addon_name': 29999, 'playback_mode': 30511, 'empty_user': 30613, 'empty_user_pass': 30608, 'empty_server': 30617, 'network_credentials': 30517, 'invalid_auth': 33009, 'addon_mode': 33036, 'native_mode': 33037, 'cancel': 30606, 'username': 30024, 'password': 30602, 'gathering': 33021, 'boxsets': 30185, 'movies': 30302, 'tvshows': 30305, 'fav_movies': 30180, 'fav_tvshows': 30181, 'fav_episodes': 30182, 'task_success': 33203, 'task_fail': 33204 } self.Settings.InitSettings() self.Dialog = xbmcgui.Dialog() self.DeviceName = xbmc.getInfoLabel('System.FriendlyName') self.DeviceID = "" self.device_id = self.get_device_id(False) self.SyncData = self.load_sync() self.PathVerified = False self.DefaultVideoSettings = None self.addon_name = xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('name').upper() self.addon_version = xbmcaddon.Addon("plugin.video.emby-next-gen").getAddonInfo('version') self.device_name = self.get_device_name() self.direct_path = self.Settings.useDirectPaths == "1" self.device_info = {'DeviceName': self.device_name, 'Version': self.addon_version, 'DeviceId': self.device_id} self.Screensaver = None self.SyncTimestampLast = datetime.fromtimestamp(0) self.DatabaseFiles = self.load_DatabaseFiles() def load_DatabaseFiles(self): DatabaseFiles = { 'emby': self.translatePath("special://database/") + "emby.db", 'texture': "", 'texture-version': 0, 'music': "", 'music-version': 0, 'video': "", 'video-version': 0 } folder = self.translatePath("special://database/") _, files = xbmcvfs.listdir(folder) for Filename in files: if not Filename.endswith('-wal') and not Filename.endswith('-shm') and not Filename.endswith('db-journal'): if Filename.startswith('Textures'): Version = int(''.join(i for i in Filename if i.isdigit())) if Version > DatabaseFiles['texture-version']: DatabaseFiles['texture'] = os.path.join(folder, Filename) DatabaseFiles['texture-version'] = Version elif Filename.startswith('MyMusic'): Version = int(''.join(i for i in Filename if i.isdigit())) if Version > DatabaseFiles['music-version']: DatabaseFiles['music'] = os.path.join(folder, Filename) DatabaseFiles['music-version'] = Version elif Filename.startswith('MyVideos'): Version = int(''.join(i for i in Filename if i.isdigit())) if Version > DatabaseFiles['video-version']: DatabaseFiles['video'] = os.path.join(folder, Filename) DatabaseFiles['video-version'] = Version return DatabaseFiles def load_defaultvideosettings(self): if not self.DefaultVideoSettings: #load from file self.DefaultVideoSettings = xmls.Xmls(self).load_defaultvideosettings() def load_sync(self): SyncData = self.Settings.emby_servers_sync if not SyncData: #load from file path = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) if xbmcvfs.exists(os.path.join(path, "sync.json")): with open(os.path.join(path, 'sync.json'), 'rb') as infile: SyncData = json.load(infile) else: SyncData = {} SyncData['Libraries'] = SyncData.get('Libraries', []) SyncData['RestorePoint'] = SyncData.get('RestorePoint', {}) SyncData['Whitelist'] = list(set(SyncData.get('Whitelist', []))) SyncData['SortedViews'] = SyncData.get('SortedViews', []) self.Settings.emby_servers_sync = SyncData return SyncData def save_sync(self, Data, ForceSave=False): CurrentDate = datetime.utcnow() if not ForceSave: if (CurrentDate - self.SyncTimestampLast).seconds <= 30: #save every 30 seconds on sync progress return self.SyncTimestampLast = CurrentDate Data['Date'] = CurrentDate.strftime('%Y-%m-%dT%H:%M:%SZ') path = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/") if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) with open(os.path.join(path, 'sync.json'), 'wb') as outfile: output = json.dumps(Data, sort_keys=True, indent=4, ensure_ascii=False) outfile.write(output.encode('utf-8')) self.Settings.emby_servers_sync = Data self.SyncData = Data def save_last_sync(self): time_now = datetime.utcnow() - timedelta(minutes=2) last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') self.Settings.set_settings('LastIncrementalSync', last_sync) self.LOG.info("--[ sync/%s ]" % last_sync) def get_device_id(self, reset): if self.DeviceID: return self.DeviceID directory = self.translatePath('special://profile/addon_data/plugin.video.emby-next-gen/') if not xbmcvfs.exists(directory): xbmcvfs.mkdir(directory) emby_guid = os.path.join(directory, "emby_guid") file_guid = xbmcvfs.File(emby_guid) self.DeviceID = file_guid.read() if not self.DeviceID or reset: self.LOG.info("Generating a new GUID.") self.DeviceID = str(self.create_id()) file_guid = xbmcvfs.File(emby_guid, 'w') file_guid.write(self.DeviceID) file_guid.close() self.LOG.info("DeviceId loaded: %s" % self.DeviceID) return self.DeviceID def get_device_name(self): ''' Detect the device name. If deviceNameOpt, then use the device name in the add-on settings. Otherwise fallback to the Kodi device name. ''' if not self.Settings.deviceNameOpt: device_name = self.DeviceName else: device_name = self.Settings.deviceName device_name = device_name.replace("\"", "_") device_name = device_name.replace("/", "_") if not device_name: device_name = "Kodi" return device_name def reset_device_id(self): self.DeviceID = "" self.get_device_id(True) self.dialog("ok", heading="{emby}", line1=self.Translate(33033)) xbmc.executebuiltin('RestartApp') #Download external subtitles to temp folder def download_file_from_Embyserver(self, request, filename, EmbyServer): temp = self.translatePath("special://profile/addon_data/plugin.video.emby-next-gen/temp/") if not xbmcvfs.exists(temp): xbmcvfs.mkdir(temp) path = os.path.join(temp, filename) response = EmbyServer.http.request(request, True, True) if response: with open(path, 'wb') as f: f.write(response) return path return None def create_id(self): return uuid.uuid4() #Find value in dictionary def find(self, dictData, item, beta): if item in dictData: return dictData[item], item for key, _ in sorted(iter(list(dictData.items())), key=lambda k_v: (k_v[1], k_v[0])): if re.match(key, item, re.I): return dictData[key], key if beta: return self.find(dictData, item.replace('beta-', ""), False) def dialog(self, dialog_type, *args, **kwargs): if "icon" in kwargs: kwargs['icon'] = kwargs['icon'].replace("{emby}", "special://home/addons/plugin.video.emby-next-gen/resources/icon.png") if "heading" in kwargs: kwargs['heading'] = kwargs['heading'].replace("{emby}", self.Translate('addon_name')) if self.KodiVersion >= 19: if "line1" in kwargs: kwargs['message'] = kwargs['line1'] del kwargs['line1'] types = { 'yesno': self.Dialog.yesno, 'ok': self.Dialog.ok, 'notification': self.Dialog.notification, 'input': self.Dialog.input, 'select': self.Dialog.select, 'numeric': self.Dialog.numeric, 'multi': self.Dialog.multiselect, 'textviewer': self.Dialog.textviewer } return types[dialog_type](*args, **kwargs) #Toggle the screensaver def set_screensaver(self, value): params = { 'setting': "screensaver.mode", 'value': value } result = jsonrpc.JSONRPC('Settings.setSettingValue').execute(params) self.Screensaver = value self.LOG.info("---[ screensaver/%s ] %s" % (value, result)) #Verify if path is accessible def validate(self, path): if self.PathVerified: return True path = self.StringDecode(path) string = "%s %s. %s" % (self.Translate(33047), path, self.Translate(33048)) if not os.path.supports_unicode_filenames: path = path.encode('utf-8') if not xbmcvfs.exists(path): self.LOG.info("Could not find %s" % path) if self.dialog("yesno", heading="{emby}", line1=string): return False self.PathVerified = True return True #Grab the values in the item for a list of keys {key},{key1}.... #If the key has no brackets, the key will be passed as is def values(self, item, keys): return (item[key.replace('{', "").replace('}', "")] if isinstance(key, str) and key.startswith('{') else key for key in keys) #Prettify xml docs def indent(self, elem, level): try: i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: self.indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i except Exception: return def write_xml(self, content, Filepath): try: with open(Filepath, 'wb') as infile: content = content.replace(b"'", b'"') content = content.replace(b'?>', b' standalone="yes" ?>', 1) infile.write(content) except Exception as error: self.LOG.error(error) #Delete objects from kodi cache def delete_folder(self, path): self.LOG.debug("--[ delete folder ]") delete_path = path is not None path = path or self.translatePath('special://temp/emby') dirs, files = xbmcvfs.listdir(path) self.delete_recursive(path, dirs) for Filename in files: xbmcvfs.delete(os.path.join(path, Filename)) if delete_path: xbmcvfs.delete(path) self.LOG.warning("DELETE %s" % path) #Delete files and dirs recursively def delete_recursive(self, path, dirs): for directory in dirs: dirs2, files = xbmcvfs.listdir(os.path.join(path, directory)) for Filename in files: xbmcvfs.delete(os.path.join(path, directory, Filename)) self.delete_recursive(os.path.join(path, directory), dirs2) xbmcvfs.rmdir(os.path.join(path, directory)) #Unzip file. zipfile module seems to fail on android with badziperror def unzip(self, path, dest, folder): path = quote_plus(path) root = "zip://" + path + '/' if folder: xbmcvfs.mkdir(os.path.join(dest, folder)) dest = os.path.join(dest, folder) root = self.get_zip_directory(root, folder) dirs, files = xbmcvfs.listdir(root) if dirs: self.unzip_recursive(root, dirs, dest) for Filename in files: self.unzip_file(os.path.join(root, Filename), os.path.join(dest, Filename)) self.LOG.warning("Unzipped %s" % path) def unzip_recursive(self, path, dirs, dest): for directory in dirs: dirs_dir = os.path.join(path, directory) dest_dir = os.path.join(dest, directory) xbmcvfs.mkdir(dest_dir) dirs2, files = xbmcvfs.listdir(dirs_dir) if dirs2: self.unzip_recursive(dirs_dir, dirs2, dest_dir) for Filename in files: self.unzip_file(os.path.join(dirs_dir, Filename), os.path.join(dest_dir, Filename)) #Unzip specific file. Path should start with zip:// def unzip_file(self, path, dest): xbmcvfs.copy(path, dest) self.LOG.debug("unzip: %s to %s" % (path, dest)) def get_zip_directory(self, path, folder): dirs, _ = xbmcvfs.listdir(path) if folder in dirs: return os.path.join(path, folder) for directory in dirs: result = self.get_zip_directory(os.path.join(path, directory), folder) if result: return result #Copy folder content from one to another def copytree(self, path, dest): dirs, files = xbmcvfs.listdir(path) if not xbmcvfs.exists(dest): xbmcvfs.mkdirs(dest) if dirs: self.copy_recursive(path, dirs, dest) for Filename in files: self.copy_file(os.path.join(path, Filename), os.path.join(dest, Filename)) self.LOG.info("Copied %s" % path) def copy_recursive(self, path, dirs, dest): for directory in dirs: dirs_dir = os.path.join(path, directory) dest_dir = os.path.join(dest, directory) xbmcvfs.mkdir(dest_dir) dirs2, files = xbmcvfs.listdir(dirs_dir) if dirs2: self.copy_recursive(dirs_dir, dirs2, dest_dir) for Filename in files: self.copy_file(os.path.join(dirs_dir, Filename), os.path.join(dest_dir, Filename)) #Copy specific file def copy_file(self, path, dest): if path.endswith('.pyo'): return xbmcvfs.copy(path, dest) self.LOG.debug("copy: %s to %s" % (path, dest)) #Delete pyo files to force Kodi to recreate them def delete_pyo(self, path): dirs, files = xbmcvfs.listdir(path) if dirs: for directory in dirs: self.delete_pyo(os.path.join(path, directory)) for Filename in files: if Filename.endswith('.pyo'): xbmcvfs.delete(os.path.join(path, Filename)) #For theme media, do not modify unless modified in TV Tunes. #Remove dots from the last character as windows can not have directories with dots at the end def normalize_string(self, text): text = text.replace(":", "") text = text.replace("/", "-") text = text.replace("\\", "-") text = text.replace("<", "") text = text.replace(">", "") text = text.replace("*", "") text = text.replace("?", "") text = text.replace('|', "") text = text.strip() text = text.rstrip('.') if self.KodiVersion >= 19: text = unicodedata.normalize('NFKD', text) else: text = unicodedata.normalize('NFKD', text).encode('utf-8') return text def direct_url(self, item): FilenameURL = self.PathToFilenameReplaceSpecialCharecters(item['Path']) if item['Type'] == 'Audio': Type = 'audio' else: Type = 'video' path = "http://127.0.0.1:57578/%s/%s-%s-stream-%s" % (Type, item['Id'], item['MediaSources'][0]['Id'], FilenameURL) return path #Split up list in pieces of size. Will generate a list of lists def split_list(self, itemlist): size = int(self.Settings.limitIndex) return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] #Convert the local datetime to local def convert_to_local(self, date): try: if not date: return "" if isinstance(date, int): date = str(date) if isinstance(date, (str, unicode)): date = parser.parse(date.encode('utf-8')) date = date.replace(tzinfo=tz.tzutc()) date = date.astimezone(tz.tzlocal()) return date.strftime('%Y-%m-%dT%H:%M:%S') except Exception as error: self.LOG.error(error) self.LOG.info("date: %s" % str(date)) return "" def set_queryIO(self, key): self.WindowID.setProperty(key, "1") def StringDecode(self, Data): if self.KodiVersion <= 18: try: Data = Data.decode('utf-8') except: Data = Data.encode('utf8').decode('utf-8') return Data def Translate(self, String): if isinstance(String, str): String = self.STRINGS[String] result = self.Addon.getLocalizedString(String) if not result: result = xbmc.getLocalizedString(String) return result def PathToFilenameReplaceSpecialCharecters(self, Path): Temp = Path Pos = Temp.rfind("/") if Pos == -1: #Windows Pos = Temp.rfind("\\") Temp = Temp[Pos + 1:] if self.KodiVersion <= 18: if isinstance(Temp, str): Temp = unicode(Temp, 'utf-8') Temp = Temp.encode('utf-8') Filename = quote(Temp, safe=u':/'.encode('utf-8')) else: Filename = quote(Temp.encode('utf-8'), safe=u':/'.encode('utf-8')) else: Filename = quote(Temp) while Filename.find("%") != -1: Pos = Filename.find("%") Filename = Filename.replace(Filename[Pos:Pos + 3], "_") return Filename def ReplaceSpecialCharecters(self, Data): if self.KodiVersion <= 18: try: Data = unicode(Data, 'utf-8') except: pass Data = Data.encode('utf-8') Data = quote(Data, safe=u':/'.encode('utf-8')) else: Data = quote(Data) Data = Data.replace("%", "") return Data def StringMod(self, Data): if self.KodiVersion >= 19: return Data return Data.encode('utf-8') def translatePath(self, Data): if self.KodiVersion >= 19: return xbmcvfs.translatePath(Data) return xbmc.translatePath(Data) def CreateListitem(self, MediaType, Data): li = xbmcgui.ListItem(Data['title']) Data['mediatype'] = MediaType Properties = {'IsPlayable': "true"} if "resume" in Data: Properties['resumetime'] = str(Data['resume']['position']) Properties['totaltime'] = str(Data['resume']['total']) del Data['resume'] if "art" in Data: li.setArt(Data['art']) del Data['art'] if 'cast' in Data: li.setCast(Data['cast']) del Data['cast'] if 'uniqueid' in Data: li.setUniqueIDs(Data['uniqueid']) del Data['uniqueid'] if "streamdetails" in Data: for key, value in list(Data['streamdetails'].items()): for stream in value: li.addStreamInfo(key, stream) del Data['streamdetails'] if "showtitle" in Data: Data['TVshowTitle'] = Data['showtitle'] del Data["showtitle"] if "firstaired" in Data: Data['premiered'] = Data['firstaired'] del Data["firstaired"] if "specialsortepisode" in Data: Data['sortseason'] = Data['specialsortepisode'] del Data["specialsortepisode"] if "specialsortseason" in Data: Data['sortepisode'] = Data['specialsortseason'] del Data["specialsortseason"] if "file" in Data: del Data["file"] if "label" in Data: li.setLabel(Data['label']) del Data["label"] if "seasonid" in Data: del Data["seasonid"] if "episodeid" in Data: del Data["episodeid"] if "movieid" in Data: del Data["movieid"] if "musicvideoid" in Data: del Data["musicvideoid"] li.setInfo('video', Data) return li