diff --git a/platformcode/launcher.py b/platformcode/launcher.py index bc1e52a4..3551c397 100644 --- a/platformcode/launcher.py +++ b/platformcode/launcher.py @@ -64,6 +64,8 @@ def run(item=None): else: item = Item(channel="channelselector", action="getmainlist", viewmode="movie") if not config.get_setting('show_once'): + from platformcode import updater + updater.calcCurrHash() from platformcode import xbmc_videolibrary xbmc_videolibrary.ask_set_content(1, config.get_setting('videolibrary_kodi_force')) config.set_setting('show_once', True) @@ -75,9 +77,6 @@ def run(item=None): if item.action == "": logger.info("Item sin accion") return - - if item.action == "update": - updater.update() # Action for main menu in channelselector elif item.action == "getmainlist": diff --git a/platformcode/updater.py b/platformcode/updater.py index 4efb0ea9..857ebf8b 100644 --- a/platformcode/updater.py +++ b/platformcode/updater.py @@ -1,314 +1,309 @@ # -*- coding: utf-8 -*- -# -------------------------------------------------------------------------------- -# Updater (kodi) -# -------------------------------------------------------------------------------- - -import json +import hashlib import os -import sys -import threading -import time -import urllib +import shutil +import zipfile +from core import httptools, filetools, downloadtools +from platformcode import logger, platformtools, config +import json import xbmc +import re +import xbmcaddon -from core import ziptools -from platformcode import config, logger +addon = xbmcaddon.Addon('plugin.video.kod') + +_hdr_pat = re.compile("^@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@.*") + +# branch = 'stable' +branch = 'updater' +# user = 'kodiondemand' +user = 'mac12m99' +repo = 'addon' +addonDir = xbmc.translatePath("special://home/addons/") + "plugin.video.kod/" +maxPage = 5 # le api restituiscono 30 commit per volta, quindi se si è rimasti troppo indietro c'è bisogno di andare avanti con le pagine +trackingFile = "last_commit.txt" + + +def loadCommits(page=1): + apiLink = 'https://api.github.com/repos/' + user + '/' + repo + '/commits?sha=' + branch + "&page=" + str(page) + commitsLink = httptools.downloadpage(apiLink).data + logger.info(apiLink) + return json.loads(commitsLink) -REMOTE_FILE = "https://github.com/kodiondemand/addon/archive/master.zip" -DESTINATION_FOLDER = xbmc.translatePath("special://home/addons") + "/plugin.video.kod" -REMOTE_VERSION_FILE = "https://raw.githubusercontent.com/kodiondemand/addon/master/version.json" def check_addon_init(): - logger.info() - - # Subtarea de monitor. Se activa cada X horas para comprobar si hay FIXES al addon - # def check_addon_monitor(): - # logger.info() - # - # # Obtiene el íntervalo entre actualizaciones y si se quieren mensajes - # try: - # timer = int(config.get_setting('addon_update_timer')) # Intervalo entre actualizaciones, en Ajustes de Alfa - # if timer <= 0: - # return # 0. No se quieren actualizaciones - # verbose = config.get_setting('addon_update_message') - # except: - # timer = 12 # Por defecto cada 12 horas - # verbose = False # Por defecto, sin mensajes - # timer = timer * 3600 # Lo pasamos a segundos - # - # if config.get_platform(True)['num_version'] >= 14: # Si es Kodi, lanzamos el monitor - # import xbmc - # monitor = xbmc.Monitor() - # else: # Lanzamos solo una actualización y salimos - # check_addon_updates(verbose) # Lanza la actualización - # return - # - # while not monitor.abortRequested(): # Loop infinito hasta cancelar Kodi - # - # check_addon_updates(verbose) # Lanza la actualización - # - # if monitor.waitForAbort(timer): # Espera el tiempo programado o hasta que cancele Kodi - # break # Cancelación de Kodi, salimos - # - # return - # - # # Lanzamos en Servicio de actualización de FIXES - # try: - # threading.Thread(target=check_addon_monitor).start() # Creamos un Thread independiente, hasta el fin de Kodi - # time.sleep(5) # Dejamos terminar la primera verificación... - # except: # Si hay problemas de threading, se llama una sola vez - # try: - # timer = int(config.get_setting('addon_update_timer')) # Intervalo entre actualizaciones, en Ajustes de Alfa - # if timer <= 0: - # return # 0. No se quieren actualizaciones - # verbose = config.get_setting('addon_update_message') - # except: - # verbose = False # Por defecto, sin mensajes - # pass - # check_addon_updates(verbose) # Lanza la actualización, en Ajustes de Alfa - # time.sleep(5) # Dejamos terminar la primera verificación... - - return - -def checkforupdates(plugin_mode=True): - logger.info("kodiondemand.core.updater checkforupdates") - - response = urllib.urlopen(REMOTE_VERSION_FILE) - data = json.loads(response.read()) - - ''' - { - "update": { - "name": "Kodi on Demand", - "tag": "1.0.0", - "version": "1000", - "date": "03/05/2019", - "changes": "Added Updater" - } - } - ''' - # remote is addon version without dots - remote_version = data["update"]["version"] - # tag version is version with dots used to a betterview gui - tag_version = data["update"]["tag"] - logger.info("kodiondemand.core.updater version remota="+tag_version+" "+remote_version) - - ''' - # Lee el fichero con la versión instalada - localFileName = LOCAL_VERSION_FILE - logger.info("kodiondemand.core.updater fichero local version: "+localFileName) - infile = open( localFileName ) - data = infile.read() - infile.close() - #logger.info("xml local="+data) - ''' - path_local = xbmc.translatePath("special://home/addons/") + "plugin.video.kod/version.json" - data = json.loads(open(path_local).read()) - - version_local = data["update"]["version"] - tag_local = data["update"]["tag"] - logger.info("kodiondemand.core.updater version local="+tag_local+" "+version_local) + # if not addon.getSetting('addon_update_enabled'): + # return False + logger.info('Cerco aggiornamenti..') + commits = loadCommits() try: - numero_remote_version = int(remote_version) - numero_version_local = int(version_local) + localCommitFile = open(addonDir+trackingFile, 'r+') except: - import traceback - logger.info(traceback.format_exc()) - remote_version = "" - version_local = "" + calcCurrHash() + localCommitFile = open(addonDir + trackingFile, 'r+') + localCommitSha = localCommitFile.read() + localCommitSha = localCommitSha.replace('\n', '') # da testare + logger.info('Commit locale: ' + localCommitSha) + updated = False - if remote_version=="" or version_local=="": - arraydescargada = tag_version.split(".") - arraylocal = tag_local.split(".") - - # local 2.8.0 - descargada 2.8.0 -> no descargar - # local 2.9.0 - descargada 2.8.0 -> no descargar - # local 2.8.0 - descargada 2.9.0 -> descargar - if len(arraylocal) == len(arraydescargada): - logger.info("caso 1") - hayqueactualizar = False - for i in range(0, len(arraylocal)): - print arraylocal[i], arraydescargada[i], int(arraydescargada[i]) > int(arraylocal[i]) - if int(arraydescargada[i]) > int(arraylocal[i]): - hayqueactualizar = True - # local 2.8.0 - descargada 2.8 -> no descargar - # local 2.9.0 - descargada 2.8 -> no descargar - # local 2.8.0 - descargada 2.9 -> descargar - if len(arraylocal) > len(arraydescargada): - logger.info("caso 2") - hayqueactualizar = False - for i in range(0, len(arraydescargada)): - #print arraylocal[i], arraydescargada[i], int(arraydescargada[i]) > int(arraylocal[i]) - if int(arraydescargada[i]) > int(arraylocal[i]): - hayqueactualizar = True - # local 2.8 - descargada 2.8.8 -> descargar - # local 2.9 - descargada 2.8.8 -> no descargar - # local 2.10 - descargada 2.9.9 -> no descargar - # local 2.5 - descargada 3.0.0 - if len(arraylocal) < len(arraydescargada): - logger.info("caso 3") - hayqueactualizar = True - for i in range(0, len(arraylocal)): - #print arraylocal[i], arraydescargada[i], int(arraylocal[i])>int(arraydescargada[i]) - if int(arraylocal[i]) > int(arraydescargada[i]): - hayqueactualizar = False - elif int(arraylocal[i]) < int(arraydescargada[i]): - hayqueactualizar = True - break + pos = None + for n, c in enumerate(commits): + if c['sha'] == localCommitSha: + pos = n + break else: - hayqueactualizar = (numero_remote_version > numero_version_local) + updateFromZip() + return True - if hayqueactualizar: - - if plugin_mode: - - logger.info("kodiondemand.core.updater actualizacion disponible") - - # Añade al listado de XBMC - import xbmcgui - #thumbnail = IMAGES_PATH+"Crystal_Clear_action_info.png" - thumbnail = os.path.join(config.get_runtime_path() , "resources" , "images", "service_update.png") - logger.info("thumbnail="+thumbnail) - listitem = xbmcgui.ListItem( "Scarica la versione "+tag_version, thumbnailImage=thumbnail ) - itemurl = '%s?action=update&version=%s' % ( sys.argv[ 0 ] , tag_version ) - import xbmcplugin - xbmcplugin.addDirectoryItem( handle = int(sys.argv[ 1 ]), url = itemurl , listitem=listitem, isFolder=True) - - # Avisa con un popup - dialog = xbmcgui.Dialog() - dialog.ok("Versione "+tag_version+" disponibile","E' possibile scaricare la nuova versione del plugin\nattraverso l'opzione nel menù principale.") + if pos > 0: + changelog = '' + nCommitApplied = 0 + for c in reversed(commits[:pos]): + commit = httptools.downloadpage(c['url']).data + commitJson = json.loads(commit) + logger.info('aggiornando a' + commitJson['sha']) + alreadyApplied = True + for file in commitJson['files']: + if file["filename"] == trackingFile: # il file di tracking non si modifica + continue + else: + logger.info(file["filename"]) + if file['status'] == 'modified' or file['status'] == 'added': + if 'patch' in file: + text = "" + try: + localFile = open(addonDir + file["filename"], 'r+') + for line in localFile: + text += line + except IOError: # nuovo file + localFile = open(addonDir + file["filename"], 'w') + + patched = apply_patch(text, (file['patch']+'\n').encode('utf-8')) + if patched != text: # non eseguo se già applicata (es. scaricato zip da github) + if getSha(patched) == file['sha']: + localFile.seek(0) + localFile.truncate() + localFile.writelines(patched) + localFile.close() + alreadyApplied = False + else: # nel caso ci siano stati problemi + logger.info('lo sha non corrisponde, scarico il file') + downloadtools.downloadfile(file['raw_url'], addonDir + file['filename'], + silent=True, continuar=True) + else: # è un file NON testuale, lo devo scaricare + # se non è già applicato + if not (filetools.isfile(addonDir + file['filename']) and getSha( + filetools.read(addonDir + file['filename']) == file['sha'])): + downloadtools.downloadfile(file['raw_url'], addonDir + file['filename'], silent=True, continuar=True) + alreadyApplied = False + elif file['status'] == 'removed': + try: + filetools.remove(addonDir+file["filename"]) + alreadyApplied = False + except: + pass + elif file['status'] == 'renamed': + # se non è già applicato + if not (filetools.isfile(addonDir + file['filename']) and getSha( + filetools.read(addonDir + file['filename']) == file['sha'])): + dirs = file['filename'].split('/') + for d in dirs[:-1]: + if not filetools.isdir(addonDir + d): + filetools.mkdir(addonDir + d) + filetools.move(addonDir + file['previous_filename'], addonDir + file['filename']) + alreadyApplied = False + if not alreadyApplied: # non mando notifica se già applicata (es. scaricato zip da github) + changelog += commitJson['commit']['message'] + " | " + nCommitApplied += 1 + time = nCommitApplied * 2000 if nCommitApplied < 10 else 20000 + platformtools.dialog_notification('Kodi on Demand', changelog, time) + + localCommitFile.seek(0) + localCommitFile.truncate() + localCommitFile.writelines(c['sha']) + localCommitFile.close() + + else: + logger.info('Nessun nuovo aggiornamento') + + return updated + + +def calcCurrHash(): + from lib import githash + treeHash = githash.tree_hash(addonDir).hexdigest() + commits = loadCommits() + page = 1 + while commits and page <= maxPage: + found = False + for n, c in enumerate(commits): + if c['commit']['tree']['sha'] == treeHash: + localCommitFile = open(addonDir + trackingFile, 'w') + localCommitFile.write(c['sha']) + localCommitFile.close() + found = True + break else: + page += 1 + commits = loadCommits(page) - import xbmcgui - yes_pressed = xbmcgui.Dialog().yesno( "Versione "+tag_version+" disponibile" , "Installarla?" ) - - if yes_pressed: - params = {"version":tag_version} - update(params) + if found: + break + else: + logger.info('Non sono riuscito a trovare il commit attuale, scarico lo zip') + updateFromZip() + calcCurrHash() -def update(): - # Descarga el ZIP - logger.info("kodiondemand.core.updater update") - remotefilename = REMOTE_FILE +# https://gist.github.com/noporpoise/16e731849eb1231e86d78f9dfeca3abc Grazie! + +def apply_patch(s,patch,revert=False): + """ + Apply unified diff patch to string s to recover newer string. + If revert is True, treat s as the newer string, recover older string. + """ + s = s.splitlines(True) + p = patch.splitlines(True) + t = '' + i = sl = 0 + (midx,sign) = (1,'+') if not revert else (3,'-') + while i < len(p) and p[i].startswith(("---","+++")): i += 1 # skip header lines + while i < len(p): + m = _hdr_pat.match(p[i]) + if not m: raise Exception("Cannot process diff") + i += 1 + l = int(m.group(midx))-1 + (m.group(midx+1) == '0') + t += ''.join(s[sl:l]) + sl = l + while i < len(p) and p[i][0] != '@': + if i+1 < len(p) and p[i+1][0] == '\\': line = p[i][:-1]; i += 2 + else: line = p[i]; i += 1 + if len(line) > 0: + if line[0] == sign or line[0] == ' ': t += line[1:] + sl += (line[0] != sign) + t += ''.join(s[sl:]) + return t + + +def getSha(fileText): + return hashlib.sha1("blob " + str(len(fileText)) + "\0" + fileText).hexdigest() + + +def updateFromZip(): + platformtools.dialog_notification('Kodi on Demand', 'Aggiornamento in corso...') + + remotefilename = 'https://github.com/' + user + "/" + repo + "/archive/" + branch + ".zip" localfilename = xbmc.translatePath("special://home/addons/") + "plugin.video.kod.update.zip" logger.info("kodiondemand.core.updater remotefilename=%s" % remotefilename) logger.info("kodiondemand.core.updater localfilename=%s" % localfilename) logger.info("kodiondemand.core.updater descarga fichero...") - - urllib.urlretrieve(remotefilename,localfilename) - #from core import downloadtools - #downloadtools.downloadfile(remotefilename, localfilename, continuar=False) - + + import urllib + urllib.urlretrieve(remotefilename, localfilename) + # Lo descomprime logger.info("kodiondemand.core.updater descomprime fichero...") - unzipper = ziptools.ziptools() - destpathname = xbmc.translatePath("special://home/addons/") + destpathname = xbmc.translatePath("special://home/addons/") logger.info("kodiondemand.core.updater destpathname=%s" % destpathname) - unzipper.extract(localfilename,destpathname, os.path.join(xbmc.translatePath("special://home/addons/"), "plugin.video.kod/")) - temp_dir = os.path.join(destpathname,"addon-master") - files = os.listdir(temp_dir) - #for f in files: - # shutil.move(os.path.join(temp_dir, f), os.path.join(xbmc.translatePath("special://home/addons/"), "plugin.video.kod/", f)) - + # puliamo tutto + shutil.rmtree(addonDir) + + unzipper = ziptools() + unzipper.extract(localfilename, destpathname) + + filetools.rename(destpathname + "addon-" + branch, addonDir) + # Borra el zip descargado logger.info("kodiondemand.core.updater borra fichero...") os.remove(localfilename) - #os.remove(temp_dir) - logger.info("kodiondemand.core.updater ...fichero borrado") + # os.remove(temp_dir) + platformtools.dialog_notification('Kodi on Demand', 'Aggiornamento completato!') - -''' -def check_addon_updates(verbose=False): - logger.info() - ADDON_UPDATES_JSON = 'https://extra.alfa-addon.com/addon_updates/updates.json' - ADDON_UPDATES_ZIP = 'https://extra.alfa-addon.com/addon_updates/updates.zip' +class ziptools: + def extract(self, file, dir, folder_to_extract="", overwrite_question=False, backup=False): + logger.info("file=%s" % file) + logger.info("dir=%s" % dir) - try: - last_fix_json = os.path.join(config.get_runtime_path(), 'last_fix.json') # información de la versión fixeada del usuario - # Se guarda en get_runtime_path en lugar de get_data_path para que se elimine al cambiar de versión + if not dir.endswith(':') and not os.path.exists(dir): + os.mkdir(dir) - # Descargar json con las posibles actualizaciones - # ----------------------------------------------- - data = httptools.downloadpage(ADDON_UPDATES_JSON, timeout=2).data - if data == '': - logger.info('No se encuentran actualizaciones del addon') - if verbose: - platformtools.dialog_notification(config.get_localized_string(70667), config.get_localized_string(70668)) - return False + zf = zipfile.ZipFile(file) + if not folder_to_extract: + self._createstructure(file, dir) + num_files = len(zf.namelist()) - data = jsontools.load(data) - if 'addon_version' not in data or 'fix_version' not in data: - logger.info('No hay actualizaciones del addon') - if verbose: - platformtools.dialog_notification(config.get_localized_string(70667), config.get_localized_string(70668)) - return False + for nameo in zf.namelist(): + name = nameo.replace(':', '_').replace('<', '_').replace('>', '_').replace('|', '_').replace('"', '_').replace('?', '_').replace('*', '_') + logger.info("name=%s" % nameo) + if not name.endswith('/'): + logger.info("no es un directorio") + try: + (path, filename) = os.path.split(os.path.join(dir, name)) + logger.info("path=%s" % path) + logger.info("name=%s" % name) + if folder_to_extract: + if path != os.path.join(dir, folder_to_extract): + break + else: + os.makedirs(path) + except: + pass + if folder_to_extract: + outfilename = os.path.join(dir, filename) - # Comprobar versión que tiene instalada el usuario con versión de la actualización - # -------------------------------------------------------------------------------- - current_version = config.get_addon_version(with_fix=False) - if current_version != data['addon_version']: - logger.info('No hay actualizaciones para la versión %s del addon' % current_version) - if verbose: - platformtools.dialog_notification(config.get_localized_string(70667), config.get_localized_string(70668)) - return False - - if os.path.exists(last_fix_json): - try: - lastfix = {} - lastfix = jsontools.load(filetools.read(last_fix_json)) - if lastfix['addon_version'] == data['addon_version'] and lastfix['fix_version'] == data['fix_version']: - logger.info(config.get_localized_string(70670) % (data['addon_version'], data['fix_version'])) - if verbose: - platformtools.dialog_notification(config.get_localized_string(70667), config.get_localized_string(70671) % (data['addon_version'], data['fix_version'])) - return False - except: - if lastfix: - logger.error('last_fix.json: ERROR en: ' + str(lastfix)) else: - logger.error('last_fix.json: ERROR desconocido') - lastfix = {} + outfilename = os.path.join(dir, name) + logger.info("outfilename=%s" % outfilename) + try: + if os.path.exists(outfilename) and overwrite_question: + from platformcode import platformtools + dyesno = platformtools.dialog_yesno("El archivo ya existe", + "El archivo %s a descomprimir ya existe" \ + ", ¿desea sobrescribirlo?" \ + % os.path.basename(outfilename)) + if not dyesno: + break + if backup: + import time + import shutil + hora_folder = "Copia seguridad [%s]" % time.strftime("%d-%m_%H-%M", time.localtime()) + backup = os.path.join(config.get_data_path(), 'backups', hora_folder, folder_to_extract) + if not os.path.exists(backup): + os.makedirs(backup) + shutil.copy2(outfilename, os.path.join(backup, os.path.basename(outfilename))) - # Descargar zip con las actualizaciones - # ------------------------------------- - localfilename = os.path.join(config.get_data_path(), 'temp_updates.zip') - if os.path.exists(localfilename): os.remove(localfilename) + outfile = open(outfilename, 'wb') + outfile.write(zf.read(nameo)) + except: + logger.error("Error en fichero " + nameo) - downloadtools.downloadfile(ADDON_UPDATES_ZIP, localfilename, silent=True) - - # Descomprimir zip dentro del addon - # --------------------------------- + def _createstructure(self, file, dir): + self._makedirs(self._listdirs(file), dir) + + def create_necessary_paths(filename): try: - unzipper = ziptools.ziptools() - unzipper.extract(localfilename, config.get_runtime_path()) + (path, name) = os.path.split(filename) + os.makedirs(path) except: - import xbmc - xbmc.executebuiltin('XBMC.Extract("%s", "%s")' % (localfilename, config.get_runtime_path())) - time.sleep(1) - - # Borrar el zip descargado - # ------------------------ - os.remove(localfilename) - - # Guardar información de la versión fixeada - # ----------------------------------------- - if 'files' in data: data.pop('files', None) - filetools.write(last_fix_json, jsontools.dump(data)) - - logger.info(config.get_localized_string(70672) % (data['addon_version'], data['fix_version'])) - if verbose: - platformtools.dialog_notification(config.get_localized_string(70673), config.get_localized_string(70671) % (data['addon_version'], data['fix_version'])) - return True + pass - except: - logger.error('Error al comprobar actualizaciones del addon!') - logger.error(traceback.format_exc()) - if verbose: - platformtools.dialog_notification(config.get_localized_string(70674), config.get_localized_string(70675)) - return False -''' \ No newline at end of file + def _makedirs(self, directories, basedir): + for dir in directories: + curdir = os.path.join(basedir, dir) + if not os.path.exists(curdir): + os.mkdir(curdir) + + def _listdirs(self, file): + zf = zipfile.ZipFile(file) + dirs = [] + for name in zf.namelist(): + if name.endswith('/'): + dirs.append(name) + + dirs.sort() + return dirs \ No newline at end of file