From 4c949d5d89b858d405acee8974caa439b5d7f8a4 Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Wed, 12 Sep 2018 11:51:44 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Generictools:=20c=C3=A1lculo=20tama=C3=B1o?= =?UTF-8?q?=20.torrent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin.video.alfa/channels/divxtotal.py | 26 +++- plugin.video.alfa/channels/elitetorrent.py | 31 +++-- plugin.video.alfa/channels/estrenosgo.py | 29 +++- plugin.video.alfa/channels/grantorrent.py | 16 ++- plugin.video.alfa/channels/mejortorrent1.py | 16 ++- plugin.video.alfa/channels/newpct1.py | 42 ++++-- plugin.video.alfa/lib/generictools.py | 141 ++++++++++++++++++-- 7 files changed, 258 insertions(+), 43 deletions(-) diff --git a/plugin.video.alfa/channels/divxtotal.py b/plugin.video.alfa/channels/divxtotal.py index 8e1ba213..1a0deaf5 100644 --- a/plugin.video.alfa/channels/divxtotal.py +++ b/plugin.video.alfa/channels/divxtotal.py @@ -519,17 +519,35 @@ def findvideos(item): item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) #Ahora tratamos los enlaces .torrent - for scrapedurl in matches: #leemos los torrents con la diferentes calidades + for scrapedurl in matches: #leemos los torrents con la diferentes calidades #Generamos una copia de Item para trabajar sobre ella item_local = item.clone() + #Buscamos si ya tiene tamaño, si no, los buscamos en el archivo .torrent + size = scrapertools.find_single_match(item_local.quality, '\s\[(\d+,?\d*?\s\w\s?[b|B])\]') + if not size: + size = generictools.get_torrent_size(item_local.url) #Buscamos el tamaño en el .torrent + if size: + item_local.title = re.sub(r'\s\[\d+,?\d*?\s\w[b|B]\]', '', item_local.title) #Quitamos size de título, si lo traía + item_local.title = '%s [%s]' % (item_local.title, size) #Agregamos size al final del título + size = size.replace('GB', 'G B').replace('Gb', 'G b').replace('MB', 'M B').replace('Mb', 'M b') + item_local.quality = re.sub(r'\s\[\d+,?\d*?\s\w\s?[b|B]\]', '', item_local.quality) #Quitamos size de calidad, si lo traía + item_local.quality = '%s [%s]' % (item_local.quality, size) #Agregamos size al final de la calidad + #Ahora pintamos el link del Torrent item_local.url = scrapedurl if host not in item_local.url and host.replace('https', 'http') not in item_local.url : item_local.url = host + item_local.url - item_local.title = '[COLOR yellow][?][/COLOR] [COLOR yellow][Torrent][/COLOR] [COLOR limegreen][%s][/COLOR] [COLOR red]%s[/COLOR]' % (item_local.quality, str(item_local.language)) #Preparamos título de Torrent - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', item_local.title) #Quitamos etiquetas vacías - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\/COLOR\]', '', item_local.title) #Quitamos colores vacíos + item_local.title = '[COLOR yellow][?][/COLOR] [COLOR yellow][Torrent][/COLOR] [COLOR limegreen][%s][/COLOR] [COLOR red]%s[/COLOR]' % (item_local.quality, str(item_local.language)) + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.quality) + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality) + item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.alive = "??" #Calidad del link sin verificar item_local.action = "play" #Visualizar vídeo item_local.server = "torrent" #Seridor Torrent diff --git a/plugin.video.alfa/channels/elitetorrent.py b/plugin.video.alfa/channels/elitetorrent.py index 6842c037..1a052a4a 100644 --- a/plugin.video.alfa/channels/elitetorrent.py +++ b/plugin.video.alfa/channels/elitetorrent.py @@ -171,8 +171,11 @@ def listado(item): #Limpiamos el título de la basura innecesaria title = title.replace("Dual", "").replace("dual", "").replace("Subtitulada", "").replace("subtitulada", "").replace("Subt", "").replace("subt", "").replace("Sub", "").replace("sub", "").replace("(Proper)", "").replace("(proper)", "").replace("Proper", "").replace("proper", "").replace("#", "").replace("(Latino)", "").replace("Latino", "") - title = title.replace("- HDRip", "").replace("(HDRip)", "").replace("- Hdrip", "").replace("(microHD)", "").replace("(DVDRip)", "").replace("(HDRip)", "").replace("(BR-LINE)", "").replace("(HDTS-SCREENER)", "").replace("(BDRip)", "").replace("(BR-Screener)", "").replace("(DVDScreener)", "").replace("TS-Screener", "").replace(" TS", "").replace(" Ts", "") + title = title.replace("- HDRip", "").replace("(HDRip)", "").replace("- Hdrip", "").replace("(microHD)", "").replace("(DVDRip)", "").replace("(HDRip)", "").replace("(BR-LINE)", "").replace("(HDTS-SCREENER)", "").replace("(BDRip)", "").replace("(BR-Screener)", "").replace("(DVDScreener)", "").replace("TS-Screener", "").replace(" TS", "").replace(" Ts", "").replace("temporada", "").replace("Temporada", "").replace("capitulo", "").replace("Capitulo", "") + + title = re.sub(r'(?:\d+)?x.?\s?\d+', '', title) title = re.sub(r'\??\s?\d*?\&.*', '', title).title().strip() + item_local.from_title = title #Guardamos esta etiqueta para posible desambiguación de título if item_local.extra == "peliculas": #preparamos Item para películas @@ -190,16 +193,17 @@ def listado(item): item_local.contentType = "episode" item_local.extra = "series" epi_mult = scrapertools.find_single_match(item_local.url, r'cap.*?-\d+-al-(\d+)') - item_local.contentSeason = scrapertools.find_single_match(item_local.url, r'temp.*?-(\d+)') + item_local.contentSeason = scrapertools.find_single_match(item_local.url, r'temporada-(\d+)') item_local.contentEpisodeNumber = scrapertools.find_single_match(item_local.url, r'cap.*?-(\d+)') if not item_local.contentSeason: item_local.contentSeason = scrapertools.find_single_match(item_local.url, r'-(\d+)[x|X]\d+') if not item_local.contentEpisodeNumber: item_local.contentEpisodeNumber = scrapertools.find_single_match(item_local.url, r'-\d+[x|X](\d+)') - if item_local.contentSeason < 1: - item_local.contentSeason = 1 + if not item_local.contentSeason or item_local.contentSeason < 1: + item_local.contentSeason = 0 if item_local.contentEpisodeNumber < 1: item_local.contentEpisodeNumber = 1 + item_local.contentSerieName = title if epi_mult: title = "%sx%s al %s -" % (item_local.contentSeason, str(item_local.contentEpisodeNumber).zfill(2), str(epi_mult).zfill(2)) #Creamos un título con el rango de episodios @@ -269,11 +273,11 @@ def findvideos(item): #data = unicode(data, "utf-8", errors="replace") #Añadimos el tamaño para todos - size = scrapertools.find_single_match(item.quality, '\s\[(\d+,?\d*?\s\w[b|B]s)\]') + size = scrapertools.find_single_match(item.quality, '\s\[(\d+,?\d*?\s\w\s?[b|B]s)\]') if size: item.title = re.sub('\s\[\d+,?\d*?\s\w[b|B]s\]', '', item.title) #Quitamos size de título, si lo traía item.title = '%s [%s]' % (item.title, size) #Agregamos size al final del título - item.quality = re.sub('\s\[\d+,?\d*?\s\w[b|B]s\]', '', item.quality) #Quitamos size de calidad, si lo traía + item.quality = re.sub('\s\[\d+,?\d*?\s\w\s?[b|B]s\]', '', item.quality) #Quitamos size de calidad, si lo traía patron_t = '
Formato:<\/b>&\w+;\s?([^<]+)
'): item_local.quality = scrapertools.find_single_match(data, 'Formato:<\/b>&\w+;\s?([^<]+)
') elif "hdtv" in item_local.url.lower() or "720p" in item_local.url.lower() or "1080p" in item_local.url.lower() or "4k" in item_local.url.lower(): item_local.quality = scrapertools.find_single_match(item_local.url, '.*?_([H|7|1|4].*?)\.torrent') item_local.quality = item_local.quality.replace("_", " ") - + # Extrae el tamaño del vídeo if scrapertools.find_single_match(data, 'Tama.*?:<\/b>&\w+;\s?([^<]+B)Tama.*?:<\/b>&\w+;\s?([^<]+B)
Size:<\/strong>?\s(\d+?\.?\d*?\s\w[b|B])<\/span>') size = size.replace(".", ",") #sustituimos . por , porque Unify lo borra if not size: - size = scrapertools.find_single_match(item.quality, '\s\[(\d+,?\d*?\s\w[b|B])\]') + size = scrapertools.find_single_match(item.quality, '\s\[(\d+,?\d*?\s\w\s?[b|B])\]') + if not size: + size = generictools.get_torrent_size(item.url) #Buscamos el tamaño en el .torrent if size: item.title = re.sub(r'\s\[\d+,?\d*?\s\w[b|B]\]', '', item.title) #Quitamos size de título, si lo traía item.title = '%s [%s]' % (item.title, size) #Agregamos size al final del título size = size.replace('GB', 'G B').replace('Gb', 'G b').replace('MB', 'M B').replace('Mb', 'M b') - item.quality = re.sub(r'\s\[\d+,?\d*?\s\w[b|B]\]', '', item.quality) #Quitamos size de calidad, si lo traía + item.quality = re.sub(r'\s\[\d+,?\d*?\s\w\s?[b|B]\]', '', item.quality) #Quitamos size de calidad, si lo traía #Llamamos al método para crear el título general del vídeo, con toda la información obtenida de TMDB item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) @@ -1399,8 +1401,15 @@ def findvideos(item): else: quality = item_local.quality item_local.title = '[COLOR yellow][?][/COLOR] [COLOR yellow][Torrent][/COLOR] [COLOR limegreen][%s][/COLOR] [COLOR red]%s[/COLOR]' % (quality, str(item_local.language)) #Preparamos título de Torrent - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', item_local.title).strip() #Quitamos etiquetas vacías - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\/COLOR\]', '', item_local.title).strip() #Quitamos colores vacíos + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', quality) + quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', quality) + quality = quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.alive = "??" #Calidad del link sin verificar item_local.action = "play" #Visualizar vídeo item_local.server = "torrent" #Servidor @@ -1485,9 +1494,15 @@ def findvideos(item): item_local.action = "play" item_local.server = servidor item_local.url = enlace - item_local.title = item_local.title.replace("[]", "").strip() - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', item_local.title).strip() - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\/COLOR\]', '', item_local.title).strip() + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.quality) + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality) + item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + itemlist.append(item_local.clone()) except: @@ -1582,9 +1597,16 @@ def findvideos(item): item_local.action = "play" item_local.server = servidor item_local.url = enlace - item_local.title = parte_title.replace("[]", "").strip() - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', item_local.title).strip() - item_local.title = re.sub(r'\[COLOR \w+\]-\[\/COLOR\]', '', item_local.title).strip() + item_local.title = parte_title.strip() + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.quality) + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality) + item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + itemlist.append(item_local.clone()) except: diff --git a/plugin.video.alfa/lib/generictools.py b/plugin.video.alfa/lib/generictools.py index fa6bb57c..24d7d5c9 100644 --- a/plugin.video.alfa/lib/generictools.py +++ b/plugin.video.alfa/lib/generictools.py @@ -8,6 +8,7 @@ # ------------------------------------------------------------ import re +import os import sys import urllib import urlparse @@ -236,8 +237,7 @@ def post_tmdb_listado(item, itemlist): del item.channel_alt if item.url_alt: del item.url_alt - if item.extra2: - del item.extra2 + #Ajustamos el nombre de la categoría if not item.category_new: item.category_new = '' @@ -389,8 +389,8 @@ def post_tmdb_listado(item, itemlist): if item_local.infoLabels['episodio_titulo']: item_local.infoLabels['episodio_titulo'] = item_local.infoLabels['episodio_titulo'].replace(" []", "").strip() title = title.replace("--", "").replace(" []", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() - title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', title).strip() - title = re.sub(r'\s\[COLOR \w+\]\[\/COLOR\]', '', title).strip() + title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', title).strip() + title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', title).strip() if item.category_new == "newest": #Viene de Novedades. Marcamos el título con el nombre del canal title += ' -%s-' % scrapertools.find_single_match(item_local.url, 'http.?\:\/\/(?:www.)?(\w+)\.\w+\/').capitalize() @@ -766,6 +766,7 @@ def post_tmdb_episodios(item, itemlist): #Si no está el título del episodio, pero sí está en "title", lo rescatamos if not item_local.infoLabels['episodio_titulo'] and item_local.infoLabels['title'].lower() != item_local.infoLabels['tvshowtitle'].lower(): item_local.infoLabels['episodio_titulo'] = item_local.infoLabels['title'] + item_local.infoLabels['episodio_titulo'] = item_local.infoLabels['episodio_titulo'].replace('GB', 'G B').replace('MB', 'M B') #Preparamos el título para que sea compatible con Añadir Serie a Videoteca if "Temporada" in item_local.title: #Compatibilizamos "Temporada" con Unify @@ -792,8 +793,8 @@ def post_tmdb_episodios(item, itemlist): item_local.infoLabels['episodio_titulo'] = item_local.infoLabels['episodio_titulo'].replace(" []", "").strip() item_local.infoLabels['title'] = item_local.infoLabels['title'].replace(" []", "").strip() item_local.title = item_local.title.replace(" []", "").strip() - item_local.title = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', item_local.title).strip() - item_local.title = re.sub(r'\s\[COLOR \w+\]-\[\/COLOR\]', '', item_local.title).strip() + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?-?\s?\]?\]\[\/COLOR\]', '', item_local.title).strip() + item_local.title = re.sub(r'\s?\[COLOR \w+\]-?\s?\[\/COLOR\]', '', item_local.title).strip() #Si la información de num. total de episodios de TMDB no es correcta, tratamos de calcularla if num_episodios < item_local.contentEpisodeNumber: @@ -1054,8 +1055,8 @@ def post_tmdb_findvideos(item, itemlist): title_gen = item.title #Limpiamos etiquetas vacías - title_gen = re.sub(r'\s\[COLOR \w+\]\[\[?\]?\]\[\/COLOR\]', '', title_gen).strip() #Quitamos etiquetas vacías - title_gen = re.sub(r'\s\[COLOR \w+\]\[\/COLOR\]', '', title_gen).strip() #Quitamos colores vacíos + title_gen = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', title_gen).strip() #Quitamos etiquetas vacías + title_gen = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', title_gen).strip() #Quitamos colores vacíos title_gen = title_gen.replace(" []", "").strip() #Quitamos etiquetas vacías title_videoteca = title_gen #Salvamos el título para Videoteca @@ -1103,7 +1104,131 @@ def post_tmdb_findvideos(item, itemlist): return (item, itemlist) + +def get_torrent_size(url): + logger.info() + + """ + + Módulo extraido del antiguo canal ZenTorrent + + Calcula el tamaño de los archivos que contienen un .torrent. Descarga el archivo .torrent en una carpeta, + lo lee y descodifica. Si contiene múltiples archivos, suma el tamaño de todos ellos + + Llamada: generictools.get_torrent_size(url) + Entrada: url: url del archivo .torrent + Salida: size: str con el tamaño y tipo de medida ( MB, GB, etc) + + """ + + def convert_size(size): + import math + if (size == 0): + return '0B' + size_name = ("B", "KB", "M B", "G B", "TB", "PB", "EB", "ZB", "YB") + i = int(math.floor(math.log(size, 1024))) + p = math.pow(1024, i) + s = round(size / p, 2) + return '%s %s' % (s, size_name[i]) + + def decode(text): + try: + src = tokenize(text) + data = decode_item(src.next, src.next()) + for token in src: # look for more tokens + raise SyntaxError("trailing junk") + except (AttributeError, ValueError, StopIteration): + try: + data = data + except: + data = src + return data + + def tokenize(text, match=re.compile("([idel])|(\d+):|(-?\d+)").match): + i = 0 + while i < len(text): + m = match(text, i) + s = m.group(m.lastindex) + i = m.end() + if m.lastindex == 2: + yield "s" + yield text[i:i + int(s)] + i = i + int(s) + else: + yield s + + def decode_item(next, token): + if token == "i": + # integer: "i" value "e" + data = int(next()) + if next() != "e": + raise ValueError + elif token == "s": + # string: "s" value (virtual tokens) + data = next() + elif token == "l" or token == "d": + # container: "l" (or "d") values "e" + data = [] + tok = next() + while tok != "e": + data.append(decode_item(next, tok)) + tok = next() + if token == "d": + data = dict(zip(data[0::2], data[1::2])) + else: + raise ValueError + return data + + + #Móludo principal + size = "" + try: + torrents_path = config.get_videolibrary_path() + '/torrents' #path para dejar el .torrent + + if not os.path.exists(torrents_path): + os.mkdir(torrents_path) #si no está la carpeta la creamos + + urllib.URLopener.version = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36 SE 2.X MetaSr 1.0' + urllib.urlretrieve(url, torrents_path + "/generictools.torrent") #desacargamos el .torrent a la carpeta + torrent_file = open(torrents_path + "/generictools.torrent", "rb").read() #leemos el .torrent + + if "used CloudFlare" in torrent_file: #Si tiene CloudFlare, usamos este proceso + try: + urllib.urlretrieve("http://anonymouse.org/cgi-bin/anon-www.cgi/" + url.strip(), + torrents_path + "/generictools.torrent") + torrent_file = open(torrents_path + "/generictools.torrent", "rb").read() + except: + torrent_file = "" + + torrent = decode(torrent_file) #decodificamos el .torrent + + #si sólo tiene un archivo, tomamos la longitud y la convertimos a una unidad legible, si no dará error + try: + sizet = torrent["info"]['length'] + size = convert_size(sizet) + except: + pass + + #si tiene múltiples archivos sumamos la longitud de todos + if not size: + check_video = scrapertools.find_multiple_matches(str(torrent["info"]["files"]), "'length': (\d+)}") + sizet = sum([int(i) for i in check_video]) + size = convert_size(sizet) + + except: + logger.error('ERROR al buscar el tamaño de un .Torrent: ' + url) + + try: + os.remove(torrents_path + "/generictools.torrent") #borramos el .torrent + except: + pass + + #logger.debug(url + ' / ' + size) + + return size + + def get_field_from_kodi_DB(item, from_fields='*', files='file'): logger.info() """ From ce3690f06b046435bf3af0b070d6ad02951184f2 Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Wed, 12 Sep 2018 11:54:44 +0200 Subject: [PATCH 2/3] =?UTF-8?q?ZonaTorrent:=20canal=20redise=C3=B1ado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Películas por géneros, calidades, idiomas y alfabeto - Series completas agrupadas por temporadas - Búsquedas - Videoteca de Series - Control de página inteligente, en función de los items y del tiempo de proceso Mejoras de títulos, con calidades --- plugin.video.alfa/channels/zonatorrent.json | 67 +- plugin.video.alfa/channels/zonatorrent.py | 1096 ++++++++++++++--- .../media/channels/thumb/zonatorrent.png | Bin 0 -> 36220 bytes 3 files changed, 948 insertions(+), 215 deletions(-) create mode 100644 plugin.video.alfa/resources/media/channels/thumb/zonatorrent.png diff --git a/plugin.video.alfa/channels/zonatorrent.json b/plugin.video.alfa/channels/zonatorrent.json index 31f31200..ce829a98 100644 --- a/plugin.video.alfa/channels/zonatorrent.json +++ b/plugin.video.alfa/channels/zonatorrent.json @@ -5,28 +5,59 @@ "adult": false, "language": ["cast", "lat"], "banner": "", - "thumbnail": "https://zonatorrent.org/wp-content/uploads/2017/04/zonatorrent-New-Logo.png", + "thumbnail": "zonatorrent.png", "version": 1, "categories": [ - "torrent", - "movie" + "torrent", + "movie", + "tvshow", + "vos" ], "settings": [ - { - "id": "include_in_global_search", - "type": "bool", - "label": "Incluir en busqueda global", - "default": true, - "enabled": true, - "visible": true - }, - { - "id": "modo_grafico", - "type": "bool", - "label": "Buscar información extra", - "default": true, - "enabled": true, - "visible": true + { + "id": "include_in_global_search", + "type": "bool", + "label": "Incluir en busqueda global", + "default": true, + "enabled": true, + "visible": true + }, + { + "id": "modo_grafico", + "type": "bool", + "label": "Buscar información extra", + "default": true, + "enabled": true, + "visible": true + }, + { + "id": "timeout_downloadpage", + "type": "list", + "label": "Timeout (segs.) en descarga de páginas o verificación de servidores", + "default": 5, + "enabled": true, + "visible": true, + "lvalues": [ + "None", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + }, + { + "id": "seleccionar_ult_temporadda_activa", + "type": "bool", + "label": "Seleccionar para Videoteca si estará activa solo la última Temporada", + "default": true, + "enabled": true, + "visible": true }, { "id": "include_in_newest_peliculas", diff --git a/plugin.video.alfa/channels/zonatorrent.py b/plugin.video.alfa/channels/zonatorrent.py index d3a85b28..6bee0e95 100644 --- a/plugin.video.alfa/channels/zonatorrent.py +++ b/plugin.video.alfa/channels/zonatorrent.py @@ -1,197 +1,899 @@ -# -*- coding: utf-8 -*- -# -*- Channel TioTorrent -*- -# -*- Created for Alfa-addon -*- -# -*- By the Alfa Develop Group -*- - -import re - -from channelselector import get_thumb -from core import httptools -from core import scrapertools -from core import servertools -from core import tmdb -from core.item import Item -from platformcode import logger - -__channel__ = "zonatorrent" - -HOST = 'https://zonatorrent.org' - -try: - __modo_grafico__ = config.get_setting('modo_grafico', __channel__) -except: - __modo_grafico__ = True - - -def mainlist(item): - logger.info() - - itemlist = list() - itemlist.append(Item(channel=item.channel, title="Últimas Películas", action="listado", url=HOST, page=False)) - itemlist.append(Item(channel=item.channel, title="Alfabético", action="alfabetico")) - itemlist.append(Item(channel=item.channel, title="Géneros", action="generos", url=HOST)) - itemlist.append(Item(channel=item.channel, title="Más vistas", action="listado", url=HOST + "/peliculas-mas-vistas/")) - itemlist.append(Item(channel=item.channel, title="Más votadas", action="listado", url=HOST + "/peliculas-mas-votadas/")) - itemlist.append(Item(channel=item.channel, title="Castellano", action="listado", url=HOST + "/?s=spanish", - page=True)) - itemlist.append(Item(channel=item.channel, title="Latino", action="listado", url=HOST + "/?s=latino", page=True)) - itemlist.append(Item(channel=item.channel, title="Subtitulado", action="listado", url=HOST + "/?s=Subtitulado", - page=True)) - itemlist.append(Item(channel=item.channel, title="Con Torrent", action="listado", url=HOST + "/?s=torrent", - page=True)) - itemlist.append(Item(channel=item.channel, title="Buscar", action="search", url=HOST + "/?s=", - page=False)) - - return itemlist - - -def alfabetico(item): - logger.info() - - itemlist = [] - - for letra in "#ABCDEFGHIJKLMNOPQRSTUVWXYZ": - itemlist.append(Item(channel=item.channel, action="listado", title=letra, page=True, - url=HOST + "/letters/%s/" % letra.replace("#", "0-9"))) - - return itemlist - - -def generos(item): - logger.info() - - itemlist = [] - - data = re.sub(r"\n|\r|\t|\s{2}|()", "", httptools.downloadpage(item.url).data) - data = scrapertools.find_single_match(data, 'Generos(.*?)') - matches = scrapertools.find_multiple_matches(data, '(.*?)') - - for url, title in matches: - itemlist.append(Item(channel=item.channel, action="listado", title=title, url=url, page=True)) - - return itemlist - - -def search(item, texto): - logger.info() - item.url = item.url + texto.replace(" ", "+") - - try: - itemlist = listado(item) - except: - import sys - for line in sys.exc_info(): - logger.error("%s" % line) - return [] - - return itemlist - - -def listado(item): - logger.info() - - itemlist = [] - - data = re.sub(r"\n|\r|\t|\s{2}|()", "", httptools.downloadpage(item.url).data) - - pattern = ']+>]+>]+src="(?P[^"]+)"[^>]+>
' \ - '

(?P.*?)</h2>.*?<span class="Time[^>]+>(?P<duration>.*?)</span><span ' \ - 'class="Date[^>]+>(?P<year>.*?)</span><span class="Qlty">(?P<quality>.*?)</span></p><div ' \ - 'class="Description"><p>.*?\:\s*(?P<plot>.*?)</p>' - matches = re.compile(pattern, re.DOTALL).findall(data) - - for url, thumb, title, duration, year, quality, plot in matches: - #title = title.strip().replace("Spanish Online Torrent", "").replace("Latino Online Torrent", "").replace(r'\d{4}','') - title = re.sub('Online|Spanish|Latino|Torrent|\d{4}','',title) - infoLabels = {"year": year} - - aux = scrapertools.find_single_match(duration, "(\d+)h\s*(\d+)m") - duration = "%s" % ((int(aux[0]) * 3600) + (int(aux[1]) * 60)) - infoLabels["duration"] = duration - - itemlist.append(Item(channel=item.channel, action="findvideos", title=title, url=url, thumbnail=thumb, - contentTitle=title, plot=plot, infoLabels=infoLabels)) - tmdb.set_infoLabels_itemlist(itemlist, __modo_grafico__) - if item.page: - pattern = "<span class='page-numbers current'>[^<]+</span><a class='page-numbers' href='([^']+)'" - url = scrapertools.find_single_match(data, pattern) - - itemlist.append(Item(channel=item.channel, action="listado", title=">> Página siguiente", url=url, page=True, - thumbnail=get_thumb("next.png"))) - - return itemlist - - -def findvideos(item): - logger.info() - - itemlist = [] - language = '' - quality = '' - data = re.sub(r"\n|\r|\t|\s{2}|(<!--.*?-->)", "", httptools.downloadpage(item.url).data) - data = re.sub(r""", '"', data) - data = re.sub(r"<", '<', data) - - titles = re.compile('data-TPlayerNv="Opt\d+">.*? <span>(.*?)</span></li>', re.DOTALL).findall(data) - urls = re.compile('id="Opt\d+"><iframe[^>]+src="([^"]+)"', re.DOTALL).findall(data) - - if len(titles) == len(urls): - for i in range(0, len(titles)): - if i > 0: - logger.debug('titles: %s' % titles[i].strip()) - language, quality = titles[i].split(' - ') - title = "%s" % titles[i].strip() - else: - title = titles[0] - - if "goo.gl" in urls[i]: - urls[i] = httptools.downloadpage(urls[i], follow_redirects=False, only_headers=True)\ - .headers.get("location", "") - videourl = servertools.findvideos(urls[i]) - if len(videourl) > 0: - server = videourl[0][0].capitalize() - title = '%s %s' % (server, title) - itemlist.append(Item(channel=item.channel, action="play", title=title, url=videourl[0][1], - server=server, thumbnail=videourl[0][3], fulltitle=item.title, - language=language, quality=quality )) - - pattern = '<a[^>]+href="([^"]+)"[^<]+</a></td><td><span><img[^>]+>(.*?)</span></td><td><span><img[^>]+>(.*?)' \ - '</span></td><td><span>(.*?)</span>' - torrents = re.compile(pattern, re.DOTALL).findall(data) - - if len(torrents) > 0: - for url, text, lang, quality in torrents: - title = "%s %s - %s" % (text, lang, quality) - itemlist.append(Item(channel=item.channel, action="play", title=title, url=url, server="torrent", - fulltitle=item.title, thumbnail=get_thumb("channels_torrent.png"))) - - return itemlist - -def newest(categoria): - logger.info() - itemlist = [] - item = Item() - try: - if categoria == 'peliculas': - item.url = HOST - elif categoria == 'infantiles': - item.url = HOST + "/animacion" - elif categoria == 'terror': - item.url = HOST + "/terror/" - elif categoria == 'torrent': - item.url = HOST + "/?s=torrent" - else: - return [] - - itemlist = listado(item) - if itemlist[-1].title == ">> Página siguiente": - itemlist.pop() - - # Se captura la excepción, para no interrumpir al canal novedades si un canal falla - except: - import sys - for line in sys.exc_info(): - logger.error("{0}".format(line)) - return [] - - return itemlist +# -*- coding: utf-8 -*- + +import re +import sys +import urllib +import urlparse +import time + +from channelselector import get_thumb +from core import httptools +from core import scrapertools +from core import servertools +from core.item import Item +from platformcode import config, logger +from core import tmdb +from lib import generictools + + +host = 'https://zonatorrent.tv/' +channel = "zonatorrent" + +categoria = channel.capitalize() +__modo_grafico__ = config.get_setting('modo_grafico', channel) +modo_ultima_temp = config.get_setting('seleccionar_ult_temporadda_activa', channel) #Actualización sólo últ. Temporada? +timeout = config.get_setting('timeout_downloadpage', channel) + + +def mainlist(item): + logger.info() + itemlist = [] + + thumb_pelis = get_thumb("channels_movie.png") + thumb_series = get_thumb("channels_tvshow.png") + thumb_buscar = get_thumb("search.png") + thumb_separador = get_thumb("next.png") + + + itemlist.append(Item(channel=item.channel, title="Películas", action="submenu", url=host, thumbnail=thumb_pelis, extra="peliculas")) + + itemlist.append(Item(channel=item.channel, url=host, title="Series", action="submenu", thumbnail=thumb_series, extra="series")) + + itemlist.append(Item(channel=item.channel, title="Buscar...", action="search", url=host + "?s=", thumbnail=thumb_buscar, extra="search")) + + return itemlist + + +def submenu(item): + logger.info() + itemlist = [] + + thumb_cartelera = get_thumb("now_playing.png") + thumb_pelis_az = get_thumb("channels_movie_az.png") + thumb_pelis = get_thumb("channels_movie.png") + thumb_pelis_hd = get_thumb("channels_movie_hd.png") + thumb_pelis_vos = get_thumb("channels_vos.png") + thumb_popular = get_thumb("popular.png") + thumb_generos = get_thumb("genres.png") + thumb_spanish = get_thumb("channels_spanish.png") + thumb_latino = get_thumb("channels_latino.png") + thumb_torrent = get_thumb("channels_torrent.png") + thumb_series = get_thumb("channels_tvshow.png") + thumb_series_az = get_thumb("channels_tvshow_az.png") + + + if item.extra != "series": + item.url_plus = "movies" + itemlist.append(item.clone(title="Últimas Películas", action="listado", url=host + "estrenos-de-cine-2", url_plus=item.url_plus, thumbnail=thumb_cartelera)) + itemlist.append(item.clone(title="Alfabético", action="alfabeto", url=host + "letters/%s", thumbnail=thumb_pelis_az, extra2 = 'alfabeto')) + itemlist.append(item.clone(title="Géneros", action="categorias", url=host + item.url_plus, url_plus=item.url_plus, extra2= "generos", thumbnail=thumb_generos)) + itemlist.append(item.clone(title="Calidades", action="categorias", url=host + item.url_plus, url_plus=item.url_plus, extra2= "calidades", thumbnail=thumb_pelis_hd)) + itemlist.append(item.clone(title="Más vistas", action="listado", url=host + "/peliculas-mas-vistas-2/", url_plus=item.url_plus, thumbnail=thumb_popular, extra2="popular")) + itemlist.append(item.clone(title="Más votadas", action="listado", url=host + "/peliculas-mas-votadas/", url_plus=item.url_plus, thumbnail=thumb_popular, extra2="popular")) + itemlist.append(item.clone(title="Castellano", action="listado", url=host + "?s=spanish", url_plus=item.url_plus, thumbnail=thumb_spanish, extra2="CAST")) + itemlist.append(item.clone(title="Latino", action="listado", url=host + "?s=latino", url_plus=item.url_plus, thumbnail=thumb_latino, lextra2="LAT")) + itemlist.append(item.clone(title="Subtitulado", action="listado", url=host + "?s=Subtitulado", url_plus=item.url_plus, thumbnail=thumb_pelis_vos, extra2="VOSE")) + + else: + item.url_plus = "serie-tv" + itemlist.append(item.clone(title="Series completas", action="listado", url=item.url + item.url_plus, url_plus=item.url_plus, thumbnail=thumb_series, extra="series")) + itemlist.append(item.clone(title="Alfabético A-Z", action="alfabeto", url=item.url + "letters/%s", url_plus=item.url_plus, thumbnail=thumb_series_az, extra="series", extra2 = 'alfabeto')) + itemlist.append(item.clone(title="Más vistas", action="listado", url=host + "/peliculas-mas-vistas-2/", url_plus=item.url_plus, thumbnail=thumb_popular, extra2="popular")) + itemlist.append(item.clone(title="Más votadas", action="listado", url=host + "/peliculas-mas-votadas/", url_plus=item.url_plus, thumbnail=thumb_popular, extra2="popular")) + itemlist.append(item.clone(title="Castellano", action="listado", url=host + "?s=spanish", url_plus=item.url_plus, thumbnail=thumb_spanish, extra2="CAST")) + itemlist.append(item.clone(title="Latino", action="listado", url=host + "?s=latino", url_plus=item.url_plus, thumbnail=thumb_latino, extra2="LAT")) + itemlist.append(item.clone(title="Subtitulado", action="listado", url=host + "?s=Subtitulado", url_plus=item.url_plus, thumbnail=thumb_pelis_vos, extra2="VOSE")) + + return itemlist + + +def categorias(item): + logger.info() + + itemlist = [] + + data = '' + try: + data = re.sub(r"\n|\r|\t|\s{2}|(<!--.*?-->)", "", httptools.downloadpage(item.url, timeout=timeout).data) + data = unicode(data, "utf-8", errors="replace").encode("utf-8") + except: + pass + + patron = '<div id="categories-2" class="Wdgt widget_categories"><div class="Title widget-title">Categorías</div><ul>(.*?)<\/ul><\/div>' + #Verificamos si se ha cargado una página, y si además tiene la estructura correcta + if not data or not scrapertools.find_single_match(data, patron): + item = generictools.web_intervenida(item, data) #Verificamos que no haya sido clausurada + if item.intervencion: #Sí ha sido clausurada judicialmente + for clone_inter, autoridad in item.intervencion: + thumb_intervenido = get_thumb(autoridad) + itemlist.append(item.clone(action='', title="[COLOR yellow]" + clone_inter.capitalize() + ': [/COLOR]' + intervenido_judicial + '. Reportar el problema en el foro', thumbnail=thumb_intervenido)) + return itemlist #Salimos + + logger.error("ERROR 01: SUBMENU: La Web no responde o ha cambiado de URL: " + item.url + data) + if not data: #Si no ha logrado encontrar nada, salimos + itemlist.append(item.clone(action='', title=item.category + ': ERROR 01: SUBMENU: La Web no responde o ha cambiado de URL. Si la Web está activa, reportar el error con el log')) + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + data = scrapertools.find_single_match(data, patron) + patron = '<li class="[^>]+><a href="([^"]+)"\s?(?:title="[^"]+")?>(.*?)<\/a><\/li>' + matches = re.compile(patron, re.DOTALL).findall(data) + + if not matches: + logger.error("ERROR 02: SUBMENU: Ha cambiado la estructura de la Web " + " / PATRON: " + patron + " / DATA: " + data) + itemlist.append(item.clone(action='', title=item.category + ': ERROR 02: SUBMENU: Ha cambiado la estructura de la Web. Reportar el error con el log')) + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + #logger.debug(item.url_plus) + #logger.debug(matches) + + for scrapedurl, scrapedtitle in matches: + + #Preguntamos por las entradas que corresponden al "extra2" + if item.extra2 == 'calidades': + if scrapedtitle.lower() in ['dvd full', 'tshq', 'bdrip', 'dvdscreener', 'brscreener r6', 'brscreener', 'webscreener', 'dvd', 'hdrip', 'screener', 'screeer', 'webrip', 'brrip', 'dvb', 'dvdrip', 'dvdsc', 'dvdsc - r6', 'hdts', 'hdtv', 'kvcd', 'line', 'ppv', 'telesync', 'ts hq', 'ts hq proper', '480p', '720p', 'ac3', 'bluray', 'camrip', 'ddc', 'hdtv - screener', 'tc screener', 'ts screener', 'ts screener alto', 'ts screener medio', 'vhs screener']: + itemlist.append(item.clone(action="listado", title=scrapedtitle.capitalize().strip(), url=scrapedurl)) + + else: + if scrapedtitle.lower() not in ['estrenos de cine', 'serie tv', 'dvd full', 'tshq', 'bdrip', 'dvdscreener', 'brscreener r6', 'brscreener', 'webscreener', 'dvd', 'hdrip', 'screener', 'screeer', 'webrip', 'brrip', 'dvb', 'dvdrip', 'dvdsc', 'dvdsc - r6', 'hdts', 'hdtv', 'kvcd', 'line', 'ppv', 'telesync', 'ts hq', 'ts hq proper', '480p', '720p', 'ac3', 'bluray', 'camrip', 'ddc', 'hdtv - screener', 'tc screener', 'ts screener', 'ts screener alto', 'ts screener medio', 'vhs screener']: + itemlist.append(item.clone(action="listado", title=scrapedtitle.capitalize().strip(), url=scrapedurl)) + + return itemlist + + +def alfabeto(item): + logger.info() + itemlist = [] + + itemlist.append(item.clone(action="listado", title="0-9", url=item.url % "0-9")) + + for letra in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']: + itemlist.append(item.clone(action="listado", title=letra, url=item.url % letra.lower())) + + return itemlist + + +def listado(item): + logger.info() + itemlist = [] + item.category = categoria + + #logger.debug(item) + + curr_page = 1 # Página inicial + last_page = 99999 # Última página inicial + if item.curr_page: + curr_page = int(item.curr_page) # Si viene de una pasada anterior, lo usamos + del item.curr_page # ... y lo borramos + if item.last_page: + last_page = int(item.last_page) # Si viene de una pasada anterior, lo usamos + del item.last_page # ... y lo borramos + + cnt_tot = 40 # Poner el num. máximo de items por página + cnt_title = 0 # Contador de líneas insertadas en Itemlist + inicio = time.time() # Controlaremos que el proceso no exceda de un tiempo razonable + fin = inicio + 10 # Después de este tiempo pintamos (segundos) + timeout_search = timeout # Timeout para descargas + if item.extra == 'search': + timeout_search = timeout * 2 # Timeout un poco más largo para las búsquedas + if timeout_search < 5: + timeout_search = 5 # Timeout un poco más largo para las búsquedas + + #Sistema de paginado para evitar páginas vacías o semi-vacías en casos de búsquedas con series con muchos episodios + title_lista = [] # Guarda la lista de series que ya están en Itemlist, para no duplicar lineas + if item.title_lista: # Si viene de una pasada anterior, la lista ya estará guardada + title_lista.extend(item.title_lista) # Se usa la lista de páginas anteriores en Item + del item.title_lista # ... limpiamos + + if not item.extra2: # Si viene de Catálogo o de Alfabeto + item.extra2 = '' + + next_page_url = item.url + #Máximo num. de líneas permitidas por TMDB. Máx de 10 segundos por Itemlist para no degradar el rendimiento + while cnt_title <= cnt_tot * 0.45 and curr_page <= last_page and fin > time.time(): + + # Descarga la página + data = '' + try: + data = re.sub(r"\n|\r|\t|\s{2}|(<!--.*?-->)| ", "", httptools.downloadpage(next_page_url, timeout=timeout_search).data) + data = unicode(data, "utf-8", errors="replace").encode("utf-8") + except: + pass + + if not data: #Si la web está caída salimos sin dar error + logger.error("ERROR 01: LISTADO: La Web no responde o ha cambiado de URL: " + item.url + " / DATA: " + data) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 01: LISTADO:. La Web no responde o ha cambiado de URL. Si la Web está activa, reportar el error con el log')) + break #si no hay más datos, algo no funciona, pintamos lo que tenemos + + #Patrón para todo, menos para Alfabeto + patron = '<li class="TPostMv"><article id="[^"]+" class="[^"]+"><a href="(?P<url>[^"]+)".*?><div[^>]+><figure[^>]+><img[^>]+src="(?P<thumb>[^"]+)"[^>]+><\/figure>(?:<span class="TpTv BgA">(.*?)<\/span>)?<\/div><h2 class="Title">(?P<title>.*?)<\/h2>.*?<span class="Time[^>]+>(?P<duration>.*?)<\/span><span class="Date[^>]+>(?P<year>.*?)<\/span>(?:<span class="Qlty">(?P<quality>.*?)<\/span>)?<\/p><div class="Description">.*?<\/div><\/div><\/article><\/li>' + + #Si viene de Alfabeto, ponemos un patrón especializado + if item.extra2 == 'alfabeto': + patron = '<td class="MvTbImg"><a href="(?P<url>[^"]+)".*?src="(?P<thumb>[^"]+)"[^>]+>(?:<span class="TpTv BgA">(.*?)<\/span>)?<\/a><\/td>[^>]+>[^>]+><strong>(?P<title>.*?)<\/strong><\/a><\/td><td>(?P<year>.*?)<\/td><td><p class="Info"><span class="Qlty">(?P<quality>.*?)<\/span><\/p><\/td><td>(?P<duration>.*?)<\/td>' + + matches = re.compile(patron, re.DOTALL).findall(data) + if not matches and not 'Lo sentimos, no tenemos nada que mostrar' in data: #error + item = generictools.web_intervenida(item, data) #Verificamos que no haya sido clausurada + if item.intervencion: #Sí ha sido clausurada judicialmente + item, itemlist = generictools.post_tmdb_episodios(item, itemlist) #Llamamos al método para el pintado del error + return itemlist #Salimos + + logger.error("ERROR 02: LISTADO: Ha cambiado la estructura de la Web " + " / PATRON: " + patron + " / DATA: " + data) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 02: LISTADO: Ha cambiado la estructura de la Web. Reportar el error con el log')) + break #si no hay más datos, algo no funciona, pintamos lo que tenemos + + #logger.debug("PATRON: " + patron) + #logger.debug(matches) + #logger.debug(data) + + #Buscamos la url de paginado y la última página + if item.extra2 == 'alfabeto': #patrón especial + patron = "<div class='wp-pagenavi'><span class='pages'>Pagina \d+ of (\d+)<\/span><span class='current'>(\d+)<\/span>" + patron += '<a class="page larger" title="[^"]+" href="([^"]+)">' + else: + patron = '<div class="tr-pagnav wp-pagenavi">' + patron += "<span aria-current='page' class='page-numbers current'>(\d+)<\/span>.*?<a class='page-numbers' href='[^+]+'>(\d+)<\/a>" + patron += '<a class="next page-numbers" href="([^"]+)">Siguiente' + + if last_page == 99999: #Si es el valor inicial, buscamos + try: + if item.extra2 == 'alfabeto': #patrón especial + last_page, curr_page, next_page_url = scrapertools.find_single_match(data, patron) + else: + curr_page, last_page, next_page_url = scrapertools.find_single_match(data, patron) + curr_page = int(curr_page) + last_page = int(last_page) + except: #Si no lo encuentra, lo ponemos a 1 + #logger.error('ERROR 03: LISTADO: Al obtener la paginación: ' + patron) + curr_page = 1 + last_page = 0 + next_page_url = item.url + '/page/1' + #logger.debug('curr_page: ' + str(curr_page) + ' / last_page: ' + str(last_page) + ' / url: ' + next_page_url) + if last_page > 1: + curr_page += 1 #Apunto ya a la página siguiente + next_page_url = re.sub(r'\/page\/\d+', '/page/%s' % curr_page, next_page_url) + + #Empezamos el procesado de matches + for scrapedurl, scrapedthumb, scrapedtype, scrapedtitle, scrapedduration, scrapedyear, scrapedquality in matches: + if item.extra2 == 'alfabeto': #Cambia el orden de tres parámetros + duration = scrapedquality + year = scrapedduration + quality = scrapedyear + else: #lo estándar + duration = scrapedduration + year = scrapedyear + quality = scrapedquality + + #estandarizamos la duración + if 'h' not in duration: + duration = '0:' + duration.replace('m', '') + else: + duration = duration.replace('h ', ':').replace('m', '') + duration = re.sub(r',.*?\]', ']', duration) + if '0:0' in duration or ',' in duration: + duration = '' + else: + try: + hora, minuto = duration.split(':') + duration = '%s:%s h' % (str(hora).zfill(2), str(minuto).zfill(2)) + except: + duration = '' + + #Algunos enlaces no filtran tipos, lo hago aquí + if item.extra2 in ['alfabeto', 'CAST', 'LAT', 'VOSE', 'popular'] or item.category_new == 'newest': + if item.extra == 'peliculas' and 'tv' in scrapedtype.lower(): + continue + elif item.extra == 'series' and not 'tv' in scrapedtype.lower(): + continue + + title = scrapedtitle + title = title.replace("á", "a").replace("é", "e").replace("í", "i").replace("ó", "o").replace("ú", "u").replace("ü", "u").replace("�", "ñ").replace("ñ", "ñ").replace("ã", "a").replace("&etilde;", "e").replace("ĩ", "i").replace("õ", "o").replace("ũ", "u").replace("ñ", "ñ").replace("’", "'") + + cnt_title += 1 + + item_local = item.clone() #Creamos copia de Item para trabajar + if item_local.tipo: #... y limpiamos + del item_local.tipo + if item_local.totalItems: + del item_local.totalItems + if item_local.post_num: + del item_local.post_num + if item_local.intervencion: + del item_local.intervencion + if item_local.viewmode: + del item_local.viewmode + item_local.text_bold = True + del item_local.text_bold + item_local.text_color = True + del item_local.text_color + if item_local.url_plus: + del item_local.url_plus + + title_subs = [] #creamos una lista para guardar info importante + item_local.language = [] #iniciamos Lenguaje + item_local.quality = quality #guardamos la calidad, si la hay + item_local.url = scrapedurl #guardamos el thumb + item_local.thumbnail = scrapedthumb #guardamos el thumb + item_local.context = "['buscar_trailer']" + + item_local.contentType = "movie" #por defecto, son películas + item_local.action = "findvideos" + + #Analizamos los formatos de series + if '-serie-tv-' in scrapedurl or item_local.extra == 'series' or 'tv' in scrapedtype.lower(): + item_local.contentType = "tvshow" + item_local.action = "episodios" + item_local.season_colapse = True #Muestra las series agrupadas por temporadas + + #Buscamos calidades adicionales + if "3d" in title.lower() and not "3d" in item_local.quality.lower(): + if item_local.quality: + item_local.quality += " 3D" + else: + item_local.quality = "3D" + title = re.sub('3D', '', title, flags=re.IGNORECASE) + title = title.replace('[]', '') + if item_local.quality: + item_local.quality += ' %s' % scrapertools.find_single_match(title, '\[(.*?)\]') + else: + item_local.quality = '%s' % scrapertools.find_single_match(title, '\[(.*?)\]') + + #Detectamos idiomas + if 'LAT' in item.extra2: + item_local.language += ['LAT'] + elif 'VOSE' in item.extra2: + item_local.language += ['VOSE'] + if item_local.extra2: del item_local.extra2 + + if ("latino" in scrapedurl.lower() or "latino" in title.lower()) and "LAT" not in item_local.language: + item_local.language += ['LAT'] + elif ('subtitulado' in scrapedurl.lower() or 'subtitulado' in title.lower() or 'vose' in title.lower()) and "VOSE" not in item_local.language: + item_local.language += ['VOSE'] + elif ('version-original' in scrapedurl.lower() or 'version original' in title.lower() or 'vo' in title.lower()) and "VO" not in item_local.language: + item_local.language += ['VO'] + + if item_local.language == []: + item_local.language = ['CAST'] + + #Detectamos info interesante a guardar para después de TMDB + if scrapertools.find_single_match(title, '[m|M].*?serie'): + title = re.sub(r'[m|M]iniserie', '', title) + title_subs += ["Miniserie"] + if scrapertools.find_single_match(title, '[s|S]aga'): + title = re.sub(r'[s|S]aga', '', title) + title_subs += ["Saga"] + if scrapertools.find_single_match(title, '[c|C]olecc'): + title = re.sub(r'[c|C]olecc...', '', title) + title_subs += ["Colección"] + + if "duolog" in title.lower(): + title_subs += ["[Saga]"] + title = title.replace(" Duologia", "").replace(" duologia", "").replace(" Duolog", "").replace(" duolog", "") + if "trilog" in title.lower(): + title_subs += ["[Saga]"] + title = title.replace(" Trilogia", "").replace(" trilogia", "").replace(" Trilog", "").replace(" trilog", "") + if "extendida" in title.lower() or "v.e." in title.lower()or "v e " in title.lower(): + title_subs += ["[V. Extendida]"] + title = title.replace("Version Extendida", "").replace("(Version Extendida)", "").replace("V. Extendida", "").replace("VExtendida", "").replace("V Extendida", "").replace("V.Extendida", "").replace("V Extendida", "").replace("V.E.", "").replace("V E ", "").replace("V:Extendida", "") + + #Analizamos el año. Si no está claro ponemos '-' + try: + yeat_int = int(year) + if yeat_int >= 1970 and yeat_int <= 2040: + item_local.infoLabels["year"] = yeat_int + else: + item_local.infoLabels["year"] = '-' + except: + item_local.infoLabels["year"] = '-' + + #Empezamos a limpiar el título en varias pasadas + patron = '\s?-?\s?(line)?\s?-\s?$' + regex = re.compile(patron, re.I) + title = regex.sub("", title) + title = re.sub(r'\(\d{4}\s*?\)', '', title) + title = re.sub(r'\[\d{4}\s*?\]', '', title) + title = re.sub(r'[s|S]erie', '', title) + title = re.sub(r'- $', '', title) + + #Limpiamos el título de la basura innecesaria + title = re.sub(r'TV|Online|Spanish|Torrent|en Espa\xc3\xb1ol|Español|Latino|Subtitulado|Blurayrip|Bluray rip|\[.*?\]|R2 Pal|\xe3\x80\x90 Descargar Torrent \xe3\x80\x91|Completa|Temporada|Descargar|Torren', '', title, flags=re.IGNORECASE) + + title = title.replace("Dual", "").replace("dual", "").replace("Subtitulada", "").replace("subtitulada", "").replace("Subt", "").replace("subt", "").replace("(Proper)", "").replace("(proper)", "").replace("Proper", "").replace("proper", "").replace("#", "").replace("(Latino)", "").replace("Latino", "").replace("LATINO", "").replace("Spanish", "").replace("Trailer", "").replace("Audio", "") + title = title.replace("HDTV-Screener", "").replace("DVDSCR", "").replace("TS ALTA", "").replace("- HDRip", "").replace("(HDRip)", "").replace("- Hdrip", "").replace("(microHD)", "").replace("(DVDRip)", "").replace("HDRip", "").replace("(BR-LINE)", "").replace("(HDTS-SCREENER)", "").replace("(BDRip)", "").replace("(BR-Screener)", "").replace("(DVDScreener)", "").replace("TS-Screener", "").replace(" TS", "").replace(" Ts", "").replace(" 480p", "").replace(" 480P", "").replace(" 720p", "").replace(" 720P", "").replace(" 1080p", "").replace(" 1080P", "").replace("DVDRip", "").replace(" Dvd", "").replace(" DVD", "").replace(" V.O", "").replace(" Unrated", "").replace(" UNRATED", "").replace(" unrated", "").replace("screener", "").replace("TS-SCREENER", "").replace("TSScreener", "").replace("HQ", "").replace("AC3 5.1", "").replace("Telesync", "").replace("Line Dubbed", "").replace("line Dubbed", "").replace("LineDuB", "").replace("Line", "").replace("XviD", "").replace("xvid", "").replace("XVID", "").replace("Mic Dubbed", "").replace("HD", "").replace("V2", "").replace("CAM", "").replace("VHS.SCR", "").replace("Dvd5", "").replace("DVD5", "").replace("Iso", "").replace("ISO", "").replace("Reparado", "").replace("reparado", "").replace("DVD9", "").replace("Dvd9", "") + + #Terminamos de limpiar el título + title = re.sub(r'\??\s?\d*?\&.*', '', title) + title = re.sub(r'[\(|\[]\s+[\)|\]]', '', title) + title = title.replace('()', '').replace('[]', '').strip().lower().title() + + #Limpiamos el año del título, siempre que no sea todo el título o una cifra de más dígitos + if not scrapertools.find_single_match(title, '\d{5}'): + title_alt = title + title_alt = re.sub(r'[\[|\(]?\d{4}[\)|\]]?', '', title_alt).strip() + if title_alt: + title = title_alt + + item_local.from_title = title.strip().lower().title() #Guardamos esta etiqueta para posible desambiguación de título + + #Salvamos el título según el tipo de contenido + if item_local.contentType == "movie": + item_local.contentTitle = title.strip().lower().title() + else: + item_local.contentSerieName = title.strip().lower().title() + + item_local.title = title.strip().lower().title() + + #Añadimos la duración a la Calidad + if duration: + if item_local.quality: + item_local.quality += ' [%s]' % duration + else: + item_local.quality = '[%s]' % duration + + #Guarda la variable temporal que almacena la info adicional del título a ser restaurada después de TMDB + item_local.title_subs = title_subs + + itemlist.append(item_local.clone()) #Pintar pantalla + + #logger.debug(item_local) + + #Pasamos a TMDB la lista completa Itemlist + tmdb.set_infoLabels(itemlist, __modo_grafico__) + + #Llamamos al método para el maquillaje de los títulos obtenidos desde TMDB + item, itemlist = generictools.post_tmdb_listado(item, itemlist) + + # Si es necesario añadir paginacion + if curr_page <= last_page: + if last_page > 1: + title = '%s de %s' % (curr_page-1, last_page) + else: + title = '%s' % curr_page-1 + + itemlist.append(Item(channel=item.channel, action="listado", title=">> Página siguiente " + title, title_lista=title_lista, url=next_page_url, extra=item.extra, extra2=item.extra2, last_page=str(last_page), curr_page=str(curr_page))) + + return itemlist + + +def findvideos(item): + logger.info() + itemlist = [] + matches = [] + item.category = categoria + + item.extra2 = 'xyz' + del item.extra2 + + #logger.debug(item) + + #Bajamos los datos de la página + data = '' + patron = '<a[^>]+href="([^"]+)"[^<]+</a></td><td><span><img[^>]+>(.*?)</span></td><td><span><img[^>]+>(.*?)</span></td><td><span>(.*?)</span>' + try: + data = re.sub(r"\n|\r|\t|\s{2}|(<!--.*?-->)", "", httptools.downloadpage(item.url, timeout=timeout).data) + data = unicode(data, "utf-8", errors="replace").encode("utf-8") + data = re.sub(r""", '"', data) + data = re.sub(r"<", '<', data) + except: + pass + + if not data: + logger.error("ERROR 01: FINDVIDEOS: La Web no responde o la URL es erronea: " + item.url) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 01: FINDVIDEOS:. La Web no responde o la URL es erronea. Si la Web está activa, reportar el error con el log')) + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + matches = re.compile(patron, re.DOTALL).findall(data) + if not matches and not scrapertools.find_single_match(data, 'data-TPlayerNv="Opt\d+">.*? <span>(.*?)</span></li>'): #error + logger.error("ERROR 02: FINDVIDEOS: No hay enlaces o ha cambiado la estructura de la Web " + " / PATRON: " + patron + data) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 02: FINDVIDEOS: No hay enlaces o ha cambiado la estructura de la Web. Verificar en la Web esto último y reportar el error con el log')) + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + #logger.debug("PATRON: " + patron) + #logger.debug(matches) + #logger.debug(data) + + #Llamamos al método para crear el título general del vídeo, con toda la información obtenida de TMDB + item, itemlist = generictools.post_tmdb_findvideos(item, itemlist) + + #Ahora tratamos los enlaces .torrent + for scrapedurl, scrapedserver, language, quality in matches: #leemos los torrents con la diferentes calidades + #Generamos una copia de Item para trabajar sobre ella + item_local = item.clone() + + if 'torrent' not in scrapedserver.lower(): #Si es un servidor Directo, lo dejamos para luego + continue + + item_local.url = scrapedurl + if '.io/' in item_local.url: + item_local.url = re.sub(r'http.?:\/\/\w+\.\w+\/', host, item_local.url) #Aseguramos el dominio del canal + + #Detectamos idiomas + if ("latino" in scrapedurl.lower() or "latino" in language.lower()) and "LAT" not in item_local.language: + item_local.language += ['LAT'] + elif ('subtitulado' in scrapedurl.lower() or 'subtitulado' in language.lower() or 'vose' in language.lower()) and "VOSE" not in item_local.language: + item_local.language += ['VOSE'] + elif ('version-original' in scrapedurl.lower() or 'version original' in language.lower() or 'vo' in language.lower()) and "VO" not in item_local.language: + item_local.language += ['VO'] + + if item_local.language == []: + item_local.language = ['CAST'] + + #Añadimos la calidad y copiamos la duración + item_local.quality = quality + if scrapertools.find_single_match(item.quality, '(\[\d+:\d+\ h])'): + item_local.quality += ' [/COLOR][COLOR white]%s' % scrapertools.find_single_match(item.quality, '(\[\d+:\d+\ h])') + + #Buscamos si ya tiene tamaño, si no, los buscamos en el archivo .torrent + size = scrapertools.find_single_match(item_local.quality, '\s\[(\d+,?\d*?\s\w\s?[b|B])\]') + if not size: + size = generictools.get_torrent_size(item_local.url) #Buscamos el tamaño en el .torrent + if size: + item_local.title = re.sub(r'\s\[\d+,?\d*?\s\w[b|B]\]', '', item_local.title) #Quitamos size de título, si lo traía + item_local.title = '%s [%s]' % (item_local.title, size) #Agregamos size al final del título + size = size.replace('GB', 'G B').replace('Gb', 'G b').replace('MB', 'M B').replace('Mb', 'M b') + item_local.quality = re.sub(r'\s\[\d+,?\d*?\s\w\s?[b|B]\]', '', item_local.quality) #Quitamos size de calidad, si lo traía + item_local.quality = '%s [%s]' % (item_local.quality, size) #Agregamos size al final de la calidad + + #Ahora pintamos el link del Torrent + item_local.title = '[COLOR yellow][?][/COLOR] [COLOR yellow][Torrent][/COLOR] [COLOR limegreen][%s][/COLOR] [COLOR red]%s[/COLOR]' % (item_local.quality, str(item_local.language)) + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.quality) + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality).strip() + item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + + item_local.alive = "??" #Calidad del link sin verificar + item_local.action = "play" #Visualizar vídeo + item_local.server = "torrent" #Servidor Torrent + + itemlist.append(item_local.clone()) #Pintar pantalla + + #logger.debug("TORRENT: " + scrapedurl + " / title gen/torr: " + item.title + " / " + item_local.title + " / calidad: " + item_local.quality + " / content: " + item_local.contentTitle + " / " + item_local.contentSerieName) + #logger.debug(item_local) + + #Ahora tratamos los Servidores Directos + titles = re.compile('data-TPlayerNv="Opt\d+">.*? <span>(.*?)</span></li>', re.DOTALL).findall(data) + urls = re.compile('id="Opt\d+"><iframe[^>]+src="([^"]+)"', re.DOTALL).findall(data) + + #Recorremos la lista de servidores Directos, excluyendo YouTube para trailers + if len(titles) == len(urls): + for i in range(0, len(titles)): + #Generamos una copia de Item para trabajar sobre ella + item_local = item.clone() + + if i > 0: + #logger.debug('titles: %s' % titles[i].strip()) + language, quality = titles[i].split(' - ') + title = "%s" % titles[i].strip() + else: + title = titles[0] + + if "goo.gl" in urls[i]: + urls[i] = httptools.downloadpage(urls[i], follow_redirects=False, only_headers=True)\ + .headers.get("location", "") + + videourl = servertools.findvideos(urls[i]) #Buscamos la url del vídeo + + #Ya tenemos un enlace, lo pintamos + if len(videourl) > 0: + server = videourl[0][0] + enlace = videourl[0][1] + mostrar_server = True + if config.get_setting("hidepremium"): #Si no se aceptan servidore premium, se ignoran + mostrar_server = servertools.is_server_enabled(server) + if mostrar_server: + item_local.alive = "??" #Se asume poe defecto que es link es dudoso + if server.lower() == 'youtube': #Pasamos de YouTube, usamos Trailers de Alfa + continue + if server.lower() != 'netutv': #Este servidor no se puede comprobar + #Llama a la subfunción de check_list_links(itemlist) para cada link de servidor + item_local.alive = servertools.check_video_link(enlace, server, timeout=timeout) + if '?' in item_local.alive: + alive = '?' #No se ha podido comprobar el vídeo + elif 'no' in item_local.alive.lower(): + continue #El enlace es malo + else: + alive = '' #El enlace está verificado + + #Detectamos idiomas + item_local.language = [] + if "latino" in language.lower() and "LAT" not in item_local.language: + item_local.language += ['LAT'] + elif ('subtitulado' in language.lower() or 'vose' in language.lower()) and "VOSE" not in item_local.language: + item_local.language += ['VOSE'] + elif ('version original' in language.lower() or 'vo' in language.lower()) and "VO" not in item_local.language: + item_local.language += ['VO'] + + if item_local.language == []: + item_local.language = ['CAST'] + + #Ahora pintamos el link del Servidor Directo + item_local.url = enlace + item_local.quality = quality #Añadimos la calidad + if scrapertools.find_single_match(item.quality, '(\[\d+:\d+\ h])'): #Añadimos la duración + item_local.quality += ' [/COLOR][COLOR white]%s' % scrapertools.find_single_match(item.quality, '(\[\d+:\d+\ h])') + item_local.title = '[COLOR yellow][%s][/COLOR] [COLOR yellow][%s][/COLOR] [COLOR limegreen][%s][/COLOR] [COLOR red]%s[/COLOR]' % (alive, server.capitalize(), item_local.quality, str(item_local.language)) + + #Preparamos título y calidad, quitamos etiquetas vacías + item_local.title = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.title) + item_local.title = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.title) + item_local.title = item_local.title.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\[\[?\s?\]?\]\[\/COLOR\]', '', item_local.quality) + item_local.quality = re.sub(r'\s?\[COLOR \w+\]\s?\[\/COLOR\]', '', item_local.quality) + item_local.quality = item_local.quality.replace("--", "").replace("[]", "").replace("()", "").replace("(/)", "").replace("[/]", "").strip() + + item_local.action = "play" #Visualizar vídeo + item_local.server = server #Servidor Directo + + itemlist.append(item_local.clone()) #Pintar pantalla + + #logger.debug("DIRECTO: " server + ' / ' + enlace + " / title: " + item.title + " / " + item_local.title + " / calidad: " + item_local.quality + " / content: " + item_local.contentTitle + " / " + item_local.contentSerieName) + #logger.debug(item_local) + + return itemlist + + +def episodios(item): + logger.info() + itemlist = [] + item.category = categoria + + #logger.debug(item) + + if item.from_title: + item.title = item.from_title + item.extra2 = 'xyz' + del item.extra2 + + item.quality = re.sub(r'\s?\[\d+:\d+\ h]', '', item.quality) #quitamos la duración de la serie + + #Limpiamos num. Temporada y Episodio que ha podido quedar por Novedades + season_display = 0 + if item.contentSeason: + if item.season_colapse: #Si viene del menú de Temporadas... + season_display = item.contentSeason #... salvamos el num de sesión a pintar + item.from_num_season_colapse = season_display + del item.season_colapse + item.contentType = "tvshow" + if item.from_title_season_colapse: + item.title = item.from_title_season_colapse + del item.from_title_season_colapse + if item.infoLabels['title']: + del item.infoLabels['title'] + del item.infoLabels['season'] + if item.contentEpisodeNumber: + del item.infoLabels['episode'] + if season_display == 0 and item.from_num_season_colapse: + season_display = item.from_num_season_colapse + + # Obtener la información actualizada de la Serie. TMDB es imprescindible para Videoteca + if not item.infoLabels['tmdb_id']: + tmdb.set_infoLabels(item, True) + + modo_ultima_temp_alt = modo_ultima_temp + if item.ow_force == "1": #Si hay un traspaso de canal o url, se actualiza todo + modo_ultima_temp_alt = False + + max_temp = 1 + if item.infoLabels['number_of_seasons']: + max_temp = item.infoLabels['number_of_seasons'] + y = [] + if modo_ultima_temp_alt and item.library_playcounts: #Averiguar cuantas temporadas hay en Videoteca + patron = 'season (\d+)' + matches = re.compile(patron, re.DOTALL).findall(str(item.library_playcounts)) + for x in matches: + y += [int(x)] + max_temp = max(y) + + # Descarga la página + data = '' #Inserto en num de página en la url + try: + data = re.sub(r"\n|\r|\t|\s{2}|(<!--.*?-->)| ", "", httptools.downloadpage(item.url, timeout=timeout).data) + data = unicode(data, "utf-8", errors="replace").encode("utf-8") + data = re.sub(r""", '"', data) + data = re.sub(r"<", '<', data) + data = re.sub(r">", '>', data) + except: #Algún error de proceso, salimos + pass + + if not data: + logger.error("ERROR 01: EPISODIOS: La Web no responde o la URL es erronea" + item.url) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 01: EPISODIOS:. La Web no responde o la URL es erronea. Si la Web está activa, reportar el error con el log')) + return itemlist + + #Buscamos los episodios + patron = '<tr><td><span class="Num">\d+<\/span><\/td><td class="MvTbImg B"><a href="([^"]+)" class="MvTbImg">(?:<span class="[^>]+>)?<img src="([^"]+)" alt="([^"]+)">(?:<\/span>)?<\/a><\/td><td class="MvTbTtl">[^>]+>(.*?)<\/a>' + matches = re.compile(patron, re.DOTALL).findall(data) + if not matches: #error + item = generictools.web_intervenida(item, data) #Verificamos que no haya sido clausurada + if item.intervencion: #Sí ha sido clausurada judicialmente + item, itemlist = generictools.post_tmdb_episodios(item, itemlist) #Llamamos al método para el pintado del error + return itemlist #Salimos + + logger.error("ERROR 02: EPISODIOS: Ha cambiado la estructura de la Web " + " / PATRON: " + patron + " / DATA: " + data) + itemlist.append(item.clone(action='', title=item.channel.capitalize() + ': ERROR 02: EPISODIOS: Ha cambiado la estructura de la Web. Reportar el error con el log')) + return itemlist #si no hay más datos, algo no funciona, pintamos lo que tenemos + + #logger.debug("PATRON: " + patron) + #logger.debug(matches) + #logger.debug(data) + + season = max_temp + #Comprobamos si realmente sabemos el num. máximo de temporadas + if item.library_playcounts or (item.infoLabels['number_of_seasons'] and item.tmdb_stat): + num_temporadas_flag = True + else: + num_temporadas_flag = False + + # Recorremos todos los episodios generando un Item local por cada uno en Itemlist + for scrapedurl, scrapedthumbnail, scrapedtitle, scrapedepi_name in matches: + item_local = item.clone() + item_local.action = "findvideos" + item_local.contentType = "episode" + item_local.extra = "episodios" + if item_local.library_playcounts: + del item_local.library_playcounts + if item_local.library_urls: + del item_local.library_urls + if item_local.path: + del item_local.path + if item_local.update_last: + del item_local.update_last + if item_local.update_next: + del item_local.update_next + if item_local.channel_host: + del item_local.channel_host + if item_local.active: + del item_local.active + if item_local.contentTitle: + del item_local.infoLabels['title'] + if item_local.season_colapse: + del item_local.season_colapse + + item_local.title = '' + item_local.context = "['buscar_trailer']" + item_local.url = scrapedurl + title = scrapedtitle + item_local.language = [] + + #Buscamos calidades del episodio + if 'hdtv' in scrapedtitle.lower() or 'hdtv' in scrapedurl: + item_local.quality = 'HDTV' + elif 'hd7' in scrapedtitle.lower() or 'hd7' in scrapedurl: + item_local.quality = 'HD720p' + elif 'hd1' in scrapedtitle.lower() or 'hd1' in scrapedurl: + item_local.quality = 'HD1080p' + + #Buscamos idiomas del episodio + lang = scrapedtitle.strip() + if ('vo' in lang.lower() or 'v.o' in lang.lower() or 'vo' in scrapedurl.lower() or 'v.o' in scrapedurl.lower()) and not 'VO' in item_local.language: + item_local.language += ['VO'] + elif ('vose' in lang.lower() or 'v.o.s.e' in lang.lower() or 'vose' in scrapedurl.lower() or 'v.o.s.e' in scrapedurl.lower()) and not 'VOSE' in item_local.language: + item_local.language += ['VOSE'] + elif ('latino' in lang.lower() or 'latino' in scrapedurl.lower()) and not 'LAT' in item_local.language: + item_local.language += ['LAT'] + + if not item_local.language: + item_local.language += ['CAST'] + + #Buscamos la Temporada y el Episodio + try: + item_local.contentEpisodeNumber = 0 + if 'miniserie' in title.lower(): + item_local.contentSeason = 1 + title = title.replace('miniserie', '').replace('MiniSerie', '') + elif 'completa' in title.lower(): + patron = '[t|T].*?(\d+) [c|C]ompleta' + if scrapertools.find_single_match(title, patron): + item_local.contentSeason = int(scrapertools.find_single_match(title, patron)) + if not item_local.contentSeason: + #Extraemos los episodios + patron = '(\d{1,2})[x|X](\d{1,2})' + item_local.contentSeason, item_local.contentEpisodeNumber = scrapertools.find_single_match(title, patron) + item_local.contentSeason = int(item_local.contentSeason) + item_local.contentEpisodeNumber = int(item_local.contentEpisodeNumber) + except: + logger.error('ERROR al extraer Temporada/Episodio: ' + title) + item_local.contentSeason = 1 + item_local.contentEpisodeNumber = 0 + + #Si son episodios múltiples, lo extraemos + patron1 = '\d+[x|X]\d{1,2}.?(?:y|Y|al|Al)?(?:\d+[x|X]\d{1,2})?.?(?:y|Y|al|Al)?.?\d+[x|X](\d{1,2})' + epi_rango = scrapertools.find_single_match(title, patron1) + if epi_rango: + item_local.infoLabels['episodio_titulo'] = 'al %s %s' % (epi_rango, scrapedepi_name) + item_local.title = '%sx%s al %s -' % (str(item_local.contentSeason), str(item_local.contentEpisodeNumber).zfill(2), str(epi_rango).zfill(2)) + else: + item_local.title = '%sx%s -' % (str(item_local.contentSeason), str(item_local.contentEpisodeNumber).zfill(2)) + item.infoLabels['episodio_titulo'] = '%s' %scrapedepi_name + + if modo_ultima_temp_alt and item.library_playcounts: #Si solo se actualiza la última temporada de Videoteca + if item_local.contentSeason < max_temp: + continue #salta al siguiente episodio + + #Mostramos solo la temporada requerida + if season_display > 0: + if item_local.contentSeason > season_display: + break + elif item_local.contentSeason < season_display: + continue + + itemlist.append(item_local.clone()) + + #logger.debug(item_local) + + if len(itemlist) > 1: + itemlist = sorted(itemlist, key=lambda it: (int(it.contentSeason), int(it.contentEpisodeNumber))) #clasificamos + + if item.season_colapse and not item.add_videolibrary: #Si viene de listado, mostramos solo Temporadas + item, itemlist = generictools.post_tmdb_seasons(item, itemlist) + + if not item.season_colapse: #Si no es pantalla de Temporadas, pintamos todo + # Pasada por TMDB y clasificación de lista por temporada y episodio + tmdb.set_infoLabels(itemlist, True) + + #Llamamos al método para el maquillaje de los títulos obtenidos desde TMDB + item, itemlist = generictools.post_tmdb_episodios(item, itemlist) + + #logger.debug(item) + + return itemlist + + +def actualizar_titulos(item): + logger.info() + + item = generictools.update_title(item) #Llamamos al método que actualiza el título con tmdb.find_and_set_infoLabels + + #Volvemos a la siguiente acción en el canal + return item + + +def search(item, texto): + logger.info() + #texto = texto.replace(" ", "+") + + item.url = item.url + texto + + if texto != '': + return listado(item) + + try: + item.url = item.url + texto + + if texto != '': + return listado(item) + except: + import sys + for line in sys.exc_info(): + logger.error("{0}".format(line)) + return [] + + +def newest(categoria): + logger.info() + itemlist = [] + item = Item() + + try: + if categoria == 'peliculas': + item.url = host + "estrenos-de-cine-2" + item.extra = "peliculas" + item.channel = channel + item.category_new= 'newest' + + itemlist = listado(item) + if ">> Página siguiente" in itemlist[-1].title: + itemlist.pop() + + # Se captura la excepción, para no interrumpir al canal novedades si un canal falla + except: + import sys + for line in sys.exc_info(): + logger.error("{0}".format(line)) + return [] + + return itemlist diff --git a/plugin.video.alfa/resources/media/channels/thumb/zonatorrent.png b/plugin.video.alfa/resources/media/channels/thumb/zonatorrent.png new file mode 100644 index 0000000000000000000000000000000000000000..13fe418f188545625aefa84895b7aadc0df31b88 GIT binary patch literal 36220 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 yqhhd&(9KY#lLR86Btz$U;(;9bw5V8?PZ#4-X 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 aVEGU0==~|RnzggwFu>N@(W=Ul5dS}Offf(| literal 0 HcmV?d00001 From 2a4fdcd095c8099a0ce2088caecf1fcb71575670 Mon Sep 17 00:00:00 2001 From: Kingbox <37674310+lopezvg@users.noreply.github.com> Date: Wed, 12 Sep 2018 21:51:20 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Kodi=2018:=20correcci=C3=B3n=20de=20compati?= =?UTF-8?q?bilidad=20con=20clientes=20Torrent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evita cuelgues y cancelaciones cuando se reproducen vídeos desde una pantalla convencional (no emergente) --- .../platformcode/platformtools.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/plugin.video.alfa/platformcode/platformtools.py b/plugin.video.alfa/platformcode/platformtools.py index f0713eaa..0f7503d8 100644 --- a/plugin.video.alfa/platformcode/platformtools.py +++ b/plugin.video.alfa/platformcode/platformtools.py @@ -1044,6 +1044,8 @@ def torrent_client_installed(show_tuple=False): def play_torrent(item, xlistitem, mediaurl): logger.info() + import time + # Opciones disponibles para Reproducir torrents torrent_options = list() torrent_options.append(["Cliente interno (necesario libtorrent)"]) @@ -1066,28 +1068,32 @@ def play_torrent(item, xlistitem, mediaurl): # Plugins externos if seleccion > 1: + + #### Compatibilidad con Kodi 18: evita cuelgues/cancelaciones cuando el .torrent se lanza desde pantalla convencional + if xbmc.getCondVisibility('Window.IsMedia'): + xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xlistitem) #Preparamos el entorno para evutar error Kod1 18 + time.sleep(1) #Dejamos que se ejecute + mediaurl = urllib.quote_plus(item.url) if ("quasar" in torrent_options[seleccion][1] or "elementum" in torrent_options[seleccion][1]) and item.infoLabels['tmdb_id']: #Llamada con más parámetros para completar el título if item.contentType == 'episode' and "elementum" not in torrent_options[seleccion][1]: mediaurl += "&episode=%s&library=&season=%s&show=%s&tmdb=%s&type=episode" % (item.infoLabels['episode'], item.infoLabels['season'], item.infoLabels['tmdb_id'], item.infoLabels['tmdb_id']) elif item.contentType == 'movie': mediaurl += "&library=&tmdb=%s&type=movie" % (item.infoLabels['tmdb_id']) - xbmc.executebuiltin("PlayMedia(" + torrent_options[seleccion][1] % mediaurl + ")") - if "quasar" in torrent_options[seleccion][1] or "elementum" in torrent_options[seleccion][1]: #Seleccionamos que clientes torrent soportamos - if item.strm_path: #Sólo si es de Videoteca - import time - time_limit = time.time() + 150 #Marcamos el timepo máx. de buffering - while not is_playing() and time.time() < time_limit: #Esperamos mientra buffera - time.sleep(5) #Repetimos cada intervalo - #logger.debug(str(time_limit)) - - if is_playing(): #Ha terminado de bufferar o ha cancelado - from platformcode import xbmc_videolibrary - xbmc_videolibrary.mark_auto_as_watched(item) #Marcamos como visto al terminar - #logger.debug("Llamado el marcado") - #else: - #logger.debug("Video cancelado o timeout") + xbmc.executebuiltin("PlayMedia(" + torrent_options[seleccion][1] % mediaurl + ")") + + #Seleccionamos que clientes torrent soportamos para el marcado de vídeos vistos + if "quasar" in torrent_options[seleccion][1] or "elementum" in torrent_options[seleccion][1]: + time_limit = time.time() + 150 #Marcamos el timepo máx. de buffering + while not is_playing() and time.time() < time_limit: #Esperamos mientra buffera + time.sleep(5) #Repetimos cada intervalo + #logger.debug(str(time_limit)) + + if item.strm_path and is_playing(): #Sólo si es de Videoteca + from platformcode import xbmc_videolibrary + xbmc_videolibrary.mark_auto_as_watched(item) #Marcamos como visto al terminar + #logger.debug("Llamado el marcado") if seleccion == 1: from platformcode import mct