commit 7b54ba45a17548f2fab08895c604ac6d14eadfd6 Author: Pavel Suha Date: Fri Apr 25 16:31:48 2025 +0200 test diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..f1f1208 --- /dev/null +++ b/addon.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + Kodi CZ/SK plugins common python modules mainly for resolving stream URLs 2025 + Common python modules pack that is used by kodi cz/sk plugins + GNU GPL + all + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..b8e90ff --- /dev/null +++ b/changelog.txt @@ -0,0 +1,208 @@ +[B]1.6.62:[/B] +- fix dailymotion auto quality stream +[B]1.6.61:[/B] +- fix youtube signature +[B]1.6.60:[/B] +- fix cloudflare scrape protection +[B]1.6.59:[/B] +- fix youtube signature +[B]1.6.58:[/B] +- preserve compatibility with older kodi versions +[B]1.6.57:[/B] +- youtube: age content +[B]1.6.56:[/B] +- fix youtube signature +- recognize youtu.be scheme +[B]1.6.55:[/B] +- duration is now recognized as infolabels attribute +[B]1.6.54:[/B] +- add dailymotion resolver +[B]1.6.52:[/B] +- fix streamujtv resolver +[B]1.6.48:[/B] +- fixed video download code which was failing when there were subtitles to +download +[B]1.6.45:[/B] +- support for filtering by language (needs to be passed by addon as 'lang' setting) +[B]1.6.40:[/B] +- add on_init hook to ContentProvider +- fix hqq & nahnoji parsers, added sk translations (jose1711) +[B]1.6.39:[/B] +- Fixed Exashare, Openload and Youwatch resolvers +- New resolver for Videram +[B]1.6.37:[/B] +- (for all plugins) search items can be now edited (Search Similar) +[B]1.6.36:[/B] +- fix streamujtv resolver to play videos with missing lang +[B]1.6.34:[/B] +- fix usage tracker issue on windows +[B]1.6.30:[/B] +- fix froto compatibility issue +[B]1.6.29:[/B] +- providers have now injected 'lang' - current language of xbmc/kodi user +[B]1.6.24:[/B] +- resolvers: support for displaying additional info (audio track language, etc) +- streamujtv: return all possible streams (incl. audio track combinations) #223 +[B]1.6.16:[/B] +- support for disabled subtitles by plugin setting +- #217 - incorrect item title on some skins +[B]1.6.15:[/B] +- fix streamujtv resolver to correctly handle subtitles +- another possible fix of showing captcha on windows +[B]1.6.14:[/B] +- fixed playedto resolver +[B]1.6.12:[/B] +- fixed streamujtv resolver +- better support for dynamic context menu items +[B]1.6.11:[/B] +- fixed putlocker and flashx resolvers (ibv) +[B]1.6.10:[/B] +- attempt to fix showing captcha image on windows (when there are special +chars in profile path) +[B]1.6.9:[/B] +- attempt to fix showing captcha image on windows (when there are special +chars in xbmc profile path) +- added anyfiles resolver +[B]1.6.8:[/B] +- added streamujtv resolver +[B]1.6.7:[/B] +- add ContentProvider.findstreams - wrapper for resolver.findstreams +[B]1.6.6:[/B] +- added koukejse resolver +- improved @cached decorator +- captcha image is now downloaded to file before showing up captcha dialog +[B]1.6.5:[/B] +- fixed youtube resolver (mx3L) +- download is now more robust #174 +[B]1.6.4:[/B] +- fixed youtube resolver (by mx3L) +- added concurrent function (by mx3L) +[B]1.6.2:[/B] +- updated zkouknito resolver, added kset, publicvideohost resolvers (by mx3L) +[B]1.6.1:[/B] +- fixed downloading of files with special chars +- support for @cached decorator for provider methods +[B]1.5.6:[/B] +- fixed koukni - works back with mp4 +[B]1.5.5:[/B] +- contentprovider fixes (does not fail when item title starts with $, supports +infolabels +[B]1.5.4:[/B] +- fixed koukni resolver to properly resolve rtmp +- removed providers - moved to plugins +[B]1.5.2:[/B] +- fixed youtube resolver +[B]1.5.1:[/B] +- when downloaded file does not have extension, it is autoappened (.mp4) +[B]1.5.0:[/B] +- added resolvers flashx, videomail (ivo, mx3L) +- resolvers can now return headers (if required for playing/downloading) +- ContentProviders can now raise ResolveException +- ulozto provider raises ResolveException when ulozto server is overloaded +[B]1.4.6:[/B] +- fixed youtube resolver +- subtitles now work in munkvideo resolver +[B]1.4.5:[/B] +- ulozto provider can now search for all file types +[B]1.4.4:[/B] +- added munkvideo resolver +[B]1.4.3:[/B] +- fixed eserial resolver to load subs correctly #136 +[B]1.3.12:[/B] +- fixed eserial resolver to support subtitles and work for more streams +- fixed quality filtering: we always return exactly 1 stream unless user wants to select himself +[B]1.3.11:[/B] +- sledujuserialy resolver resolves mp4 videos as well +- fixed #127 - searching does not work on frodo beta1 +- added eserialresolver +- resolvers have now priority (higher=resolver is asked fist) +[B]1.3.10:[/B] +- fixed #127 - searching does not work on frodo beta1 +- fixed ulozto to show search results again +- fixed sledujuserialy resolver (to resolve mp4 videos) +[B]1.3.9:[/B] +- got rid of old resolver API +- fixed ulozto provider to be able to retrieve non-VIP files again +- hellspy provider has now configurable server url and supports http://stiahnito.sk (slovak mirror) +[B]1.3.8:[/B] +- resolver.findstreams - prevent multiple resolving same url +- added sledujuserialy resolver +[B]1.3.7:[/B] +- fixed vkontakte resolver +[B]1.3.6:[/B] +- fixed vkontakte resolver +[B]1.3.4:[/B] +- fixed youtube resolver +- added streamcloud & gosuparg resolvers (Ivo) +[B]1.3.3:[/B] +- ulozto and hellspy fixed: login() failed when called repeatedly +[B]1.3.2:[/B] +- fixed bezvadata content provider +- added support for delayed file download (bezvadata uses it) +[B]1.3.1:[/B] +- vkontakte resolver now supports 720p videos +- added videonet resolver (thanks to Ivo Brhel), rutube resolver +[B]1.3.0:[/B] +- introduced ContentProvider API +- util.py splitted to util.py and xbmcutil.py (so we're now able to run tests in non-xbmc environment) +[B]1.2.8:[/B] +- added nahnoji and moevideo resolvers by Ivo Brhel +- fixed putlocker (by Ivo), videozer & videobb resolvers +[B]1.2.7:[/B] +- youtube resolver fixes +- all resolvers now return non-empty 'subs' key within resolve2 or findstreams +[B]1.2.6:[/B] +- resolvers can now retrieve subtitles +- added koukni.cz resovler (supports subtitles) +[B]1.2.2:[/B] +- bugfixes +- search.py now supports multiple search items within one addon +[B]1.2.0:[/B] +- added method to prelace czech diaktitic chars +- fixed divByZero when downloading, made notifications more robust +[B]1.1.7:[/B] +- new API for stream resolving - quality aware API +- youtube and vkontakte now implement quality aware API, others have dummy +impl only +[B]1.1.6:[/B] +- another small fix of youtuberesolver +- fixed videobb and videozer resovlers, thanks to Lynx187 +[B]1.1.5:[/B] +- added higher level searching API +- fixed youtuberesolver +[B]1.1.4:[/B] +- added dependency on script.usage.tracker +[B]1.1.3:[/B] +- download notification: added EST +[B]1.1.2:[/B] +- saved searches can be removed now +[B]1.1.0:[/B] +- added new API for retrieving common icons from github +- fixed downloading to report dialog on finish (when not playing) +[B]1.0.8:[/B] +- added new API for downloading, browsing local folders +- fixed string encoding issues +- fixed zideo resolver +[B]1.0.7:[/B] +- added servertip.cz resolver +- fixed youtube +- added api for search history +- added simplejson dependency +[B]1.0.6:[/B] +- added zideo.nl +- stagevu - do not fail if movie was removed +- videobb - proper resolving +[B]1.0.5:[/B] +- added novamov, youtube, trivial - special does nothing +- fixed megavideo and videozer +[B]1.0.4:[/B] +- added megavideo, videozer resolvers +- fixed zkouknito +[B]1.0.3:[/B] +- added other resolvers +- added simple debug logging +- all resovlers renamed not to collide with other 3rd party on Camelot +[B]1.0.1:[/B] + - added putlocker resolver +[B]1.0.0:[/B] +Initial version diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..dd30da9 --- /dev/null +++ b/install.sh @@ -0,0 +1,7 @@ +#/bin/sh +# do NOT use this script from Kodi addons directory, it is intented for development only +DESTDIR=~/.kodi/addons/script.module.stream.resolver + +rm -rf ${DESTDIR} +mkdir -p ${DESTDIR} +cp -a * ${DESTDIR} diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/__pycache__/aadecode.cpython-38.opt-1.pyc b/lib/__pycache__/aadecode.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e82e104 Binary files /dev/null and b/lib/__pycache__/aadecode.cpython-38.opt-1.pyc differ diff --git a/lib/__pycache__/cloudflare.cpython-38.opt-1.pyc b/lib/__pycache__/cloudflare.cpython-38.opt-1.pyc new file mode 100644 index 0000000..dbf9106 Binary files /dev/null and b/lib/__pycache__/cloudflare.cpython-38.opt-1.pyc differ diff --git a/lib/__pycache__/resolver.cpython-38.opt-1.pyc b/lib/__pycache__/resolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f9334f9 Binary files /dev/null and b/lib/__pycache__/resolver.cpython-38.opt-1.pyc differ diff --git a/lib/__pycache__/search.cpython-38.opt-1.pyc b/lib/__pycache__/search.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f201d3c Binary files /dev/null and b/lib/__pycache__/search.cpython-38.opt-1.pyc differ diff --git a/lib/__pycache__/util.cpython-38.opt-1.pyc b/lib/__pycache__/util.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9383f77 Binary files /dev/null and b/lib/__pycache__/util.cpython-38.opt-1.pyc differ diff --git a/lib/__pycache__/xbmcutil.cpython-38.opt-1.pyc b/lib/__pycache__/xbmcutil.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bdcf05a Binary files /dev/null and b/lib/__pycache__/xbmcutil.cpython-38.opt-1.pyc differ diff --git a/lib/aadecode.py b/lib/aadecode.py new file mode 100644 index 0000000..92135a2 --- /dev/null +++ b/lib/aadecode.py @@ -0,0 +1,209 @@ +#-*- coding: utf-8 -*- +# +# author : Djeman +# Updated by Shani-08 (https://github.com/Shani-08/ShaniXBMCWork2) + +import re + +class AADecoder(object): + def __init__(self, aa_encoded_data): + self.encoded_str = aa_encoded_data.replace('/*´∇`*/','') + + self.b = ["(c^_^o)", "(゚Θ゚)", "((o^_^o) - (゚Θ゚))", "(o^_^o)", + "(゚ー゚)", "((゚ー゚) + (゚Θ゚))", "((o^_^o) +(o^_^o))", "((゚ー゚) + (o^_^o))", + "((゚ー゚) + (゚ー゚))", "((゚ー゚) + (゚ー゚) + (゚Θ゚))", "(゚Д゚) .゚ω゚ノ", "(゚Д゚) .゚Θ゚ノ", + "(゚Д゚) ['c']", "(゚Д゚) .゚ー゚ノ", "(゚Д゚) .゚Д゚ノ", "(゚Д゚) [゚Θ゚]"] + + def is_aaencoded(self): + idx = self.encoded_str.find("゚ω゚ノ= /`m´)ノ ~┻━┻ //*´∇`*/ ['_']; o=(゚ー゚) =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); ") + if idx == -1: + return False + + if self.encoded_str.find("(゚Д゚)[゚o゚]) (゚Θ゚)) ('_');", idx) == -1: + return False + + return True + + def base_repr(self, number, base=2, padding=0): + digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + if base > len(digits): + base = len(digits) + + num = abs(number) + res = [] + while num: + res.append(digits[num % base]) + num //= base + if padding: + res.append('0' * padding) + if number < 0: + res.append('-') + return ''.join(reversed(res or '0')) + + def decode_char(self, enc_char, radix): + end_char = "+ " + str_char = "" + while enc_char != '': + found = False + + if not found: + for i in range(len(self.b)): + enc_char=enc_char.replace(self.b[i], str(i)) + + startpos=0 + findClose=True + balance=1 + result=[] + if enc_char.startswith('('): + l=0 + + for t in enc_char[1:]: + l+=1 + if findClose and t==')': + balance-=1; + if balance==0: + result+=[enc_char[startpos:l+1]] + findClose=False + continue + elif not findClose and t=='(': + startpos=l + findClose=True + balance=1 + continue + elif t=='(': + balance+=1 + + + if result is None or len(result)==0: + return "" + else: + + for r in result: + value = self.decode_digit(r, radix) + if value == "": + return "" + else: + str_char += value + + return str_char + + enc_char = enc_char[len(end_char):] + + return str_char + + + + def decode_digit(self, enc_int, radix): + + rr = '(\(.+?\)\))\+' + rerr=enc_int.split('))+') + v = '' + + #new mode + + for c in rerr: + + if len(c)>0: + if c.strip().endswith('+'): + c=c.strip()[:-1] + + startbrackets=len(c)-len(c.replace('(','')) + endbrackets=len(c)-len(c.replace(')','')) + + if startbrackets>endbrackets: + c+=')'*startbrackets-endbrackets + + c = c.replace('!+[]','1') + c = c.replace('-~','1+') + c = c.replace('[]','0') + + v+=str(eval(c)) + + return v + + mode = 0 + value = 0 + + while enc_int != '': + found = False + for i in range(len(self.b)): + if enc_int.find(self.b[i]) == 0: + if mode == 0: + value += i + else: + value -= i + enc_int = enc_int[len(self.b[i]):] + found = True + break + + if not found: + return "" + + enc_int = re.sub('^\s+|\s+$', '', enc_int) + if enc_int.find("+") == 0: + mode = 0 + else: + mode = 1 + + enc_int = enc_int[1:] + enc_int = re.sub('^\s+|\s+$', '', enc_int) + + return self.base_repr(value, radix) + + def decode(self): + + self.encoded_str = re.sub('^\s+|\s+$', '', self.encoded_str) + + # get data + pattern = (r"\(゚Д゚\)\[゚o゚\]\+ (.+?)\(゚Д゚\)\[゚o゚\]\)") + result = re.search(pattern, self.encoded_str, re.DOTALL) + if result is None: + print("AADecoder: data not found") + return False + + data = result.group(1) + + # hex decode string + begin_char = "(゚Д゚)[゚ε゚]+" + alt_char = "(o゚ー゚o)+ " + + out = '' + + while data != '': + # Check new char + if data.find(begin_char) != 0: + print("AADecoder: data not found") + return False + + data = data[len(begin_char):] + + # Find encoded char + enc_char = "" + if data.find(begin_char) == -1: + enc_char = data + data = "" + else: + enc_char = data[:data.find(begin_char)] + data = data[len(enc_char):] + + + radix = 8 + # Detect radix 16 for utf8 char + if enc_char.find(alt_char) == 0: + enc_char = enc_char[len(alt_char):] + radix = 16 + + str_char = self.decode_char(enc_char, radix) + + if str_char == "": + print("no match : ") + print(data + "\nout = " + out + "\n") + return False + + out += chr(int(str_char, radix)) + + if out == "": + print("no match : " + data) + return False + + return out diff --git a/lib/cloudflare.py b/lib/cloudflare.py new file mode 100644 index 0000000..855d90d --- /dev/null +++ b/lib/cloudflare.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 tknorris (Derived from Mikey1234's & Lambda's) +# +# This Program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This Program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with XBMC; see the file COPYING. If not, write to +# the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# http://www.gnu.org/copyleft/gpl.html +# +# This code is a derivative of the YouTube plugin for XBMC and associated works +# released under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 + +import re +import urllib.request, urllib.parse, urllib.error +import urllib.parse +import util +import xbmc + +MAX_TRIES = 3 +COMPONENT = __name__ + + +class NoRedirection(urllib.request.HTTPErrorProcessor): + + def http_response(self, request, response): + util.info('[CF] Stopping Redirect') + return response + + https_response = http_response + +def solve_equation(equation): + try: + offset = (1 if equation[0] == '+' else 0) + ev = equation.replace('!+[]', '1').replace('!![]', + '1').replace('[]', '0').replace('(', 'str(')[offset:] + ev = re.sub(r'^str', 'float', re.sub(r'\/(.)str', r'/\1float', ev)) + # util.debug('[CF] eval: {0}'.format(ev)) + return float(eval(ev)) + except: + pass + + +def solve(url, cj, user_agent=None, wait=True): + if user_agent is None: + user_agent = util.UA + headers = {'User-Agent': user_agent, 'Referer': url} + if cj is not None: + try: + cj.load(ignore_discard=True) + except: + pass + opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) + urllib.request.install_opener(opener) + + scheme = urllib.parse.urlparse(url).scheme + domain = urllib.parse.urlparse(url).hostname + request = urllib.request.Request(url) + for key in headers: + request.add_header(key, headers[key]) + try: + response = urllib.request.urlopen(request) + html = response.read() + except urllib.error.HTTPError as e: + html = e.read() + + tries = 0 + while tries < MAX_TRIES: + solver_pattern = \ + 'var (?:s,t,o,p,b,r,e,a,k,i,n,g|t,r,a),f,\s*([^=]+)' + solver_pattern += \ + '={"([^"]+)":([^}]+)};.+challenge-form\'\);' + vc_pattern = \ + 'input type="hidden" name="jschl_vc" value="([^"]+)' + pass_pattern = 'input type="hidden" name="pass" value="([^"]+)' + s_pattern = 'input type="hidden" name="s" value="([^"]+)' + init_match = re.search(solver_pattern, html, re.DOTALL) + vc_match = re.search(vc_pattern, html) + pass_match = re.search(pass_pattern, html) + s_match = re.search(s_pattern, html) + + if not init_match or not vc_match or not pass_match or not s_match: + msg = \ + "[CF] Couldn't find attribute: init: |%s| vc: |%s| pass: |%s| No cloudflare check?" + util.info(msg % (init_match, vc_match, pass_match)) + return False + + (init_dict, init_var, init_equation) = \ + init_match.groups() + vc = vc_match.group(1) + password = pass_match.group(1) + s = s_match.group(1) + + equations = re.compile(r"challenge-form\'\);\s*(.*)a.v").findall(html)[0] + # util.info("[CF] VC is: %s" % (vc)) + varname = (init_dict, init_var) + # util.info('[CF] init: [{0}]'.format((init_equation.rstrip()))) + result = float(solve_equation(init_equation.rstrip())) + util.info('[CF] Initial value: [ {0} ] Result: [ {1} ]'.format(init_equation, + result)) + + for equation in equations.split(';'): + equation = equation.rstrip() + if len(equation) > len('.'.join(varname)): + # util.debug('[CF] varname {0} line {1}'.format('.'.join(varname), equation)) + if equation[:len('.'.join(varname))] != '.'.join(varname): + util.info('[CF] Equation does not start with varname |%s|' + % equation) + else: + equation = equation[len('.'.join(varname)):] + + expression = equation[2:] + operator = equation[0] + if operator not in ['+', '-', '*', '/']: + util.info('[CF] Unknown operator: |%s|' % equation) + continue + + result = float(str(eval(str(result) + operator + str(solve_equation( + expression))))) + #util.info('[CF] intermediate: %s = %s' % (equation, result)) + + #util.debug('[CF] POCET: {0} {1}'.format(result, len(domain))) + result = '{0:.10f}'.format(eval('float({0} + {1})'.format(result, len(domain)))) + util.info('[CF] Final Result: |%s|' % result) + + if wait: + util.info('[CF] Sleeping for 5 Seconds') + xbmc.sleep(5000) + + url = \ + '%s://%s/cdn-cgi/l/chk_jschl?s=%s&jschl_vc=%s&pass=%s&jschl_answer=%s' \ + % (scheme, domain, urllib.parse.quote(s), urllib.parse.quote(vc), urllib.parse.quote(password), urllib.parse.quote(result)) + # util.info('[CF] url: %s' % url) + # util.debug('[CF] headers: {0}'.format(headers)) + request = urllib.request.Request(url) + for key in headers: + request.add_header(key, headers[key]) + + try: + opener = urllib.request.build_opener(NoRedirection) + urllib.request.install_opener(opener) + response = urllib.request.urlopen(request) + # util.info('[CF] code: {}'.format(response.getcode())) + while response.getcode() in [301, 302, 303, 307]: + if cj is not None: + cj.extract_cookies(response, request) + + redir_url = response.info().getheader('location') + if not redir_url.startswith('http'): + base_url = '%s://%s' % (scheme, domain) + redir_url = urllib.parse.urljoin(base_url, redir_url) + + request = urllib.request.Request(redir_url) + for key in headers: + request.add_header(key, headers[key]) + if cj is not None: + cj.add_cookie_header(request) + + response = urllib.request.urlopen(request) + final = response.read() + if 'cf-browser-verification' in final: + util.info('[CF] Failure: html: %s url: %s' % (html, url)) + tries += 1 + html = final + else: + break + except urllib.error.HTTPError as e: + util.info('[CF] HTTP Error: %s on url: %s' % (e.code, + url)) + return False + except urllib.error.URLError as e: + util.info('[CF] URLError Error: %s on url: %s' % (e, + url)) + return False + + if cj is not None: + util.cache_cookies() + + return final diff --git a/lib/contentprovider/__pycache__/provider.cpython-38.opt-1.pyc b/lib/contentprovider/__pycache__/provider.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a2599ca Binary files /dev/null and b/lib/contentprovider/__pycache__/provider.cpython-38.opt-1.pyc differ diff --git a/lib/contentprovider/__pycache__/xbmcprovider.cpython-38.opt-1.pyc b/lib/contentprovider/__pycache__/xbmcprovider.cpython-38.opt-1.pyc new file mode 100644 index 0000000..11d95fb Binary files /dev/null and b/lib/contentprovider/__pycache__/xbmcprovider.cpython-38.opt-1.pyc differ diff --git a/lib/contentprovider/provider.py b/lib/contentprovider/provider.py new file mode 100644 index 0000000..8536a39 --- /dev/null +++ b/lib/contentprovider/provider.py @@ -0,0 +1,230 @@ +# * Copyright (C) 2012 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ +from collections import defaultdict + +import util +import resolver + + +try: + import StorageServer +except ImportError: + print('Using dummy storage server') + import storageserverdummy as StorageServer + + +class ResolveException(Exception): + pass + + +class ContentProvider(object): + """ + ContentProvider class provides an internet content. It should NOT have any xbmc-related imports + and must be testable without XBMC runtime. This is a basic/dummy implementation. + """ + + def __init__(self, name='dummy', base_url='/', username=None, password=None, filter=None, tmp_dir='.'): + """ + ContentProvider constructor + Args: + name (str): name of provider + base_url (str): base url of site being accessed + username (str): login username + password (str): login password + filter (func{item}): function to filter results returned by search or list methods + tmp_dir (str): temporary dir where provider can store/cache files + """ + self.name = name + self.username = username + self.password = password + if not base_url[-1] == '/': + base_url += '/' + self.base_url = base_url + self.filter = filter + self.tmp_dir = tmp_dir + self.cache = StorageServer.StorageServer(self.name, 24) + self.lang = 'cs' # initialize, current language could be set by XBMContentProvider + + def __str__(self): + return 'ContentProvider' + self.name + + def on_init(self): + """ + This function gets called by XbmcContentProvider after it initializes itself + and sets eveything up (especially 'lang' property of ContentProvider which gets detected + from kodi) + """ + pass + + def capabilities(self): + """ + This way class defines which capabilities it provides ['login','search','resolve','categories'] + It may also contain '!download' when provider does not support downloading + """ + return [] + + def video_item(self, url='', img='', quality='???'): + """ + Returns empty video item - contains all required fields + """ + return {'type': 'video', 'title': '', 'rating': 0, 'year': 0, 'size': '0MB', 'url': url, 'img': img, + 'length': '', 'quality': quality, 'subs': '', 'surl': '', 'lang': ''} + + def dir_item(self, title='', url='', type='dir'): + """ + Returns empty directory item + """ + return {'type': type, 'title': title, 'size': '0', 'url': url} + + def login(self): + """ + A login method returns True on successfull login, False otherwise + """ + return False + + def search(self, keyword): + """ + Search for a keyword on a site + Args: + keyword (str) + + returns: + array of video or directory items + """ + return [] + + def list(self, url): + """ + Lists content on given url + Args: + url (str): either relative or absolute provider URL + + Returns: + array of video or directory items + + """ + return [] + + def categories(self): + """ + Lists categories on provided site + + Returns: + array of video or directory items + """ + return [] + + def findstreams(self, data, regexes=None): + """ + Finds streams in given data (see resovler.findstreams for more details) + + :param data: A string (piece of HTML, for example) or an array of URLs + :param regexes: An array of regexes to be used for extracting URLs from + 'data' of type 'string' + :returns: An array of video items + """ + resolved = resolver.findstreams(data, regexes) + if resolved is None: + raise ResolveException( + 'Nelze ziskat video link [CR]zkontrolujte jestli video nebylo odstraneno') + elif isinstance(resolved, list) and not resolved: + raise ResolveException('Video je na serveru, ktery neni podporovan') + elif not resolved: + raise ResolveException( + 'Nebyl nalezen zadny video embed [CR]zkontrolujte stranku pripadne nahlaste chybu pluginu') + result = [] + for j in resolved: + i = defaultdict(lambda: '', j) + item = self.video_item() + item['title'] = i['name'] + item['url'] = i['url'] + item['quality'] = i['quality'] + item['surl'] = i['surl'] + item['subs'] = i['subs'] + item['headers'] = i['headers'] + item['lang'] = i['lang'] + item['info'] = i['info'] + result.append(item) + return result + + def resolve(self, item, captcha_cb=None, select_cb=None, wait_cb=None): + """ + Resolves given video item to a downloable/playable file/stream URL + + Args: + url (str): relative or absolute URL to be resolved + captcha_cb(func{obj}): callback function when user input is required (captcha, one-time passwords etc). + function implementation must be Provider-specific + select_cb(func{array}): callback function for cases when given url resolves to multiple streams, + provider class may call this function and require user interaction + wait_cb(func{obj}): callback function for cases when url resolves to stream which becomes available + somewhere in future (typically in several seconds). Provider may call this and require waiting. + Returns: + None - if ``url`` was not resolved. Video item with 'url' key pointing to resolved target + """ + return None + + def _url(self, url): + """ + Transforms relative to absolute url based on ``base_url`` class property + """ + if url.startswith('http'): + return url + return self.base_url + url.lstrip('./') + + def _filter(self, result, item): + """ + Applies filter, if filter passes `item` is appended to `result` + + Args: + result (array) : target array + item (obj) : item that is being applied filter on + """ + if self.filter: + if self.filter(item): + result.append(item) + else: + result.append(item) + + def info(self, msg): + util.info('[%s] %s' % (self.name, msg)) + + def error(self, msg): + util.error('[%s] %s' % (self.name, msg)) + + +class cached(object): + """ + A method decorator that can be used on any ContentProvider method + Having this decorator means that results of such method are going + to be cached for 24hours by default. You can pass number argument + to decorator, for example @cached(1) would cache for 1 hour. + """ + + def __init__(self, ttl=24): + self.ttl = ttl + + def __call__(self, f): + def wrap(*args): + provider = args[0] + cache = StorageServer.StorageServer(provider.name + str(self.ttl), self.ttl) + return cache.cacheFunction(f, *args) + + return wrap diff --git a/lib/contentprovider/xbmcprovider.py b/lib/contentprovider/xbmcprovider.py new file mode 100644 index 0000000..f7e1401 --- /dev/null +++ b/lib/contentprovider/xbmcprovider.py @@ -0,0 +1,464 @@ +# * Copyright (C) 2012 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ + +import sys +import os +import re +import traceback +import util +import xbmcutil +import resolver +import time +import xbmcplugin +import xbmc +import xbmcvfs +import xbmcgui +import urllib.parse +import urllib.request, urllib.parse, urllib.error +from collections import defaultdict +from provider import ResolveException + + +class XBMContentProvider(object): + ''' + ContentProvider class provides an internet content. It should NOT have any xbmc-related imports + and must be testable without XBMC runtime. This is a basic/dummy implementation. + ''' + + def __init__(self, provider, settings, addon): + ''' + XBMContentProvider constructor + Args: + name (str): name of provider + ''' + self.provider = provider + # inject current user language + try: # not fully supported on Frodo + provider.lang = xbmc.getLanguage(xbmc.ISO_639_1) + except: + provider.lang = None + pass + self.settings = settings + # lang setting is optional for plugins + if not 'lang' in self.settings: + self.settings['lang'] = '0' + + util.info('Initializing provider %s with settings %s' % (provider.name, settings)) + self.addon = addon + self.addon_id = addon.getAddonInfo('id') + if '!download' not in self.provider.capabilities(): + self.check_setting_keys(['downloads']) + self.cache = provider.cache + provider.on_init() + + def check_setting_keys(self, keys): + for key in keys: + if not key in list(self.settings.keys()): + raise Exception('Invalid settings passed - [' + key + '] setting is required') + + def params(self): + return {'cp': self.provider.name} + + def run(self, params): + if params == {} or params == self.params(): + return self.root() + if 'list' in list(params.keys()): + self.list(self.provider.list(params['list'])) + return xbmcplugin.endOfDirectory(int(sys.argv[1])) + if 'down' in list(params.keys()): + return self.download({'url': params['down'], 'title': params['title']}) + if 'play' in list(params.keys()): + return self.play({'url': params['play'], 'info': params}) + if 'search-list' in list(params.keys()): + return self.search_list() + if 'search' in list(params.keys()): + return self.do_search(params['search']) + if 'search-remove' in list(params.keys()): + return self.search_remove(params['search-remove']) + if 'search-edit' in list(params.keys()): + return self.search_edit(params['search-edit']) + if self.run_custom: + return self.run_custom(params) + + def search_list(self): + params = self.params() + params.update({'search': '#'}) + menu1 = self.params() + menu2 = self.params() + xbmcutil.add_dir(xbmcutil.__lang__(30004), params, xbmcutil.icon('search.png')) + for what in xbmcutil.search_list(self.cache): + params['search'] = what + menu1['search-remove'] = what + menu2['search-edit'] = what + xbmcutil.add_dir(what, params, menuItems={xbmcutil.__lang__( + 30016): menu2, xbmc.getLocalizedString(117): menu1}) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + + def search_remove(self, what): + xbmcutil.search_remove(self.cache, what) + xbmc.executebuiltin('Container.Refresh') + + def search_edit(self, what): + kb = xbmc.Keyboard(what, xbmcutil.__lang__(30003), False) + kb.doModal() + if kb.isConfirmed(): + replacement = kb.getText() + xbmcutil.search_replace(self.cache, what, replacement) + params = self.params() + params.update({'search': replacement}) + action = xbmcutil._create_plugin_url(params) + xbmc.executebuiltin('Container.Update(%s)' % action) + + def do_search(self, what): + if what == '' or what == '#': + kb = xbmc.Keyboard('', xbmcutil.__lang__(30003), False) + kb.doModal() + if kb.isConfirmed(): + what = kb.getText() + if not what == '': + maximum = 20 + try: + maximum = int(self.settings['keep-searches']) + except: + util.error('Unable to parse convert addon setting to number') + pass + xbmcutil.search_add(self.cache, what, maximum) + self.search(what) + + def root(self): + searches = xbmcutil.get_searches(self.addon, self.provider.name) + if len(searches) > 0: + self.provider.info('Upgrading to new saved search storage...') + for s in searches: + self.provider.info('Moving item %s' % s) + xbmcutil.search_add(self.cache, s, 9999999) + xbmcutil.delete_search_history(self.addon, self.provider.name) + + if 'search' in self.provider.capabilities(): + params = self.params() + params.update({'search-list': '#'}) + xbmcutil.add_dir(xbmcutil.__lang__(30003), params, xbmcutil.icon('search.png')) + if not '!download' in self.provider.capabilities(): + xbmcutil.add_local_dir(xbmcutil.__lang__(30006), self.settings[ + 'downloads'], xbmcutil.icon('download.png')) + self.list(self.provider.categories()) + return xbmcplugin.endOfDirectory(int(sys.argv[1])) + + def download(self, item): + downloads = self.settings['downloads'] + if '' == downloads: + xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30009)) + return + stream = self.resolve(item['url']) + if stream: + if not 'headers' in list(stream.keys()): + stream['headers'] = {} + xbmcutil.reportUsage(self.addon_id, self.addon_id + '/download') + # clean up \ and / + name = item['title'].replace('/', '_').replace('\\', '_') + if not stream['subs'] == '': + xbmcutil.save_to_file(stream['subs'], os.path.join( + downloads, name + '.srt'), stream['headers']) + dot = name.find('.') + if dot <= 0: + # name does not contain extension, append some + name += '.mp4' + xbmcutil.download(self.addon, name, self.provider._url( + stream['url']), os.path.join(downloads, name), headers=stream['headers']) + + def play(self, item): + stream = self.resolve(item['url']) + if stream: + xbmcutil.reportUsage(self.addon_id, self.addon_id + '/play') + if 'headers' in list(stream.keys()): + headerStr = '|' + urllib.parse.urlencode(stream['headers']) + if len(headerStr) > 1: + stream['url'] += headerStr.encode('utf-8') + print('Sending %s to player' % stream['url']) + li = xbmcgui.ListItem(path=stream['url']) + li.setArt({'icon': 'DefaulVideo.png'}) + il = self._extract_infolabels(item['info']) + if len(il) > 0: # only set when something was extracted + li.setInfo('video', il) + try: + local_subs = xbmcutil.set_subtitles(li, stream['subs'], stream.get('headers')) + except: + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, li) + xbmcutil.load_subtitles(stream['subs'], stream.get('headers')) + else: + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, li) + + def _handle_exc(self, e): + msg = e.message + if msg.find('$') == 0: + try: + msg = self.addon.getLocalizedString(int(msg[1:])) + except: + try: + msg = xbmcutil.__lang__(int(msg[1:])) + except: + pass + xbmcgui.Dialog().ok(self.provider.name, msg) + + def resolve(self, url): + item = self.provider.video_item() + item.update({'url': url}) + try: + return self.provider.resolve(item) + except ResolveException as e: + self._handle_exc(e) + + def search(self, keyword): + self.list(self.provider.search(keyword)) + return xbmcplugin.endOfDirectory(int(sys.argv[1])) + + def list(self, items): + params = self.params() + for item in items: + if item['type'] == 'dir': + self.render_dir(item) + elif item['type'] == 'next': + params.update({'list': item['url']}) + xbmcutil.add_dir(xbmcutil.__lang__(30007), params, xbmcutil.icon('next.png')) + elif item['type'] == 'prev': + params.update({'list': item['url']}) + xbmcutil.add_dir(xbmcutil.__lang__(30008), params, xbmcutil.icon('prev.png')) + elif item['type'] == 'new': + params.update({'list': item['url']}) + xbmcutil.add_dir(xbmcutil.__lang__(30012), params, xbmcutil.icon('new.png')) + elif item['type'] == 'top': + params.update({'list': item['url']}) + xbmcutil.add_dir(xbmcutil.__lang__(30013), params, xbmcutil.icon('top.png')) + elif item['type'] == 'video': + self.render_video(item) + else: + self.render_default(item) + + def render_default(self, item): + raise Exception("Unable to render item " + str(item)) + + def render_dir(self, item): + params = self.params() + params.update({'list': item['url']}) + title = item['title'] + img = None + if 'img' in list(item.keys()): + img = item['img'] + if title.find('$') == 0: + try: + title = self.addon.getLocalizedString(int(title[1:])) + except: + pass + menuItems = {} + if 'menu' in list(item.keys()): + for ctxtitle, value in item['menu'].items(): + if ctxtitle.find('$') == 0: + try: + ctxtitle = self.addon.getLocalizedString(int(ctxtitle[1:])) + except: + pass + menuItems[ctxtitle] = value + xbmcutil.add_dir(title, params, img, infoLabels=self._extract_infolabels( + item), menuItems=menuItems) + + def _extract_infolabels(self, item): + infoLabels = {} + for label in ['title', 'plot', 'year', 'genre', 'rating', 'director', + 'votes', 'cast', 'trailer', 'tvshowtitle', 'season', + 'episode', 'duration']: + if label in list(item.keys()): + infoLabels[label] = util.decode_html(item[label]) + return infoLabels + + def render_video(self, item): + params = self.params() + params.update({'play': item['url']}) + downparams = self.params() + downparams.update({'title': item['title'], 'down': item['url']}) + def_item = self.provider.video_item() + if item['size'] == def_item['size']: + item['size'] = '' + else: + item['size'] = ' (%s)' % item['size'] + title = '%s%s' % (item['title'], item['size']) + menuItems = {} + if "!download" not in self.provider.capabilities(): + menuItems[xbmc.getLocalizedString(33003)] = downparams + if 'menu' in list(item.keys()): + for ctxtitle, value in item['menu'].items(): + if ctxtitle.find('$') == 0: + try: + ctxtitle = self.addon.getLocalizedString(int(ctxtitle[1:])) + except: + pass + menuItems[ctxtitle] = value + xbmcutil.add_video(title, + params, + item['img'], + infoLabels=self._extract_infolabels(item), + menuItems=menuItems + ) + + def categories(self): + self.list(self.provider.categories(keyword)) + return xbmcplugin.endOfDirectory(int(sys.argv[1])) + + +class XBMCMultiResolverContentProvider(XBMContentProvider): + + def __init__(self, provider, settings, addon): + XBMContentProvider.__init__(self, provider, settings, addon) + self.check_setting_keys(['quality']) + + def resolve(self, url): + item = self.provider.video_item() + item.update({'url': url}) + + def select_cb(resolved): + + quality = self.settings['quality'] or '0' + filtered = resolver.filter_by_quality(resolved, quality) + lang = self.settings['lang'] or '0' + filtered = resolver.filter_by_language(filtered, lang) + # if user requested something but 'ask me' or filtered result is exactly 1 + if len(filtered) == 1 or (int(quality) > 0 and int(lang) == 0): + return filtered[0] + # if user requested particular language and we have it + if len(filtered) > 0 and int(lang) > 0: + return filtered[0] + dialog = xbmcgui.Dialog() + opts = [] + for r in resolved: + d = defaultdict(lambda: '', r) + opts.append('%s [%s] %s' % (d['title'], d['quality'], d['lang'])) + ret = dialog.select(xbmcutil.__lang__(30005), opts) + if ret >= 0: + return resolved[ret] + try: + return self.provider.resolve(item, select_cb=select_cb) + except ResolveException as e: + self._handle_exc(e) + + +class XBMCLoginRequiredContentProvider(XBMContentProvider): + + def root(self): + if not self.provider.login(): + xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011)) + else: + return XBMContentProvider.root(self) + + +class XBMCLoginOptionalContentProvider(XBMContentProvider): + + def __init__(self, provider, settings, addon): + XBMContentProvider.__init__(self, provider, settings, addon) + self.check_setting_keys(['vip']) + + def ask_for_captcha(self, params): + img = os.path.join(str(xbmcvfs.translatePath( + self.addon.getAddonInfo('profile'))), 'captcha.png') + util.save_to_file(params['img'], img) + cd = CaptchaDialog('captcha-dialog.xml', + xbmcutil.__addon__.getAddonInfo('path'), 'default', '0') + cd.image = img + xbmc.sleep(3000) + cd.doModal() + del cd + kb = xbmc.Keyboard('', self.addon.getLocalizedString(200), False) + kb.doModal() + if kb.isConfirmed(): + print('got code ' + kb.getText()) + return kb.getText() + + def ask_for_account_type(self): + if len(self.provider.username) == 0: + util.info('Username is not set, NOT using VIP account') + return False + if self.settings['vip'] == '0': + util.info('Asking user whether to use VIP account') + ret = xbmcgui.Dialog().yesno(self.provider.name, xbmcutil.__lang__(30010)) + return ret == 1 + return self.settings['vip'] == '1' + + def resolve(self, url): + item = self.provider.video_item() + item.update({'url': url}) + if not self.ask_for_account_type(): + # set user/pass to null - user does not want to use VIP at this time + self.provider.username = None + self.provider.password = None + else: + if not self.provider.login(): + xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011)) + return + try: + return self.provider.resolve(item, captcha_cb=self.ask_for_captcha) + except ResolveException as e: + self._handle_exc(e) + + +class XBMCLoginOptionalDelayedContentProvider(XBMCLoginOptionalContentProvider): + + def wait_cb(self, wait): + left = wait + msg = xbmcutil.__lang__(30014).encode('utf-8') + while left > 0: + xbmc.executebuiltin("XBMC.Notification(%s,%s,1000,%s)" % + (self.provider.name, msg % str(left), '')) + left -= 1 + time.sleep(1) + + def resolve(self, url): + item = self.video_item() + item.update({'url': url}) + if not self.ask_for_account_type(): + # set user/pass to null - user does not want to use VIP at this time + self.provider.username = None + self.provider.password = None + else: + if not self.provider.login(): + xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011)) + return + try: + return self.provider.resolve(item, captcha_cb=self.ask_for_captcha, wait_cb=self.wait_cb) + except ResolveException as e: + self._handle_exc(e) + + +class CaptchaDialog (xbmcgui.WindowXMLDialog): + + def __init__(self, *args, **kwargs): + super(xbmcgui.WindowXMLDialog, self).__init__(args, kwargs) + self.image = None + + def onFocus(self, controlId): + self.controlId = controlId + + def onInit(self): + self.getControl(101).setImage(self.image) + + def onAction(self, action): + if action.getId() in [9, 10]: + self.close() + + def onClick(self, controlId): + if controlId == 102: + self.close() diff --git a/lib/crypto/__init__.py b/lib/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/crypto/__pycache__/__init__.cpython-38.opt-1.pyc b/lib/crypto/__pycache__/__init__.cpython-38.opt-1.pyc new file mode 100644 index 0000000..ea61905 Binary files /dev/null and b/lib/crypto/__pycache__/__init__.cpython-38.opt-1.pyc differ diff --git a/lib/crypto/__pycache__/md5crypt.cpython-38.opt-1.pyc b/lib/crypto/__pycache__/md5crypt.cpython-38.opt-1.pyc new file mode 100644 index 0000000..dcc6efa Binary files /dev/null and b/lib/crypto/__pycache__/md5crypt.cpython-38.opt-1.pyc differ diff --git a/lib/crypto/md5crypt.py b/lib/crypto/md5crypt.py new file mode 100644 index 0000000..8023187 --- /dev/null +++ b/lib/crypto/md5crypt.py @@ -0,0 +1,75 @@ +import hashlib + +MAGIC = '$1$' +ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +def to64(v, n): + ret = '' + while n > 0: + ret += ITOA64[v & 0x3f] + v >>= 6 + n -= 1 + return ret + +def apache_md5_crypt(pw, salt): + return unix_md5_crypt(pw, salt, '$apr1$') + +def unix_md5_crypt(pw, salt, magic=None): + if magic is None: + magic = MAGIC + + if salt.startswith(magic): + salt = salt[len(magic):] + + salt = salt.split('$', 1)[0][:8] + + pw_bytes = pw.encode('utf-8') + salt_bytes = salt.encode('utf-8') + magic_bytes = magic.encode('utf-8') + + ctx = pw_bytes + magic_bytes + salt_bytes + final = hashlib.md5(pw_bytes + salt_bytes + pw_bytes).digest() + + for pl in range(len(pw_bytes), 0, -16): + ctx += final[:min(16, pl)] + + i = len(pw_bytes) + while i: + if i & 1: + ctx += b'\x00' + else: + ctx += pw_bytes[:1] + i >>= 1 + + final = hashlib.md5(ctx).digest() + + for i in range(1000): + ctx1 = b'' + if i & 1: + ctx1 += pw_bytes + else: + ctx1 += final + if i % 3: + ctx1 += salt_bytes + if i % 7: + ctx1 += pw_bytes + if i & 1: + ctx1 += final + else: + ctx1 += pw_bytes + final = hashlib.md5(ctx1).digest() + + passwd = '' + passwd += to64((final[0] << 16) | (final[6] << 8) | final[12], 4) + passwd += to64((final[1] << 16) | (final[7] << 8) | final[13], 4) + passwd += to64((final[2] << 16) | (final[8] << 8) | final[14], 4) + passwd += to64((final[3] << 16) | (final[9] << 8) | final[15], 4) + passwd += to64((final[4] << 16) | (final[10] << 8) | final[5], 4) + passwd += to64(final[11], 2) + + return magic + salt + '$' + passwd + +md5crypt = unix_md5_crypt + +if __name__ == "__main__": + print(unix_md5_crypt("cat", "hat")) diff --git a/lib/resolver.py b/lib/resolver.py new file mode 100644 index 0000000..063f107 --- /dev/null +++ b/lib/resolver.py @@ -0,0 +1,289 @@ +# -*- coding: UTF-8 -*- +# * Copyright (C) 2011 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ + +import sys +import os +import re +import traceback + +import util + +sys.path.append(os.path.join(os.path.dirname(__file__), 'server')) + +RESOLVERS = [] +util.debug('%s searching for modules' % __name__) +for module in os.listdir(os.path.join(os.path.dirname(__file__), 'server')): + if module == '__init__.py' or module[-3:] != '.py': + continue + module = module[:-3] + exec('import %s' % module) + resolver = eval(module) + util.debug('found %s %s' % (resolver, dir(resolver))) + + if not hasattr(resolver, '__priority__'): + resolver.__priority__ = 0 + RESOLVERS.append(resolver) + del module +RESOLVERS = sorted(RESOLVERS, key=lambda m: -m.__priority__) +util.debug('done') + + +def item(): + return {'name': '', 'url': '', 'quality': '???', 'surl': '', 'subs': '', 'headers': {}} + + +def resolve(url): + """ + resolves given url by asking all resolvers + + returns None if no resolver advised to be able to resolve this url + returns False if resolver did his job, but did not return any value (thus failed) + returns Array of resolved objects in positive usecase + """ + url = util.decode_html(url) + util.info('Resolving ' + url) + resolver = _get_resolver(url) + value = None + if resolver is None: + return None + util.info('Using resolver \'%s\'' % str(resolver.__name__)); + try: + value = resolver.resolve(url) + except: + traceback.print_exc() + if value is None: + return False + default = item() + + def fix_stream(i, url, resolver, default): + """ fix missing but required values """ + if 'name' not in list(i.keys()): + i['name'] = resolver.__name__ + if 'surl' not in list(i.keys()): + i['surl'] = url + for key in list(default.keys()): + if key not in list(i.keys()): + i[key] = default[key] + + [fix_stream(i, url, resolver, default) for i in value] + return sorted(value, key=lambda i: i['quality']) + + +def _get_resolver(url): + util.debug('Get resolver for ' + url) + for r in RESOLVERS: + util.debug('querying %s' % r) + if r.supports(url): + return r + + +def can_resolve(url): + """ Returns true if we are able to resolve stream by given URL """ + return _get_resolver(url) is not None + + +def filter_resolvable(url): + if url.find('facebook') > 0 or url.find('yield') > 0: + return + return url.strip('\'\"') + + +def findstreams(data, regexes=None): + """ + Finds streams in given data. Respects caller add-on settings about + quality and asks user if necessary. + + :param data: A string (piece of text / HTML code), an array of URLs or an + array of dictionaries, where 'url' key stores actual URL and + all other keys not present in item() are being copied to the + resolved stream dictionary + :param regexes: An array of strings - regular expressions, each MUST define + named group called 'url', which retrieves resolvable URL + (that one is passed to resolve operation); only used + with 'data' of type 'string' + :returns: An array of resolved objects, None if at least 1 resolver failed + to resolve and nothing else was found, an empty array if no + resolvers for URLs has been found or False if none of regexes + found anything + """ + + def get_url(obj): + return obj['url'] if isinstance(obj, dict) else obj + + urls = [] + resolvables = [] + resolved = [] + not_found = False + if isinstance(data, str) and regexes: + for regex in regexes: + for match in re.finditer(regex, data, re.IGNORECASE | re.DOTALL): + urls.append(match.group('url')) + elif isinstance(data, list): + urls = data + else: + raise TypeError + for url in urls: + if isinstance(url, dict): + url['url'] = filter_resolvable(url['url']) + else: + url = filter_resolvable(url) + if url and url not in resolvables: + util.info('Found resolvable ' + get_url(url)) + resolvables.append(url) + if len(resolvables) == 0: + util.info('No resolvables found!') + return False + for url in resolvables: + streams = resolve(get_url(url)) + if streams is None: + util.info('No resolver found for ' + get_url(url)) + not_found = True + elif not streams: + util.info('There was an error resolving ' + get_url(url)) + elif len(streams) > 0: + for stream in streams: + if isinstance(url, dict): + for key in list(url.keys()): + if key not in stream: + stream[key] = url[key] + elif key not in item(): + if isinstance(stream[key], str) and \ + isinstance(url[key], str): + stream[key] = url[key] + ' +' + stream[key] + elif isinstance(stream[key], list) and \ + isinstance(url[key], list): + stream[key] = url[key] + stream[key] + elif isinstance(stream[key], dict) and \ + isinstance(url[key], dict): + stream[key].update(url[key]) + resolved.append(stream) + if len(resolved) == 0: + if not_found: + return [] + return None + resolved = sorted(resolved, key=lambda i: i['quality']) + resolved = sorted(resolved, key=lambda i: len(i['quality'])) + resolved.reverse() + return resolved + + +q_map = {'3': '720p', '4': '480p', '5': '360p'} + + +def filter_by_quality(resolved, q): + util.info('filtering by quality setting ' + q) + if q == '0': + return resolved + sources = {} + ret = [] + # first group streams by source url + for item in resolved: + if item['surl'] in list(sources.keys()): + sources[item['surl']].append(item) + else: + sources[item['surl']] = [item] + if q == '1': + # always return best quality from each source + for key in list(sources.keys()): + ret.append(sources[key][0]) + elif q == '2': + # always return worse quality from each source + for key in list(sources.keys()): + ret.append(sources[key][-1]) + else: + # we try to select sources of desired qualities + quality = q_map[q] + # 3,4,5 are 720,480,360 + for key in list(sources.keys()): + added = False + for item in sources[key]: + if quality == item['quality']: + ret.append(item) + added = True + if not added: + util.debug('Desired quality %s not found, adding best found' % quality) + ret.append(sources[key][-1]) + # sort results again, so best quality streams appear first + ret = sorted(ret, key=lambda i: i['quality']) + if not q == '2': + ret.reverse() + return ret + +def filter_by_language(resolved, lang): + util.info('filtering by language setting ' + lang) + if lang == '0': + return resolved + ret = [] + # first group streams by source url + for item in resolved: + if 'lang' in item and item['lang'] != '': + util.info(item) + if lang == '1' and re.match('en', item['lang'], re.IGNORECASE): + ret.append(item) + if lang == '2' and re.match('cs|cz|čeština', item['lang'], re.IGNORECASE): + ret.append(item) + return ret + +def findstreams_multi(data, regexes): + """ + Finds streams in given data according to given regexes + respects caller addon's setting about desired quality, asks user if needed + assumes, that all resolvables need to be returned, but in particular quality + @param data piece of text (HTML code) to search in + @param regexes - array of strings - regular expressions, each MUST define named group called 'url' + which retrieves resolvable URL (that one is passsed to resolve operation) + @return array of dictionaries with keys: name,url,quality,surl + @return None if at least 1 resoler failed to resolve and nothing else has been found + @return [] if no resolvable URLs or no resolvers for URL has been found + """ + resolved = [] + # keep list of found urls to aviod having duplicates + urls = [] + error = False + for regex in regexes: + for match in re.finditer(regex, data, re.IGNORECASE | re.DOTALL): + print('Found resolvable %s ' % match.group('url')) + streams = resolve(match.group('url')) + if isinstance(streams, list) and streams: + util.debug('There was an error resolving ' + match.group('url')) + error = True + if streams is not None: + if len(streams) > 0: + for stream in streams: + resolved.append(stream) + if error and len(resolved) == 0: + return None + if len(resolved) == 0: + return [] + resolved = sorted(resolved, key=lambda i: i['quality']) + resolved = sorted(resolved, key=lambda i: len(i['quality'])) + resolved2 = resolved + resolved2.reverse() + qualities = {} + for item in resolved2: + if item['quality'] in list(qualities.keys()): + qualities[item['quality']].append(item) + else: + qualities[item['quality']] = [item] + # now .. we must sort items to be in same order as they were found on page + for q in list(qualities.keys()): + qualities[q] = sorted(qualities[q], key=lambda i: resolved.index(i)) + return qualities diff --git a/lib/search.py b/lib/search.py new file mode 100644 index 0000000..69b727f --- /dev/null +++ b/lib/search.py @@ -0,0 +1,76 @@ + +# -*- coding: UTF-8 -*- +#/* +# * Copyright (C) 2011 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ +import os,re,sys +import xbmcplugin,xbmcaddon,xbmc +import xbmcutil,util + +def _list(addon,history,key,value): + params = {} + menuItems = {} + if key: + params[key] = value + menuItems[key] = value + params['search'] = '#' + xbmcutil.add_dir(xbmcutil.__lang__(30004),params,xbmcutil.icon('search.png')) + for what in xbmcutil.get_searches(addon,history): + params['search'] = what + menuItems['search-remove'] = what + xbmcutil.add_dir(what,params,menuItems={xbmc.getLocalizedString(117):menuItems}) + xbmcplugin.endOfDirectory(int(sys.argv[1])) + +def _remove(addon,history,search): + xbmcutil.remove_search(addon,history,search) + xbmc.executebuiltin('Container.Refresh') + +def _search(addon,history,what,update_history,callback): + if what == '' or what == '#': + kb = xbmc.Keyboard('',xbmcutil.__lang__(30003),False) + kb.doModal() + if kb.isConfirmed(): + what = kb.getText() + if not what == '': + maximum = 20 + try: + maximum = int(addon.getSetting('keep-searches')) + except: + util.error('Unable to parse convert addon setting to number') + pass + if update_history: + xbmcutil.add_search(addon,history,what,maximum) + callback(what) + +def item(items={},label=xbmcutil.__lang__(30003)): + items['search-list'] = '#' + xbmcutil.add_dir(label,items,xbmcutil.icon('search.png')) + +def main(addon,history,p,callback,key=None,value=None): + if (key==None) or (key in p and p[key] == value): + if 'search-list' in list(p.keys()): + _list(addon,history,key,value) + if 'search' in list(p.keys()): + update_history=True + if 'search-no-history' in list(p.keys()): + update_history=False + _search(addon,history,p['search'],update_history,callback) + if 'search-remove' in list(p.keys()): + _remove(addon,history,p['search-remove']) diff --git a/lib/server/__init__.py b/lib/server/__init__.py new file mode 100644 index 0000000..56e0887 --- /dev/null +++ b/lib/server/__init__.py @@ -0,0 +1,31 @@ +#/* +# * Copyright (C) 2011 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ + + +##########################################################3 +# all resolvers modules in this directory must have following methods: + +# __name__ - name of the resolver module - can override module filename +# def supports(url) - returns true iff resolver is able to resolve url to stream otherwise false +# def resolve(url) - returns array of all hashmaps that were resolved +# - if resolving fails, nothing is returned +# - a hash MUST contain key 'url' - it's value is stream URL +# - optional keys are 'subs' (link to subtitle), 'quality' (quality string like '240p' or just 'HD' diff --git a/lib/server/__pycache__/anyfilesresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/anyfilesresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5d00482 Binary files /dev/null and b/lib/server/__pycache__/anyfilesresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/dailymotionresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/dailymotionresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a522930 Binary files /dev/null and b/lib/server/__pycache__/dailymotionresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/divxstageresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/divxstageresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..503d788 Binary files /dev/null and b/lib/server/__pycache__/divxstageresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/eserialresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/eserialresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..0335f11 Binary files /dev/null and b/lib/server/__pycache__/eserialresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/exashareresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/exashareresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..3fe3e38 Binary files /dev/null and b/lib/server/__pycache__/exashareresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/flashxresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/flashxresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..aef6817 Binary files /dev/null and b/lib/server/__pycache__/flashxresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/gosuparkresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/gosuparkresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9394260 Binary files /dev/null and b/lib/server/__pycache__/gosuparkresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/hqqresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/hqqresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2b06755 Binary files /dev/null and b/lib/server/__pycache__/hqqresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/koukejseresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/koukejseresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..da93aaf Binary files /dev/null and b/lib/server/__pycache__/koukejseresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/koukniresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/koukniresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..73fdbb3 Binary files /dev/null and b/lib/server/__pycache__/koukniresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/ksetresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/ksetresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5e7dd9b Binary files /dev/null and b/lib/server/__pycache__/ksetresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/letwatch.cpython-38.opt-1.pyc b/lib/server/__pycache__/letwatch.cpython-38.opt-1.pyc new file mode 100644 index 0000000..600508d Binary files /dev/null and b/lib/server/__pycache__/letwatch.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/mixturevideoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/mixturevideoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a7cf04c Binary files /dev/null and b/lib/server/__pycache__/mixturevideoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/moevideoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/moevideoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a9409da Binary files /dev/null and b/lib/server/__pycache__/moevideoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/mojevideosk.cpython-38.opt-1.pyc b/lib/server/__pycache__/mojevideosk.cpython-38.opt-1.pyc new file mode 100644 index 0000000..e42f064 Binary files /dev/null and b/lib/server/__pycache__/mojevideosk.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/movshareresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/movshareresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..aed5b98 Binary files /dev/null and b/lib/server/__pycache__/movshareresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/munkvideoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/munkvideoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..24adaeb Binary files /dev/null and b/lib/server/__pycache__/munkvideoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/myviruresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/myviruresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..f538c42 Binary files /dev/null and b/lib/server/__pycache__/myviruresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/nahnojiresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/nahnojiresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d7f5911 Binary files /dev/null and b/lib/server/__pycache__/nahnojiresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/novamovresovler.cpython-38.opt-1.pyc b/lib/server/__pycache__/novamovresovler.cpython-38.opt-1.pyc new file mode 100644 index 0000000..944d12d Binary files /dev/null and b/lib/server/__pycache__/novamovresovler.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/openload.cpython-38.opt-1.pyc b/lib/server/__pycache__/openload.cpython-38.opt-1.pyc new file mode 100644 index 0000000..fd96ad2 Binary files /dev/null and b/lib/server/__pycache__/openload.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/playedtoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/playedtoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..04ba62f Binary files /dev/null and b/lib/server/__pycache__/playedtoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/playmdresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/playmdresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..481c619 Binary files /dev/null and b/lib/server/__pycache__/playmdresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/publicvideohostresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/publicvideohostresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..c216658 Binary files /dev/null and b/lib/server/__pycache__/publicvideohostresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/putlockerresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/putlockerresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..df68aca Binary files /dev/null and b/lib/server/__pycache__/putlockerresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/rutuberesolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/rutuberesolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d117486 Binary files /dev/null and b/lib/server/__pycache__/rutuberesolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/servertipczresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/servertipczresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..d3f48c5 Binary files /dev/null and b/lib/server/__pycache__/servertipczresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/sledujuserialyresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/sledujuserialyresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..a0fb6cc Binary files /dev/null and b/lib/server/__pycache__/sledujuserialyresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/stagevuresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/stagevuresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4aa8c81 Binary files /dev/null and b/lib/server/__pycache__/stagevuresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/streamcloudresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/streamcloudresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..592973e Binary files /dev/null and b/lib/server/__pycache__/streamcloudresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/streamintoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/streamintoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5f8114b Binary files /dev/null and b/lib/server/__pycache__/streamintoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/streamujtvresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/streamujtvresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..9b170a8 Binary files /dev/null and b/lib/server/__pycache__/streamujtvresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/trivialresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/trivialresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..4e20ed5 Binary files /dev/null and b/lib/server/__pycache__/trivialresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videobbresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/videobbresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..16ce114 Binary files /dev/null and b/lib/server/__pycache__/videobbresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videomailresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/videomailresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..35820c9 Binary files /dev/null and b/lib/server/__pycache__/videomailresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videonetresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/videonetresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..da71741 Binary files /dev/null and b/lib/server/__pycache__/videonetresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videoweedresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/videoweedresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..2e4d672 Binary files /dev/null and b/lib/server/__pycache__/videoweedresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videozerresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/videozerresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..547c3f2 Binary files /dev/null and b/lib/server/__pycache__/videozerresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/videram.cpython-38.opt-1.pyc b/lib/server/__pycache__/videram.cpython-38.opt-1.pyc new file mode 100644 index 0000000..bfeed34 Binary files /dev/null and b/lib/server/__pycache__/videram.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/vimeoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/vimeoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..13ba13f Binary files /dev/null and b/lib/server/__pycache__/vimeoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/vkontakteresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/vkontakteresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..5cf3ed8 Binary files /dev/null and b/lib/server/__pycache__/vkontakteresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/vuuzlaresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/vuuzlaresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..1970332 Binary files /dev/null and b/lib/server/__pycache__/vuuzlaresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/youtuberesolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/youtuberesolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..7fa170c Binary files /dev/null and b/lib/server/__pycache__/youtuberesolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/youwatch.cpython-38.opt-1.pyc b/lib/server/__pycache__/youwatch.cpython-38.opt-1.pyc new file mode 100644 index 0000000..63e4e6e Binary files /dev/null and b/lib/server/__pycache__/youwatch.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/zideonlresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/zideonlresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..08a0b43 Binary files /dev/null and b/lib/server/__pycache__/zideonlresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/__pycache__/zkouknitoresolver.cpython-38.opt-1.pyc b/lib/server/__pycache__/zkouknitoresolver.cpython-38.opt-1.pyc new file mode 100644 index 0000000..53df311 Binary files /dev/null and b/lib/server/__pycache__/zkouknitoresolver.cpython-38.opt-1.pyc differ diff --git a/lib/server/anyfilesresolver.py b/lib/server/anyfilesresolver.py new file mode 100644 index 0000000..c6fec63 --- /dev/null +++ b/lib/server/anyfilesresolver.py @@ -0,0 +1,104 @@ +# -*- coding: UTF-8 -*- +# * GNU General Public License for more details. +# * +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# * +# * thanks to http://code.google.com/p/sd-xbmc/ +# */ + +import re +import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.error, urllib.parse +import random +import decimal + +import util + +__name__='anyfiles' + +BASE_URL = 'http://video.anyfiles.pl' + +def supports(url): + return not _regex(url) == None + +def _gen_random_decimal(i, d): + return decimal.Decimal('%d.%d' % (random.randint(0, i), random.randint(0, d))) + + +def _decode(param): + #-- define variables + loc_3 = [0,0,0,0] + loc_4 = [0,0,0] + loc_2 = '' + #-- define hash parameters for decoding + dec = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + hash1 = ["L", "y", "c", "X", "2", "M", "a", "l", "p", "5", "Q", "e", "R", "t", "Z", "Y", "9", "m", "d", "0", "s", "V", "b", "3", "7", "="] + hash2 = ["i", "B", "v", "U", "H", "4", "D", "n", "k", "8", "x", "T", "u", "G", "w", "f", "N", "J", "6", "W", "1", "g", "z", "o", "I", "r"] + hash1 = ["c", "u", "4", "V", "z", "5", "k", "m", "y", "p", "L", "J", "I", "d", "0", "M", "9", "e", "3", "8", "v", "l", "i", "7", "n", "="]; + hash2 = ["t", "Y", "T", "x", "B", "g", "G", "b", "2", "X", "1", "R", "a", "N", "w", "Q", "f", "W", "U", "D", "Z", "s", "6", "H", "o", "r"] + + #-- decode + for i in range(0, len(hash1)): + re1 = hash1[i] + re2 = hash2[i] + + param = param.replace(re1, '___') + param = param.replace(re2, re1) + param = param.replace('___', re2) + + i = 0 + while i < len(param): + j = 0 + while j < 4 and i+j < len(param): + loc_3[j] = dec.find(param[i+j]) + j = j + 1 + + loc_4[0] = (loc_3[0] << 2) + ((loc_3[1] & 48) >> 4); + loc_4[1] = ((loc_3[1] & 15) << 4) + ((loc_3[2] & 60) >> 2); + loc_4[2] = ((loc_3[2] & 3) << 6) + loc_3[3]; + + j = 0 + while j < 3: + if loc_3[j + 1] == 64: + break + try: + loc_2 += chr(loc_4[j]) + except: + pass + j = j + 1 + + i = i + 4; + + return loc_2 + +def resolve(url): + m = _regex(url) + if m: + resp = urllib.request.urlopen(url) + sessc = resp.headers.get('Set-Cookie').split(';')[0] + resp.close() + furl = "%s/w.jsp?id=%s&width=620&height=349&pos=&skin=0" % (BASE_URL,m.group('id')) + headers = {'Cookie':sessc, 'Referer':url} + data = util.request(furl,headers) + m1 = re.search('document.cookie = "([^"]+?)"',data) + m2 = re.search('src="(\/pcsevlet\?code=[^"]+)', data) + if m1 and m2: + headers['Cookie'] = headers['Cookie'] + '; ' + m1.group(1) + headers['Referer'] = BASE_URL + '/flowplayer/flowplayer.commercial-3.2.16.swf' + data = util.request(BASE_URL + m2.group(1),headers) + m_vurl = re.search("'url':.*?'(http[^']+?mp4)'", data, re.DOTALL) + m_surl = re.search("'captionUrl':.*?'(http[^']+)'",data, re.DOTALL) + if m_vurl: + resolved = {'url':m_vurl.group(1).strip(),'quality':'???'} + if m_surl: + resolved['subs'] = m_surl.group(1).strip() + return [resolved] + else: + return [] +def _regex(url): + return re.search('video\.anyfiles\.pl/w\.jsp\?id=(?P\d+)',url,re.IGNORECASE | re.DOTALL) diff --git a/lib/server/dailymotionresolver.py b/lib/server/dailymotionresolver.py new file mode 100644 index 0000000..538db52 --- /dev/null +++ b/lib/server/dailymotionresolver.py @@ -0,0 +1,91 @@ +# -*- coding: UTF-8 -*- +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * + +import re +from xml.etree import ElementTree +import util +from copy import deepcopy +import json + +__name__ = 'dailymotion' + + +def supports(url): + return re.search(r'dailymotion.com/embed', url) is not None + + +def resolve(url): + print('The url is ::', url) + id = re.search(r'dailymotion.com/embed/video/(.+)', url).group(1) + print('The id is ::', id) + headers = {'User-Agent': 'Android'} + cookie = {'Cookie': "lang=en; ff=off"} + r = util.request("http://www.dailymotion.com/player/metadata/video/" + id, + headers) + content = json.loads(r) + cc = content['qualities'] + cc = list(cc.items()) + + cc = sorted(cc, reverse=True) + m_url = '' + other_playable_url = [] + + items = [] + result = [] + + for source, json_source in cc: + source = source.split("@")[0] + for item in json_source: + + m_url = item.get('url', None) + # xbmc.log("DAILYMOTION - m_url = %s" % m_url, xbmc.LOGNOTICE) + if m_url: + if source == "auto": + continue + + elif '.mnft' in m_url: + continue + + if 'video' in item.get('type', None): + item = {} + item['url'] = m_url + item['quality'] = source + item['title'] = 'video' + items.append(item) + + other_playable_url.append(m_url) + + if items: + for item in items: + newitem = deepcopy(item) + item['lang'] = '???' + item['headers'] = headers + result.append(newitem) + if not result and cc[0][0]=='auto': + json_source=cc[0][1] + m_url=json_source[0].get('url', None) + r = util.request(m_url) + streams = re.compile(r'RESOLUTION=\d+x(\d+).*\n([^\s]+)').findall(r) + for quality, url in streams: + item = {} + item['url'] = url + item['quality'] = quality + 'p' + item['title'] = 'video' + result.append(item) + return result diff --git a/lib/server/divxstageresolver.py b/lib/server/divxstageresolver.py new file mode 100644 index 0000000..44d520d --- /dev/null +++ b/lib/server/divxstageresolver.py @@ -0,0 +1,36 @@ +# -*- coding: UTF-8 -*- +#/* +# * Copyright (C) 2011 Libor Zoubek +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ +import re,util,resolver +__name__ = 'divxstage' +def supports(url): + return not _regex(url) == None + +# returns the steam url +def resolve(url): + if not _regex(url) == None: + data = util.substr(util.request(url),'') + link = re.search('src=\"([^\"]+)',data,re.IGNORECASE | re.DOTALL) + if link: + return [{'url':link.group(1)}] + +def _regex(url): + return re.search('embed.divxstage.eu/(.+?)',url,re.IGNORECASE | re.DOTALL) diff --git a/lib/server/eserialresolver.py b/lib/server/eserialresolver.py new file mode 100644 index 0000000..0b8dc80 --- /dev/null +++ b/lib/server/eserialresolver.py @@ -0,0 +1,27 @@ +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ +import re +__name__='eserial' +def supports(url): + return not _regex(url) == None + +# returns the steam url +def resolve(url): + m = _regex(url) + if m: + stream = re.search('(?P.+?)(\&|$)',m.group('url')).group('url') + show = re.search('serial=(?P.+?)(\&|$)',m.group('url')) + tit = re.search('srt=(?P.+?)(\&|$)',m.group('url')) + if show and tit: + return [{'url':stream,'subs':'http://www.eserial.cz/titulky/%s/%s.srt' % (show.group('url'),tit.group('url'))}] + return [{'url':stream}] + +def _regex(url): + return re.search('eserial\.cz/video\.php\?file=(?P.+?)$',url,re.IGNORECASE | re.DOTALL) + diff --git a/lib/server/exashareresolver.py b/lib/server/exashareresolver.py new file mode 100644 index 0000000..5ccccd0 --- /dev/null +++ b/lib/server/exashareresolver.py @@ -0,0 +1,53 @@ +# -*- coding: UTF-8 -*- +# /* +# * Copyright (C) 2015 Lubomir Kucera +# * +# * +# * This Program is free software; you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation; either version 2, or (at your option) +# * any later version. +# * +# * This Program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; see the file COPYING. If not, write to +# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. +# * http://www.gnu.org/copyleft/gpl.html +# * +# */ +import re +import util +from demjson import demjson + +__author__ = 'Jose Riha/Lubomir Kucera' +__name__ = 'exashare' + + +def supports(url): + return re.search(r'exashare\.com/embed\-[^\.]+\.html', url) is not None + + +def resolve(url): + realurl = re.search(r'