# -*- coding: utf-8 -*- import base64 import re import struct import zlib from hashlib import sha1 from core import config from core import filetools from core import httptools from core import logger from core import scrapertools GLOBAL_HEADER = {'User-Agent': 'Mozilla/5.0', 'Accept-Language': '*'} proxy = "http://anonymouse.org/cgi-bin/anon-www.cgi/" def test_video_exists(page_url): logger.info("(page_url='%s')" % page_url) premium = config.get_setting("premium", server="crunchyroll") if premium: return login(page_url) data = httptools.downloadpage(page_url, headers=GLOBAL_HEADER, replace_headers=True).data if "Este es un clip de muestra" in data: disp = scrapertools.find_single_match(data, '.*?\s*(.*?)') disp = disp.strip() if disp: disp = "Disponible gratuitamente: %s" % disp return False, "[Crunchyroll] Error, se necesita cuenta premium. %s" % disp return True, "" def get_video_url(page_url, premium=False, user="", password="", video_password=""): logger.info("url=" + page_url) video_urls = [] if "crunchyroll.com" in page_url: media_id = page_url.rsplit("-", 1)[1] else: media_id = scrapertools.find_single_match(page_url, 'media_id=(\d+)') url = "http://www.crunchyroll.com/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=%s" \ "&video_format=0&video_quality=0&auto_play=0&aff=af-12299-plwa" % media_id post = "current_page=%s" % page_url data = httptools.downloadpage(url, post, headers=GLOBAL_HEADER, replace_headers=True).data if "Media not available" in data or "flash_block.png" in data: data = httptools.downloadpage(proxy + url, post, headers=GLOBAL_HEADER, replace_headers=True, cookies=False).data media_url = scrapertools.find_single_match(data, '(.*?)').replace("&", "&") if not media_url: return video_urls elif not media_url.startswith("http"): rtmp = scrapertools.find_single_match(data, '(.*?)').replace("&", "&") media_url = rtmp + " playpath=%s" % media_url filename = "RTMP" else: filename = scrapertools.get_filename_from_url(media_url)[-4:] quality = scrapertools.find_single_match(data, '(.*?)') try: idiomas = ['Español \(España\)', 'Español\]', 'English', 'Italiano', 'Français', 'Português', 'Deutsch'] index_sub = int(config.get_setting("sub", server="crunchyroll")) idioma_sub = idiomas[index_sub] link_sub = scrapertools.find_single_match(data, "link='([^']+)' title='\[%s" % idioma_sub) if not link_sub and index_sub == 0: link_sub = scrapertools.find_single_match(data, "link='([^']+)' title='\[Español\]") elif not link_sub and index_sub == 1: link_sub = scrapertools.find_single_match(data, "link='([^']+)' title='\[Español \(España\)") if not link_sub: link_sub = scrapertools.find_single_match(data, "link='([^']+)' title='\[English") data_sub = httptools.downloadpage(link_sub.replace("&", "&"), headers=GLOBAL_HEADER, replace_headers=True).data id_sub = scrapertools.find_single_match(data_sub, "subtitle id='([^']+)'") iv = scrapertools.find_single_match(data_sub, '(.*?)') data_sub = scrapertools.find_single_match(data_sub, '(.*?)') file_sub = decrypt_subs(iv, data_sub, id_sub) except: import traceback logger.error(traceback.format_exc()) file_sub = "" video_urls.append(["%s %sp [crunchyroll]" % (filename, quality), media_url, 0, file_sub]) for video_url in video_urls: logger.info("%s - %s" % (video_url[0], video_url[1])) return video_urls def login(page_url): login_page = "https://www.crunchyroll.com/login" user = config.get_setting("user", server="crunchyroll") password = config.get_setting("password", server="crunchyroll") data = httptools.downloadpage(login_page, headers=GLOBAL_HEADER, replace_headers=True).data if not "Redirecting" in data: token = scrapertools.find_single_match(data, 'name="login_form\[_token\]" value="([^"]+)"') redirect_url = scrapertools.find_single_match(data, 'name="login_form\[redirect_url\]" value="([^"]+)"') post = "login_form%5Bname%5D=" + user + "&login_form%5Bpassword%5D=" + password + \ "&login_form%5Bredirect_url%5D=" + redirect_url + "&login_form%5B_token%5D=" + token data = httptools.downloadpage(login_page, post, headers=GLOBAL_HEADER, replace_headers=True).data if "<title>Redirecting" in data: return True, "" else: if "Usuario %s no disponible" % user in data: return False, "El usuario de crunchyroll no existe. Corrígelo o desactiva la opción premium para ver enlaces free" elif '<li class="error">Captcha' in data: return False, "Es necesario resolver un captcha. Loguéate desde un navegador y vuelve a intentarlo" else: return False, "Error en la contraseña de crunchyroll. Corrígelo o desactiva la opción premium para ver enlaces free" return True, "" def decrypt_subs(iv, data, id): import jscrypto data = base64.b64decode(data.encode('utf-8')) iv = base64.b64decode(iv.encode('utf-8')) id = int(id) def obfuscate_key_aux(count, modulo, start): output = list(start) for _ in range(count): output.append(output[-1] + output[-2]) # cut off start values output = output[2:] output = list(map(lambda x: x % modulo + 33, output)) return output def obfuscate_key(key): from math import pow, sqrt, floor num1 = int(floor(pow(2, 25) * sqrt(6.9))) num2 = (num1 ^ key) << 5 num3 = key ^ num1 num4 = num3 ^ (num3 >> 3) ^ num2 prefix = obfuscate_key_aux(20, 97, (1, 2)) prefix = struct.pack('B' * len(prefix), *prefix) shaHash = sha1(prefix + str(num4).encode('ascii')).digest() decshaHash = [] for char in shaHash: decshaHash.append(ord(char)) # Extend 160 Bit hash to 256 Bit return decshaHash + [0] * 12 key = obfuscate_key(id) key = struct.pack('B' * len(key), *key) decryptor = jscrypto.new(key, 2, iv) decrypted_data = decryptor.decrypt(data) data = zlib.decompress(decrypted_data) import xml.etree.ElementTree as ET raiz = ET.fromstring(data) ass_sub = convert_to_ass(raiz) file_sub = filetools.join(config.get_data_path(), 'crunchyroll_sub.ass') filetools.write(file_sub, ass_sub) return file_sub def convert_to_ass(raiz): output = '' def ass_bool(strvalue): assvalue = '0' if strvalue == '1': assvalue = '-1' return assvalue output = '[Script Info]\n' output += 'Title: %s\n' % raiz.attrib['title'] output += 'ScriptType: v4.00+\n' output += 'WrapStyle: %s\n' % raiz.attrib['wrap_style'] output += 'PlayResX: %s\n' % raiz.attrib['play_res_x'] output += 'PlayResY: %s\n' % raiz.attrib['play_res_y'] output += """ScaledBorderAndShadow: yes [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding """ for style in raiz.findall('./styles/style'): output += 'Style: ' + style.attrib['name'] output += ',' + style.attrib['font_name'] output += ',' + style.attrib['font_size'] output += ',' + style.attrib['primary_colour'] output += ',' + style.attrib['secondary_colour'] output += ',' + style.attrib['outline_colour'] output += ',' + style.attrib['back_colour'] output += ',' + ass_bool(style.attrib['bold']) output += ',' + ass_bool(style.attrib['italic']) output += ',' + ass_bool(style.attrib['underline']) output += ',' + ass_bool(style.attrib['strikeout']) output += ',' + style.attrib['scale_x'] output += ',' + style.attrib['scale_y'] output += ',' + style.attrib['spacing'] output += ',' + style.attrib['angle'] output += ',' + style.attrib['border_style'] output += ',' + style.attrib['outline'] output += ',' + style.attrib['shadow'] output += ',' + style.attrib['alignment'] output += ',' + style.attrib['margin_l'] output += ',' + style.attrib['margin_r'] output += ',' + style.attrib['margin_v'] output += ',' + style.attrib['encoding'] output += '\n' output += """ [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text """ for event in raiz.findall('./events/event'): output += 'Dialogue: 0' output += ',' + event.attrib['start'] output += ',' + event.attrib['end'] output += ',' + event.attrib['style'] output += ',' + event.attrib['name'] output += ',' + event.attrib['margin_l'] output += ',' + event.attrib['margin_r'] output += ',' + event.attrib['margin_v'] output += ',' + event.attrib['effect'] output += ',' + event.attrib['text'] output += '\n' return output.encode('utf-8')