From 73e7a73270213ffa88a33d7c81c008e6231fa44d Mon Sep 17 00:00:00 2001 From: angedam Date: Tue, 19 Jun 2018 13:07:11 +0200 Subject: [PATCH] fix translation --- plugin.video.alfa/core/downloader.py | 546 +++++++++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 plugin.video.alfa/core/downloader.py diff --git a/plugin.video.alfa/core/downloader.py b/plugin.video.alfa/core/downloader.py new file mode 100644 index 00000000..6efb1554 --- /dev/null +++ b/plugin.video.alfa/core/downloader.py @@ -0,0 +1,546 @@ +# -*- coding: utf-8 -*- + +""" +Clase Downloader +Downloader(url, path [, filename, headers, resume]) + + url : string - url para descargar + path : string - Directorio donde se guarda la descarga + filename : [opt] string - Nombre de archivo para guardar + headers : [opt] dict - Headers para usar en la descarga + resume : [opt] bool - continuar una descarga previa en caso de existir, por defecto True + + +metodos: + start_dialog() Inicia la descarga mostrando el progreso + start() Inicia la descarga en segundo plano + stop(erase = False) Detiene la descarga, con erase = True elimina los datos descargados + +""" +import mimetypes +import os +import re +import sys +import threading +import time +import urllib +import urllib2 +import urlparse +from threading import Thread, Lock + +from core import filetools +from platformcode import logger + + +class Downloader: + @property + def state(self): + return self._state + + @property + def connections(self): + return len([c for c in self._download_info["parts"] if + c["status"] in [self.states.downloading, self.states.connecting]]), self._max_connections + + @property + def downloaded(self): + return self.__change_units__(sum([c["current"] - c["start"] for c in self._download_info["parts"]])) + + @property + def average_speed(self): + return self.__change_units__(self._average_speed) + + @property + def speed(self): + return self.__change_units__(self._speed) + + @property + def remaining_time(self): + if self.speed[0] and self._file_size: + t = (self.size[0] - self.downloaded[0]) / self.speed[0] + else: + t = 0 + + return time.strftime("%H:%M:%S", time.gmtime(t)) + + @property + def download_url(self): + return self.url + + @property + def size(self): + return self.__change_units__(self._file_size) + + @property + def progress(self): + if self._file_size: + return float(self.downloaded[0]) * 100 / float(self._file_size) + elif self._state == self.states.completed: + return 100 + else: + return 0 + + @property + def filename(self): + return self._filename + + @property + def fullpath(self): + return os.path.abspath(filetools.join(self._path, self._filename)) + + # Funciones + def start_dialog(self, title="Descargando..."): + from platformcode import platformtools + progreso = platformtools.dialog_progress(title, "Iniciando descarga...") + self.start() + while self.state == self.states.downloading and not progreso.iscanceled(): + time.sleep(0.1) + line1 = "%s" % (self.filename) + line2 = "%.2f%% - %.2f %s de %.2f %s a %.2f %s/s (%d/%d)" % ( + self.progress, self.downloaded[1], self.downloaded[2], self.size[1], self.size[2], + self.speed[1], self.speed[2], self.connections[0], self.connections[1]) + line3 = "Tiempo restante: %s" % (self.remaining_time) + + progreso.update(int(self.progress), line1, line2, line3) + if self.state == self.states.downloading: + self.stop() + progreso.close() + + def start(self): + if self._state == self.states.error: return + conns = [] + for x in range(self._max_connections): + try: + conns.append(self.__open_connection__("0", "")) + except: + self._max_connections = x + self._threads = [ + Thread(target=self.__start_part__, name="Downloader %s/%s" % (x + 1, self._max_connections)) for x + in range(self._max_connections)] + break + del conns + self._start_time = time.time() - 1 + self._state = self.states.downloading + self._speed_thread.start() + self._save_thread.start() + + for t in self._threads: t.start() + + def stop(self, erase=False): + if self._state == self.states.downloading: + # Detenemos la descarga + self._state = self.states.stopped + for t in self._threads: + if t.isAlive(): t.join() + + if self._save_thread.isAlive(): self._save_thread.join() + + if self._seekable: + # Guardamos la info al final del archivo + self.file.seek(0, 2) + offset = self.file.tell() + self.file.write(str(self._download_info)) + self.file.write("%0.16d" % offset) + + self.file.close() + + if erase: os.remove(filetools.join(self._path, self._filename)) + + def __speed_metter__(self): + self._speed = 0 + self._average_speed = 0 + + downloaded = self._start_downloaded + downloaded2 = self._start_downloaded + t = time.time() + t2 = time.time() + time.sleep(1) + + while self.state == self.states.downloading: + self._average_speed = (self.downloaded[0] - self._start_downloaded) / (time.time() - self._start_time) + self._speed = (self.downloaded[0] - self._start_downloaded) / (time.time() - self._start_time) + # self._speed = (self.downloaded[0] - downloaded) / (time.time() -t) + + if time.time() - t > 5: + t = t2 + downloaded = downloaded2 + t2 = time.time() + downloaded2 = self.downloaded[0] + + time.sleep(0.5) + + # Funciones internas + def __init__(self, url, path, filename=None, headers=[], resume=True, max_connections=10, block_size=2 ** 17, + part_size=2 ** 24, max_buffer=10): + # Parametros + self._resume = resume + self._path = path + self._filename = filename + self._max_connections = max_connections + self._block_size = block_size + self._part_size = part_size + self._max_buffer = max_buffer + + try: + import xbmc + self.tmp_path = xbmc.translatePath("special://temp/") + except: + self.tmp_path = os.getenv("TEMP") or os.getenv("TMP") or os.getenv("TMPDIR") + + self.states = type('states', (), + {"stopped": 0, "connecting": 1, "downloading": 2, "completed": 3, "error": 4, "saving": 5}) + + self._state = self.states.stopped + self._download_lock = Lock() + self._headers = { + "User-Agent": "Kodi/15.2 (Windows NT 10.0; WOW64) App_Bitness/32 Version/15.2-Git:20151019-02e7013"} + self._speed = 0 + self._buffer = {} + self._seekable = True + + self._threads = [Thread(target=self.__start_part__, name="Downloader %s/%s" % (x + 1, self._max_connections)) + for x in range(self._max_connections)] + self._speed_thread = Thread(target=self.__speed_metter__, name="Speed Meter") + self._save_thread = Thread(target=self.__save_file__, name="File Writer") + + # Actualizamos los headers + self._headers.update(dict(headers)) + + # Separamos los headers de la url + self.__url_to_headers__(url) + + # Obtenemos la info del servidor + self.__get_download_headers__() + + self._file_size = int(self.response_headers.get("content-length", "0")) + + if not self.response_headers.get("accept-ranges") == "bytes" or self._file_size == 0: + self._max_connections = 1 + self._part_size = 0 + self._resume = False + + # Obtenemos el nombre del archivo + self.__get_download_filename__() + + # Abrimos en modo "a+" para que cree el archivo si no existe, luego en modo "r+b" para poder hacer seek() + self.file = filetools.file_open(filetools.join(self._path, self._filename), "a+") + self.file = filetools.file_open(filetools.join(self._path, self._filename), "r+b") + + if self._file_size >= 2 ** 31 or not self._file_size: + try: + self.file.seek(2 ** 31) + except OverflowError: + self._seekable = False + logger.info("No se puede hacer seek() ni tell() en ficheros mayores de 2GB") + + self.__get_download_info__() + + logger.info("Descarga inicializada: Partes: %s | Ruta: %s | Archivo: %s | Tamaño: %s" % ( + len(self._download_info["parts"]), self._path, self._filename, self._download_info["size"])) + + def __url_to_headers__(self, url): + # Separamos la url de los headers adicionales + self.url = url.split("|")[0] + + # headers adicionales + if "|" in url: + self._headers.update(dict([[header.split("=")[0], urllib.unquote_plus(header.split("=")[1])] for header in + url.split("|")[1].split("&")])) + + def __get_download_headers__(self): + if self.url.startswith("https"): + try: + conn = urllib2.urlopen(urllib2.Request(self.url.replace("https", "http"), headers=self._headers)) + conn.fp._sock.close() + self.url = self.url.replace("https", "http") + except: + pass + + for x in range(3): + try: + if not sys.hexversion > 0x0204FFFF: + conn = urllib2.urlopen(urllib2.Request(self.url, headers=self._headers)) + conn.fp._sock.close() + else: + conn = urllib2.urlopen(urllib2.Request(self.url, headers=self._headers), timeout=5) + + except: + self.response_headers = dict() + self._state = self.states.error + else: + self.response_headers = conn.headers.dict + self._state = self.states.stopped + break + + def __get_download_filename__(self): + # Obtenemos nombre de archivo y extension + if "filename" in self.response_headers.get("content-disposition", + "") and "attachment" in self.response_headers.get( + "content-disposition", ""): + cd_filename, cd_ext = os.path.splitext(urllib.unquote_plus( + re.compile("attachment; filename ?= ?[\"|']?([^\"']+)[\"|']?").match( + self.response_headers.get("content-disposition")).group(1))) + if "filename" in self.response_headers.get("content-disposition", "") and "inline" in self.response_headers.get( + "content-disposition", ""): + cd_filename, cd_ext = os.path.splitext(urllib.unquote_plus( + re.compile("inline; filename ?= ?[\"|']?([^\"']+)[\"|']?").match( + self.response_headers.get("content-disposition")).group(1))) + else: + cd_filename, cd_ext = "", "" + + url_filename, url_ext = os.path.splitext( + urllib.unquote_plus(filetools.basename(urlparse.urlparse(self.url)[2]))) + if self.response_headers.get("content-type", "application/octet-stream") <> "application/octet-stream": + mime_ext = mimetypes.guess_extension(self.response_headers.get("content-type")) + else: + mime_ext = "" + + # Seleccionamos el nombre mas adecuado + if cd_filename: + self.remote_filename = cd_filename + if not self._filename: + self._filename = cd_filename + + elif url_filename: + self.remote_filename = url_filename + if not self._filename: + self._filename = url_filename + + # Seleccionamos la extension mas adecuada + if cd_ext: + if not cd_ext in self._filename: self._filename += cd_ext + if self.remote_filename: self.remote_filename += cd_ext + elif mime_ext: + if not mime_ext in self._filename: self._filename += mime_ext + if self.remote_filename: self.remote_filename += mime_ext + elif url_ext: + if not url_ext in self._filename: self._filename += url_ext + if self.remote_filename: self.remote_filename += url_ext + + def __change_units__(self, value): + import math + units = ["B", "KB", "MB", "GB"] + if value <= 0: + return 0, 0, units[0] + else: + return value, value / 1024.0 ** int(math.log(value, 1024)), units[int(math.log(value, 1024))] + + def __get_download_info__(self): + # Continuamos con una descarga que contiene la info al final del archivo + self._download_info = {} + + try: + if not self._resume: + raise Exception() + self.file.seek(-16, 2) + offset = int(self.file.read()) + self.file.seek(offset) + data = self.file.read()[:-16] + self._download_info = eval(data) + if not self._download_info["size"] == self._file_size: + raise Exception() + self.file.seek(offset) + self.file.truncate() + + if not self._seekable: + for part in self._download_info["parts"]: + if part["start"] >= 2 ** 31 and part["status"] == self.states.completed: + part["status"] == self.states.stopped + part["current"] == part["start"] + + self._start_downloaded = sum([c["current"] - c["start"] for c in self._download_info["parts"]]) + self.pending_parts = set( + [x for x, a in enumerate(self._download_info["parts"]) if not a["status"] == self.states.completed]) + self.completed_parts = set( + [x for x, a in enumerate(self._download_info["parts"]) if a["status"] == self.states.completed]) + self.save_parts = set() + self.download_parts = set() + + # La info no existe o no es correcta, comenzamos de 0 + except: + self._download_info["parts"] = [] + if self._file_size and self._part_size: + for x in range(0, self._file_size, self._part_size): + end = x + self._part_size - 1 + if end >= self._file_size: end = self._file_size - 1 + self._download_info["parts"].append( + {"start": x, "end": end, "current": x, "status": self.states.stopped}) + else: + self._download_info["parts"].append( + {"start": 0, "end": self._file_size - 1, "current": 0, "status": self.states.stopped}) + + self._download_info["size"] = self._file_size + self._start_downloaded = 0 + self.pending_parts = set([x for x in range(len(self._download_info["parts"]))]) + self.completed_parts = set() + self.save_parts = set() + self.download_parts = set() + + self.file.seek(0) + self.file.truncate() + + def __open_connection__(self, start, end): + headers = self._headers.copy() + if not end: end = "" + headers.update({"Range": "bytes=%s-%s" % (start, end)}) + if not sys.hexversion > 0x0204FFFF: + conn = urllib2.urlopen(urllib2.Request(self.url, headers=headers)) + else: + conn = urllib2.urlopen(urllib2.Request(self.url, headers=headers), timeout=5) + return conn + + def __check_consecutive__(self, id): + return id == 0 or (len(self.completed_parts) >= id and sorted(self.completed_parts)[id - 1] == id - 1) + + def __save_file__(self): + logger.info("Thread iniciado: %s" % threading.current_thread().name) + + while self._state == self.states.downloading: + if not self.pending_parts and not self.download_parts and not self.save_parts: # Descarga finalizada + self._state = self.states.completed + self.file.close() + continue + + elif not self.save_parts: + continue + + save_id = min(self.save_parts) + + if not self._seekable and self._download_info["parts"][save_id][ + "start"] >= 2 ** 31 and not self.__check_consecutive__(save_id): + continue + + if self._seekable or self._download_info["parts"][save_id]["start"] < 2 ** 31: + self.file.seek(self._download_info["parts"][save_id]["start"]) + + try: + # file = open(os.path.join(self.tmp_path, self._filename + ".part%s" % save_id), "rb") + # self.file.write(file.read()) + # file.close() + # os.remove(os.path.join(self.tmp_path, self._filename + ".part%s" % save_id)) + for a in self._buffer.pop(save_id): + self.file.write(a) + self.save_parts.remove(save_id) + self.completed_parts.add(save_id) + self._download_info["parts"][save_id]["status"] = self.states.completed + except: + import traceback + logger.error(traceback.format_exc()) + self._state = self.states.error + + if self.save_parts: + for s in self.save_parts: + self._download_info["parts"][s]["status"] = self.states.stopped + self._download_info["parts"][s]["current"] = self._download_info["parts"][s]["start"] + + logger.info("Thread detenido: %s" % threading.current_thread().name) + + def __get_part_id__(self): + self._download_lock.acquire() + if len(self.pending_parts): + id = min(self.pending_parts) + self.pending_parts.remove(id) + self.download_parts.add(id) + self._download_lock.release() + return id + else: + self._download_lock.release() + return None + + def __set_part_connecting__(self, id): + logger.info("ID: %s Estableciendo conexión" % id) + self._download_info["parts"][id]["status"] = self.states.connecting + + def __set_part__error__(self, id): + logger.info("ID: %s Error al descargar" % id) + self._download_info["parts"][id]["status"] = self.states.error + self.pending_parts.add(id) + self.download_parts.remove(id) + + def __set_part__downloading__(self, id): + logger.info("ID: %s Descargando datos..." % id) + self._download_info["parts"][id]["status"] = self.states.downloading + + def __set_part_completed__(self, id): + logger.info("ID: %s ¡Descarga finalizada!" % id) + self._download_info["parts"][id]["status"] = self.states.saving + self.download_parts.remove(id) + self.save_parts.add(id) + while self._state == self.states.downloading and len(self._buffer) > self._max_connections + self._max_buffer: + time.sleep(0.1) + + def __set_part_stopped__(self, id): + if self._download_info["parts"][id]["status"] == self.states.downloading: + self._download_info["parts"][id]["status"] = self.states.stopped + self.download_parts.remove(id) + self.pending_parts.add(id) + + def __open_part_file__(self, id): + file = open(os.path.join(self.tmp_path, self._filename + ".part%s" % id), "a+") + file = open(os.path.join(self.tmp_path, self._filename + ".part%s" % id), "r+b") + file.seek(self._download_info["parts"][id]["current"] - self._download_info["parts"][id]["start"]) + return file + + def __start_part__(self): + logger.info("Thread Iniciado: %s" % threading.current_thread().name) + while self._state == self.states.downloading: + id = self.__get_part_id__() + if id is None: break + + self.__set_part_connecting__(id) + + try: + connection = self.__open_connection__(self._download_info["parts"][id]["current"], + self._download_info["parts"][id]["end"]) + except: + self.__set_part__error__(id) + time.sleep(5) + continue + + self.__set_part__downloading__(id) + # file = self.__open_part_file__(id) + + if not id in self._buffer: + self._buffer[id] = [] + speed = [] + + while self._state == self.states.downloading: + try: + start = time.time() + buffer = connection.read(self._block_size) + speed.append(len(buffer) / ((time.time() - start) or 0.001)) + except: + logger.info("ID: %s Error al descargar los datos" % id) + self._download_info["parts"][id]["status"] = self.states.error + self.pending_parts.add(id) + self.download_parts.remove(id) + break + else: + if len(buffer) and self._download_info["parts"][id]["current"] < self._download_info["parts"][id][ + "end"]: + # file.write(buffer) + self._buffer[id].append(buffer) + self._download_info["parts"][id]["current"] += len(buffer) + if len(speed) > 10: + velocidad_minima = sum(speed) / len(speed) / 3 + velocidad = speed[-1] + vm = self.__change_units__(velocidad_minima) + v = self.__change_units__(velocidad) + + if velocidad_minima > speed[-1] and velocidad_minima > speed[-2] and \ + self._download_info["parts"][id]["current"] < \ + self._download_info["parts"][id]["end"]: + connection.fp._sock.close() + logger.info( + "ID: %s ¡Reiniciando conexión! | Velocidad minima: %.2f %s/s | Velocidad: %.2f %s/s" % \ + (id, vm[1], vm[2], v[1], v[2])) + # file.close() + break + else: + self.__set_part_completed__(id) + connection.fp._sock.close() + # file.close() + break + + self.__set_part_stopped__(id) + logger.info("Thread detenido: %s" % threading.current_thread().name)