Initial commit: xStream Downloader
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/domain_check.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/no_cover.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/plugin_info.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/resolveurl_settings.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/resolveurl_update.png
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/search.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/settings.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/animetoast.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/aniworld.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/api_sites.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/burningseries.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/einschalten.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/fhdfilme.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/filmpalast.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/hdfilme.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/internetarchive.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/kinoger.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/kkiste.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/megakino.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/moflix-stream.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/moviedream.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/netzkino.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/serienstream.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/streamcloud.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/tmdb_browser.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/topstreamfilm.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/tmdb_browser.png
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/vavoo.png
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/xstream_settings.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,182 @@
|
||||
# Fixed tools.py - all string inputs normalized, no Kodi imports
|
||||
import hashlib, re, os, pyaes, base64
|
||||
from urllib.parse import quote, unquote, quote_plus, unquote_plus, urlparse
|
||||
from html.entities import name2codepoint
|
||||
from difflib import SequenceMatcher
|
||||
from functools import lru_cache
|
||||
|
||||
def _e(s):
|
||||
if s is None: return ''
|
||||
if isinstance(s, bytes): return s.decode('utf-8')
|
||||
if not isinstance(s, str): return str(s)
|
||||
return s
|
||||
|
||||
class cParser:
|
||||
@staticmethod
|
||||
def _get_compiled_pattern(pattern, flags=0):
|
||||
return re.compile(pattern, flags)
|
||||
|
||||
@staticmethod
|
||||
def _replaceSpecialCharacters(s):
|
||||
try:
|
||||
for t in (('\\/', '/'), ('&', '&'), ('\\u00c4', 'Ä'), ('\\u00e4', 'ä'),
|
||||
('\\u00d6', 'Ö'), ('\\u00f6', 'ö'), ('\\u00dc', 'Ü'), ('\\u00fc', 'ü'),
|
||||
('\\u00df', 'ß'), ('\\u2013', '-'), ('\\u00b2', '²'), ('\\u00b3', '³'),
|
||||
('\\u00e9', 'é'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'),
|
||||
('\\u00c9', 'É'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"),
|
||||
('\\u0308', '̈'), ('\\u00e8', 'è'), ('#038;', ''), ('\\u00f8', 'ø'),
|
||||
('/', '/'), ('\\u00e1', 'á'), ('–', '-'), ('“', '"'), ('„', '"'),
|
||||
('’', "'"), ('…', '…'), ('\\u00bc', '¼'), ('\\u00bd', '½'), ('\\u00be', '¾'),
|
||||
('\\u2153', '⅓'), ('\\u002A', '*')):
|
||||
s = s.replace(*t)
|
||||
for h in (('\\/', '/'), ('&', '&'), (''', "'"), ("'", "'"),
|
||||
('Ä', 'Ä'), ('ä', 'ä'), ('Ö', 'Ö'), ('ö', 'ö'),
|
||||
('Ü', 'Ü'), ('ü', 'ü'), ('ß', 'ß'), ('²', '²'),
|
||||
('Ü', '³'), ('¼', '¼'), ('½', '½'), ('¾', '¾'),
|
||||
('⅓', '⅓'), ('∗', '*')):
|
||||
s = s.replace(*h)
|
||||
except: pass
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def parseSingleResult(sHtmlContent, pattern, ignoreCase=False):
|
||||
if sHtmlContent:
|
||||
flags = re.S | re.M
|
||||
if ignoreCase: flags |= re.I
|
||||
matches = cParser._get_compiled_pattern(pattern, flags).search(sHtmlContent)
|
||||
if matches:
|
||||
if matches.lastindex is not None and matches.lastindex >= 1:
|
||||
return True, cParser._replaceSpecialCharacters(matches.group(1))
|
||||
else:
|
||||
return True, cParser._replaceSpecialCharacters(matches.group(0))
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def parse(sHtmlContent, pattern, iMinFoundValue=1, ignoreCase=False):
|
||||
if sHtmlContent:
|
||||
flags = re.DOTALL
|
||||
if ignoreCase: flags |= re.I
|
||||
aMatches = cParser._get_compiled_pattern(pattern, flags).findall(sHtmlContent)
|
||||
if len(aMatches) >= iMinFoundValue:
|
||||
if isinstance(aMatches[0], tuple):
|
||||
aMatches = [tuple(cParser._replaceSpecialCharacters(x) if isinstance(x, str) and x is not None else '' for x in match) for match in aMatches]
|
||||
else:
|
||||
aMatches = [cParser._replaceSpecialCharacters(x) if isinstance(x, str) and x is not None else '' for x in aMatches]
|
||||
return True, aMatches
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def replace(pattern, sReplaceString, sValue):
|
||||
return cParser._get_compiled_pattern(pattern).sub(sReplaceString, sValue)
|
||||
|
||||
@staticmethod
|
||||
def search(pattern, sValue, ignoreCase=True):
|
||||
flags = re.IGNORECASE if ignoreCase else 0
|
||||
return cParser._get_compiled_pattern(pattern, flags).search(sValue)
|
||||
|
||||
@staticmethod
|
||||
def escape(sValue):
|
||||
return re.escape(sValue)
|
||||
|
||||
@staticmethod
|
||||
def getNumberFromString(sValue):
|
||||
aMatches = re.compile(r'\d+').findall(_e(sValue))
|
||||
return int(aMatches[0]) if aMatches else 0
|
||||
|
||||
@staticmethod
|
||||
def urlparse(sUrl):
|
||||
return urlparse(_e(sUrl).replace('www.', '')).netloc.title()
|
||||
|
||||
@staticmethod
|
||||
def urlDecode(sUrl):
|
||||
return unquote(_e(sUrl))
|
||||
|
||||
@staticmethod
|
||||
def urlEncode(sUrl, safe=''):
|
||||
return quote(_e(sUrl), safe)
|
||||
|
||||
@staticmethod
|
||||
def quote(sUrl):
|
||||
return quote(_e(sUrl))
|
||||
|
||||
@staticmethod
|
||||
def unquotePlus(sUrl):
|
||||
return unquote_plus(_e(sUrl))
|
||||
|
||||
@staticmethod
|
||||
def quotePlus(sUrl):
|
||||
return quote_plus(_e(sUrl))
|
||||
|
||||
@staticmethod
|
||||
def B64decode(text):
|
||||
return base64.b64decode(_e(text)).decode('utf-8')
|
||||
|
||||
|
||||
class cUtil:
|
||||
@staticmethod
|
||||
def removeHtmlTags(sValue, sReplace=''):
|
||||
return re.compile(r'<.*?>').sub(sReplace, sValue)
|
||||
|
||||
@staticmethod
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if not text.endswith(';'): text += ';'
|
||||
if text[:2] == '&#':
|
||||
try:
|
||||
if text[:3] == '&#x': return chr(int(text[3:-1], 16))
|
||||
else: return chr(int(text[2:-1]))
|
||||
except ValueError: pass
|
||||
else:
|
||||
try: return chr(name2codepoint[text[1:-1]])
|
||||
except KeyError: pass
|
||||
return text
|
||||
t = _e(text)
|
||||
try: t = t.decode('utf-8')
|
||||
except: pass
|
||||
return re.compile(r'&(\w+;|#x?\d+;?)').sub(fixup, t.strip())
|
||||
|
||||
@staticmethod
|
||||
def cleanse_text(text):
|
||||
if text is None: text = ''
|
||||
return cUtil.removeHtmlTags(text)
|
||||
|
||||
@staticmethod
|
||||
def evp_decode(cipher_text, passphrase, salt=None):
|
||||
ct = _e(cipher_text).encode('latin-1') if isinstance(cipher_text, str) else cipher_text
|
||||
pwd = _e(passphrase).encode('latin-1') if isinstance(passphrase, str) else passphrase
|
||||
if not salt: salt = ct[8:16]; ct = ct[16:]
|
||||
else: salt = _e(salt).encode('latin-1') if isinstance(salt, str) else salt
|
||||
key, iv = cUtil.evpKDF(pwd, salt)
|
||||
decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
|
||||
plain_text = decrypter.feed(ct)
|
||||
plain_text += decrypter.feed()
|
||||
return plain_text.decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def evpKDF(pwd, salt, key_size=32, iv_size=16):
|
||||
temp = b''
|
||||
fd = b''
|
||||
while len(fd) < key_size + iv_size:
|
||||
h = hashlib.md5()
|
||||
h.update(temp + pwd + salt)
|
||||
temp = h.digest()
|
||||
fd += temp
|
||||
return fd[0:key_size], fd[key_size:key_size + iv_size]
|
||||
|
||||
@staticmethod
|
||||
def isSimilar(sSearch, sText, threshold=0.9):
|
||||
return SequenceMatcher(None, _e(sSearch), _e(sText)).ratio() >= threshold
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=200000)
|
||||
def get_seq_match_ratio(token1, token2):
|
||||
return SequenceMatcher(None, _e(token1), _e(token2)).ratio()
|
||||
|
||||
@staticmethod
|
||||
def isSimilarByToken(sSearch, sText, threshold=0.9):
|
||||
tokens_sSearch = _e(sSearch).split()
|
||||
tokens_sText = _e(sText).split()
|
||||
if not tokens_sSearch: return False
|
||||
best_ratios = [max(cUtil.get_seq_match_ratio(token, token2) for token2 in tokens_sText) for token in tokens_sSearch]
|
||||
return (sum(best_ratios) / len(best_ratios)) >= threshold
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/cache.py
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/captcha/captcha_helper.py
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/captcha/captcha_solver.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/config.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/contextElement.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/gui.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/guiElement.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/hoster.py
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/ParameterHandler.py
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,193 @@
|
||||
# Echte xStream-Handler (Standard-Lib only)
|
||||
import re
|
||||
from urllib.parse import quote, urlencode
|
||||
import gzip, ssl
|
||||
from urllib.request import Request, urlopen, HTTPCookieProcessor, build_opener, HTTPSHandler
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class _FakeConfig:
|
||||
_settings = {
|
||||
'cacheTime': '1', 'requestTimeout': '15',
|
||||
'bypassDNSlock': 'false', 'volatileHtmlCache': 'false',
|
||||
}
|
||||
def getAddonInfo(self, k):
|
||||
return {'profile': '/tmp', 'id': 'xstream', 'name': 'xStream',
|
||||
'version': '1.0.0', 'icon': '', 'author': '', 'description': ''}.get(k, '')
|
||||
def getSetting(self, k): return self._settings.get(k, '')
|
||||
def setSetting(self, k, v): self._settings[k] = str(v)
|
||||
def getLocalizedString(self, k): return f'[{k}]'
|
||||
|
||||
class _FakeCache:
|
||||
def get(self, key, cache_time): return None
|
||||
def set(self, key, data): pass
|
||||
def clear(self): pass
|
||||
|
||||
class _FakeLogger:
|
||||
def info(self, *a, **k): pass
|
||||
def debug(self, *a, **k): pass
|
||||
def error(self, *a, **k): pass
|
||||
|
||||
def _infoDialog(*a, **k): pass
|
||||
|
||||
|
||||
class cRequestHandler:
|
||||
persistent_openers = {}
|
||||
|
||||
UA_LIST = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15',
|
||||
]
|
||||
|
||||
def __init__(self, sUrl='', caching=True, ignoreErrors=False, method='GET',
|
||||
data=None, compression=True, jspost=False, ssl_verify=False):
|
||||
self._sUrl = str(sUrl) if sUrl else ''
|
||||
self._sRealUrl = ''
|
||||
self._USER_AGENT = self.UA_LIST[0]
|
||||
self._aParameters = {}
|
||||
self._headerEntries = {}
|
||||
self._Status = ''
|
||||
self._sResponseHeader = ''
|
||||
self._ssl_verify = ssl_verify
|
||||
self.caching = caching
|
||||
self.method = method
|
||||
self.data = data
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.compression = compression
|
||||
self.jspost = jspost
|
||||
self.requestTimeout = 15
|
||||
self.__bRemoveNewLines = True
|
||||
self.__bRemoveBreakLines = True
|
||||
self.isMemoryCacheActive = False
|
||||
self.__setDefaultHeader()
|
||||
|
||||
def __setDefaultHeader(self):
|
||||
self.addHeaderEntry('User-Agent', self._USER_AGENT)
|
||||
self.addHeaderEntry('Accept', '*/*')
|
||||
self.addHeaderEntry('Accept-Language', 'en-US,en;q=0.5')
|
||||
if self.compression:
|
||||
self.addHeaderEntry('Accept-Encoding', 'gzip, deflate')
|
||||
|
||||
def removeNewLines(self, b): self.__bRemoveNewLines = b
|
||||
def removeBreakLines(self, b): self.__bRemoveBreakLines = b
|
||||
|
||||
def addHeaderEntry(self, k, v): self._headerEntries[k] = v
|
||||
def addParameters(self, k, v, Quote=False):
|
||||
self._aParameters[k] = v if not Quote else quote(str(v))
|
||||
def getHeaderEntry(self, k): return self._headerEntries.get(k)
|
||||
def getResponseHeader(self): return self._sResponseHeader
|
||||
def getRealUrl(self): return self._sRealUrl or self._sUrl
|
||||
def getStatus(self): return self._Status
|
||||
def getUrl(self): return self._sUrl
|
||||
def setUrl(self, url):
|
||||
self._sUrl = str(url)
|
||||
return self
|
||||
def ignoreDiscard(self, v): pass
|
||||
def ignoreExpired(self, v): pass
|
||||
|
||||
def getHtml(self, encoding='utf-8'):
|
||||
if not self._sUrl or self._sUrl in ('False', 'None', ''):
|
||||
return ''
|
||||
try:
|
||||
url = self._sUrl
|
||||
if self._aParameters and self.method == 'GET':
|
||||
sep = '&' if '?' in url else '?'
|
||||
url = url + sep + urlencode(self._aParameters)
|
||||
|
||||
req = Request(url, data=self.data, method=self.method)
|
||||
for k, v in self._headerEntries.items():
|
||||
try: req.add_header(k, v)
|
||||
except: pass
|
||||
|
||||
ctx = None
|
||||
if self._ssl_verify is False:
|
||||
ctx = ssl._create_unverified_context()
|
||||
|
||||
opener = build_opener(HTTPCookieProcessor())
|
||||
with opener.open(req, timeout=self.requestTimeout) as resp:
|
||||
self._sRealUrl = resp.geturl()
|
||||
self._sResponseHeader = str(resp.headers)
|
||||
data = resp.read()
|
||||
|
||||
if self.compression and 'gzip' in resp.headers.get('Content-Encoding', ''):
|
||||
try: data = gzip.decompress(data)
|
||||
except: pass
|
||||
|
||||
text = data.decode(encoding, errors='replace')
|
||||
|
||||
if self.__bRemoveBreakLines:
|
||||
text = re.sub(r'[\r\n]+', '\n', text)
|
||||
if self.__bRemoveNewLines:
|
||||
text = re.sub(r'\n+', ' ', text)
|
||||
|
||||
return text
|
||||
|
||||
except HTTPError as e:
|
||||
self._Status = str(e.code)
|
||||
return f'<!-- HTTPError {e.code} -->' if not self.ignoreErrors else ''
|
||||
except URLError as e:
|
||||
self._Status = str(e.reason)
|
||||
return f'<!-- URLError {e.reason} -->' if not self.ignoreErrors else ''
|
||||
except Exception as e:
|
||||
self._Status = str(e)
|
||||
return f'<!-- Error {e} -->' if not self.ignoreErrors else ''
|
||||
|
||||
def getJson(self):
|
||||
import json
|
||||
text = self.getHtml()
|
||||
if text.strip().startswith('<!--'): return {}
|
||||
try: return json.loads(text)
|
||||
except: return {}
|
||||
|
||||
|
||||
class cParser:
|
||||
@staticmethod
|
||||
def urlEncode(params):
|
||||
if isinstance(params, dict): return urlencode(params)
|
||||
return quote(str(params) if params else '')
|
||||
|
||||
@staticmethod
|
||||
def parse(source, pattern, flags=0):
|
||||
return re.findall(pattern, source, flags)
|
||||
|
||||
@staticmethod
|
||||
def replace(pattern, replacement, source, count=0):
|
||||
return re.sub(pattern, replacement, source, count)
|
||||
|
||||
@staticmethod
|
||||
def searchSingle(result, searchPattern, outputParam=None, source=None):
|
||||
text = source if source is not None else (str(result) if result else '')
|
||||
match = re.search(searchPattern, text, re.S)
|
||||
if not match: return None
|
||||
return match.group(outputParam) if outputParam and outputParam <= match.lastindex else match.group(0)
|
||||
|
||||
@staticmethod
|
||||
def htmlParse(html):
|
||||
if not html: return []
|
||||
text = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.S|re.I)
|
||||
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.S|re.I)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return [t.strip() for t in text.split(' ') if t.strip()]
|
||||
|
||||
|
||||
class ParameterHandler:
|
||||
def __init__(self, url=''):
|
||||
self._params = {}
|
||||
if url and '?' in url:
|
||||
_, qs = url.split('?', 1)
|
||||
for part in qs.split('&'):
|
||||
if '=' in part:
|
||||
k, v = part.split('=', 1)
|
||||
self._params[k] = v
|
||||
|
||||
def getValue(self, key='', default=''):
|
||||
return self._params.get(key, default)
|
||||
|
||||
def setParam(self, key, value):
|
||||
self._params[str(key)] = str(value)
|
||||
|
||||
def getAll(self):
|
||||
return self._params
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/pluginHandler.py
|
||||
@@ -0,0 +1,495 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Python 3
|
||||
|
||||
import time
|
||||
import xbmcgui
|
||||
import re
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
import traceback
|
||||
import ssl
|
||||
import certifi
|
||||
import socket
|
||||
import zlib
|
||||
|
||||
from resources.lib.config import cConfig
|
||||
from resources.lib.logger import logger
|
||||
from resources.lib.cache import cCache
|
||||
from resources.lib.tools import infoDialog
|
||||
from xbmcvfs import translatePath
|
||||
|
||||
from urllib.parse import quote, urlencode, urlparse, quote_plus
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import HTTPHandler, HTTPSHandler, Request, HTTPCookieProcessor, build_opener, urlopen, HTTPRedirectHandler
|
||||
from http.cookiejar import LWPCookieJar, Cookie
|
||||
from http.client import HTTPException
|
||||
from random import choice
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _doh_resolution(hostname, ip):
|
||||
"""DNS-Bypass via getaddrinfo-Patch: loest 'hostname' temporaer auf 'ip' auf.
|
||||
Anders als ein direkter IP-Connect bleibt das SSL-Zertifikat gueltig, weil die
|
||||
Verbindung weiterhin den echten Hostnamen kennt (SNI + Cert-Hostname stimmen).
|
||||
Patch ist eng gekapselt und wird im finally IMMER zurueckgesetzt."""
|
||||
if not ip:
|
||||
yield
|
||||
return
|
||||
_orig_getaddrinfo = socket.getaddrinfo
|
||||
def _patched(host, *args, **kwargs):
|
||||
if host == hostname:
|
||||
return _orig_getaddrinfo(ip, *args, **kwargs)
|
||||
return _orig_getaddrinfo(host, *args, **kwargs)
|
||||
socket.getaddrinfo = _patched
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
socket.getaddrinfo = _orig_getaddrinfo
|
||||
|
||||
|
||||
class RedirectFilter(HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, hdrs, newurl):
|
||||
if cConfig().getSetting('bypassDNSlock', 'false') != 'true':
|
||||
if 'notice.cuii' in newurl:
|
||||
xbmcgui.Dialog().ok(cConfig().getLocalizedString(30265), cConfig().getLocalizedString(30260) + '\n' + cConfig().getLocalizedString(30261))
|
||||
return None
|
||||
return HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, hdrs, newurl)
|
||||
|
||||
class cRequestHandler:
|
||||
# useful for e.g. tmdb request where multiple requests are made within a loop
|
||||
persistent_openers = {}
|
||||
|
||||
@staticmethod
|
||||
def RandomUA():
|
||||
# Random User Agents aktualisiert 01.07.2026 (Chrome 150 Stable ab 30.06.2026)
|
||||
FF_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0'
|
||||
OPERA_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 OPR/133.0.0.0'
|
||||
EDGE_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0'
|
||||
CHROME_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36'
|
||||
SAFARI_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15'
|
||||
|
||||
_User_Agents = [FF_USER_AGENT, OPERA_USER_AGENT, EDGE_USER_AGENT, CHROME_USER_AGENT, SAFARI_USER_AGENT]
|
||||
return choice(_User_Agents)
|
||||
|
||||
def __init__(self, sUrl, caching=True, ignoreErrors=False, method='GET', data=None, compression=True, jspost=False, ssl_verify=False):
|
||||
self._sUrl = self.__cleanupUrl(sUrl)
|
||||
self._sRealUrl = ''
|
||||
self._USER_AGENT = self.RandomUA()
|
||||
self._aParameters = {}
|
||||
self._headerEntries = {}
|
||||
self._profilePath = translatePath(cConfig().getAddonInfo('profile'))
|
||||
self._cachePath = ''
|
||||
self._cookiePath = ''
|
||||
self._Status = ''
|
||||
self._sResponseHeader = ''
|
||||
self._ssl_verify = ssl_verify
|
||||
self.ignoreDiscard(False)
|
||||
self.ignoreExpired(False)
|
||||
self.caching = caching
|
||||
self.method = method
|
||||
self.data = data
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.compression = compression
|
||||
self.jspost = jspost
|
||||
self.cacheTime = int(cConfig().getSetting('cacheTime', 1)) * 3600 # Stunden * 3600 = Sekunden
|
||||
self.requestTimeout = int(cConfig().getSetting('requestTimeout', 10))
|
||||
self.bypassDNSlock = (cConfig().getSetting('bypassDNSlock', 'false') == 'true')
|
||||
self.removeBreakLines(True)
|
||||
self.removeNewLines(True)
|
||||
self.__setDefaultHeader()
|
||||
self.__setCachePath()
|
||||
self.__setCookiePath()
|
||||
self.isMemoryCacheActive = (cConfig().getSetting('volatileHtmlCache', 'false') == 'true')
|
||||
if self.isMemoryCacheActive:
|
||||
self._memCache = cCache()
|
||||
|
||||
socket.setdefaulttimeout(self.requestTimeout)
|
||||
|
||||
def getStatus(self):
|
||||
return self._Status
|
||||
|
||||
def removeNewLines(self, bRemoveNewLines):
|
||||
self.__bRemoveNewLines = bRemoveNewLines
|
||||
|
||||
def removeBreakLines(self, bRemoveBreakLines):
|
||||
self.__bRemoveBreakLines = bRemoveBreakLines
|
||||
|
||||
def addHeaderEntry(self, sHeaderKey, sHeaderValue):
|
||||
self._headerEntries[sHeaderKey] = sHeaderValue
|
||||
|
||||
def getHeaderEntry(self, sHeaderKey):
|
||||
if sHeaderKey in self._headerEntries:
|
||||
return self._headerEntries[sHeaderKey]
|
||||
|
||||
def addParameters(self, key, value, Quote=False):
|
||||
self._aParameters[key] = value if not Quote else quote(str(value))
|
||||
|
||||
def getResponseHeader(self):
|
||||
return self._sResponseHeader
|
||||
|
||||
def getRealUrl(self):
|
||||
return self._sRealUrl
|
||||
|
||||
def getRequestUri(self):
|
||||
return self._sUrl + '?' + urlencode(self._aParameters)
|
||||
|
||||
def __setDefaultHeader(self):
|
||||
self.addHeaderEntry('User-Agent', self._USER_AGENT)
|
||||
self.addHeaderEntry('Accept-Language', 'de,en-US;q=0.7,en;q=0.3')
|
||||
self.addHeaderEntry('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8')
|
||||
if self.compression:
|
||||
self.addHeaderEntry('Accept-Encoding', 'gzip, deflate')
|
||||
self.addHeaderEntry('Connection', 'keep-alive')
|
||||
self.addHeaderEntry('Keep-Alive', 'timeout=5')
|
||||
|
||||
@staticmethod
|
||||
def __getDefaultHandler(ssl_verify):
|
||||
if ssl_verify:
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
ssl_context.check_hostname = True
|
||||
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
||||
return [HTTPSHandler(context=ssl_context)]
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
return [HTTPSHandler(context=ssl_context)]
|
||||
|
||||
@staticmethod
|
||||
def __cleanupUrl(url):
|
||||
if not isinstance(url, str):
|
||||
return ''
|
||||
if not isinstance(url, str):
|
||||
return ''
|
||||
#p = urlparse(url)
|
||||
#if p.query:
|
||||
# query = quote_plus(p.query).replace('%3D', '=').replace('%26', '&')
|
||||
# p = p._replace(query=p.query.replace(p.query, query))
|
||||
#else:
|
||||
# path = quote_plus(p.path).replace('%2F', '/').replace('%26', '&').replace('%3D', '=')
|
||||
# p = p._replace(path=p.path.replace(p.path, path))
|
||||
#return p.geturl()
|
||||
return url
|
||||
|
||||
def request(self):
|
||||
if self.caching and self.cacheTime > 0 and self.method == 'GET' and self.data is None:
|
||||
if self.isMemoryCacheActive:
|
||||
sContent = self.__readVolatileCache(self.getRequestUri(), self.cacheTime)
|
||||
else:
|
||||
sContent = self.__readPersistentCache(self.getRequestUri())
|
||||
if sContent:
|
||||
self._Status = '200'
|
||||
return sContent
|
||||
else:
|
||||
logger.info('-> [requestHandler]: read html for %s' % self.getRequestUri())
|
||||
|
||||
# DNS-Bypass global ueber Setting (kein per-Site-Flag mehr)
|
||||
if self.bypassDNSlock:
|
||||
ip_override = self.__doh_request(self._sUrl)
|
||||
else:
|
||||
ip_override = None
|
||||
_doh_host = urlparse(self._sUrl).hostname
|
||||
|
||||
cookieJar = LWPCookieJar(filename=self._cookiePath)
|
||||
try:
|
||||
cookieJar.load(ignore_discard=self.__bIgnoreDiscard, ignore_expires=self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
|
||||
domain = urlparse(self._sUrl).netloc
|
||||
if domain in cRequestHandler.persistent_openers:
|
||||
opener = cRequestHandler.persistent_openers[domain]
|
||||
else:
|
||||
handlers = self.__getDefaultHandler(self._ssl_verify)
|
||||
handlers += [HTTPHandler(), HTTPCookieProcessor(cookiejar=cookieJar), RedirectFilter()]
|
||||
opener = build_opener(*handlers)
|
||||
cRequestHandler.persistent_openers[domain] = opener
|
||||
|
||||
# Prepare parameters for GET/POST
|
||||
if self.method == 'POST':
|
||||
if self.data is not None:
|
||||
if isinstance(self.data, dict):
|
||||
# Default: form data
|
||||
sParameters = urlencode(self.data).encode()
|
||||
elif isinstance(self.data, str):
|
||||
sParameters = self.data.encode()
|
||||
else:
|
||||
sParameters = self.data
|
||||
else:
|
||||
sParameters = None
|
||||
else:
|
||||
sParameters = json.dumps(self._aParameters).encode() if self.jspost else urlencode(self._aParameters, True).encode()
|
||||
if len(sParameters) == 0:
|
||||
sParameters = None
|
||||
|
||||
oRequest = Request(self._sUrl, sParameters if sParameters and len(sParameters) > 0 else None)
|
||||
|
||||
for key, value in self._headerEntries.items():
|
||||
oRequest.add_header(key, value)
|
||||
|
||||
if self.method == 'POST' and 'Content-Type' not in self._headerEntries:
|
||||
oRequest.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
elif self.jspost:
|
||||
oRequest.add_header('Content-Type', 'application/json')
|
||||
|
||||
cookieJar.add_cookie_header(oRequest)
|
||||
|
||||
try:
|
||||
with _doh_resolution(_doh_host, ip_override):
|
||||
oResponse = opener.open(oRequest)
|
||||
except HTTPError as e:
|
||||
if e.code >= 400:
|
||||
self._Status = str(e.code)
|
||||
data = e.fp.read()
|
||||
if 'DDOS-GUARD' in str(data):
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
response = opener.open('https://check.ddos-guard.net/check.js')
|
||||
content = response.read().decode('utf-8', 'replace')
|
||||
url2 = re.findall("Image.*?'([^']+)'; new", content)
|
||||
url3 = urlparse(self._sUrl)
|
||||
url3 = '%s://%s/%s' % (url3.scheme, url3.netloc, url2[0])
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
opener.open(url3).read()
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
oResponse = opener.open(self._sUrl, sParameters if len(sParameters) > 0 else None)
|
||||
if not oResponse:
|
||||
logger.error(' -> [requestHandler]: Failed DDOS-GUARD active: ' + self._sUrl)
|
||||
return 'DDOS GUARD SCHUTZ'
|
||||
elif 'cloudflare' in str(e.headers):
|
||||
if not self.ignoreErrors:
|
||||
infoDialog(cConfig().getLocalizedString(30829), icon='WARNING', time=10000)
|
||||
logger.error(' -> [requestHandler]: Failed Cloudflare active: ' + self._sUrl)
|
||||
return 'CLOUDFLARE-SCHUTZ AKTIV' # Meldung geht als "e.doc" in die exception nach default.py
|
||||
else:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', cConfig().getLocalizedString(30259) + ' {0} {1}'.format(self._sUrl, str(e)))
|
||||
logger.error(' -> [requestHandler]: HTTPError ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'SEITE NICHT ERREICHBAR'
|
||||
else:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', cConfig().getLocalizedString(30259) + ' {0} {1}'.format(self._sUrl, str(e)))
|
||||
logger.error(' -> [requestHandler]: HTTPError ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'SEITE NICHT ERREICHBAR'
|
||||
except URLError as e:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', str(e.reason))
|
||||
logger.error(' -> [requestHandler]: URLError ' + str(e.reason) + ' Url: ' + self._sUrl)
|
||||
return 'URL FEHLER'
|
||||
except HTTPException as e:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', str(e))
|
||||
logger.error(' -> [requestHandler]: HTTPException ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'TIMEOUT'
|
||||
|
||||
self._sResponseHeader = oResponse.info()
|
||||
|
||||
content_encoding = self._sResponseHeader.get('Content-Encoding', '').lower()
|
||||
if content_encoding:
|
||||
raw_content = oResponse.read()
|
||||
if content_encoding == 'gzip':
|
||||
decompressed = zlib.decompress(raw_content, wbits=zlib.MAX_WBITS | 16)
|
||||
elif content_encoding == 'deflate':
|
||||
decompressed = zlib.decompress(raw_content, wbits=-zlib.MAX_WBITS)
|
||||
else:
|
||||
decompressed = raw_content
|
||||
sContent = decompressed.decode('utf-8', 'replace')
|
||||
else:
|
||||
sContent = oResponse.read().decode('utf-8', 'replace')
|
||||
|
||||
if 'lazingfast' in sContent:
|
||||
bf = cBF().resolve(self._sUrl, sContent, cookieJar, self._USER_AGENT, sParameters)
|
||||
if bf:
|
||||
sContent = bf
|
||||
else:
|
||||
logger.error(' -> [requestHandler]: Failed Blazingfast active: ' + self._sUrl)
|
||||
|
||||
try:
|
||||
cookieJar.save(ignore_discard=self.__bIgnoreDiscard, ignore_expires=self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(' -> [requestHandler]: Failed save cookie: %s' % e)
|
||||
|
||||
self._sRealUrl = oResponse.geturl()
|
||||
self._Status = oResponse.getcode() if self._sUrl == self._sRealUrl else '301'
|
||||
|
||||
if self.__bRemoveNewLines:
|
||||
sContent = sContent.replace('\n', '').replace('\r\t', '')
|
||||
if self.__bRemoveBreakLines:
|
||||
sContent = sContent.replace(' ', '')
|
||||
|
||||
if self.caching and self.cacheTime > 0 and self.method == 'GET' and self.data is None:
|
||||
if self.isMemoryCacheActive:
|
||||
self.__writeVolatileCache(self.getRequestUri(), sContent)
|
||||
else:
|
||||
self.__writePersistentCache(self.getRequestUri(), sContent)
|
||||
|
||||
return sContent
|
||||
|
||||
def __setCookiePath(self):
|
||||
cookieFile = os.path.join(self._profilePath, 'cookies')
|
||||
if not os.path.exists(cookieFile):
|
||||
os.makedirs(cookieFile)
|
||||
if 'dummy' not in self._sUrl:
|
||||
cookieFile = os.path.join(cookieFile, urlparse(self._sUrl).netloc.replace('.', '_') + '.txt')
|
||||
if not os.path.exists(cookieFile):
|
||||
open(cookieFile, 'w').close()
|
||||
self._cookiePath = cookieFile
|
||||
|
||||
def getCookie(self, sCookieName, sDomain=''):
|
||||
cookieJar = LWPCookieJar()
|
||||
try:
|
||||
cookieJar.load(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
for entry in cookieJar:
|
||||
if entry.name == sCookieName:
|
||||
if sDomain == '':
|
||||
return entry
|
||||
elif entry.domain == sDomain:
|
||||
return entry
|
||||
return False
|
||||
|
||||
def setCookie(self, oCookie):
|
||||
cookieJar = LWPCookieJar()
|
||||
try:
|
||||
cookieJar.load(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
cookieJar.set_cookie(oCookie)
|
||||
cookieJar.save(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def ignoreDiscard(self, bIgnoreDiscard):
|
||||
self.__bIgnoreDiscard = bIgnoreDiscard
|
||||
|
||||
def ignoreExpired(self, bIgnoreExpired):
|
||||
self.__bIgnoreExpired = bIgnoreExpired
|
||||
|
||||
def __doh_request(self, url, doh_server="https://cloudflare-dns.com/dns-query"):
|
||||
# Parse the URL
|
||||
parsed_url = urlparse(url)
|
||||
hostname = parsed_url.hostname
|
||||
# Bewusst NICHT gecacht: DDoS-Guard-Edges rotieren, eine im RAM-Cache
|
||||
# festgehaltene IP koennte innerhalb der TTL sterben -> immer frisch aufloesen.
|
||||
params = urlencode({"name": hostname, "type": "A"})
|
||||
doh_url = f"{doh_server}?{params}"
|
||||
req = Request(doh_url)
|
||||
req.add_header("Accept", "application/dns-json")
|
||||
|
||||
try:
|
||||
response = urlopen(req, timeout=5)
|
||||
response_text = response.read().decode("utf-8", "replace")
|
||||
dns_response = json.loads(response_text)
|
||||
if "Answer" not in dns_response:
|
||||
raise Exception("Invalid DNS response")
|
||||
# Ersten echten A-Record (type 1) aus der Answer-Liste nehmen statt stur [0]:
|
||||
# bei CNAME-Ketten kann die IP erst weiter hinten stehen, [0] waere dann der CNAME.
|
||||
ip_address = next((a["data"] for a in dns_response["Answer"] if a.get("type") == 1), None)
|
||||
if not ip_address:
|
||||
raise Exception("No A record in DNS answer")
|
||||
return ip_address
|
||||
except Exception as e:
|
||||
logger.error(' -> [requestHandler]: DNS query failed: %s' % e)
|
||||
return None
|
||||
|
||||
def __setCachePath(self):
|
||||
cache = os.path.join(self._profilePath, 'htmlcache')
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(cache)
|
||||
self._cachePath = cache
|
||||
|
||||
def __readPersistentCache(self, url):
|
||||
h = hashlib.md5(url.encode('utf8')).hexdigest()
|
||||
cacheFile = os.path.join(self._cachePath, h)
|
||||
fileAge = self.getFileAge(cacheFile)
|
||||
if 0 < fileAge < self.cacheTime:
|
||||
try:
|
||||
with open(cacheFile, 'rb') as f:
|
||||
content = f.read().decode('utf8')
|
||||
except Exception:
|
||||
logger.error(' -> [requestHandler]: Could not read Cache')
|
||||
if content:
|
||||
logger.info(' -> [requestHandler]: read html for %s from cache' % url)
|
||||
return content
|
||||
return None
|
||||
|
||||
def __writePersistentCache(self, url, content):
|
||||
try:
|
||||
h = hashlib.md5(url.encode('utf8')).hexdigest()
|
||||
with open(os.path.join(self._cachePath, h), 'wb') as f:
|
||||
f.write(content.encode('utf8'))
|
||||
except Exception:
|
||||
logger.error(' -> [requestHandler]: Could not write Cache')
|
||||
|
||||
def __writeVolatileCache(self, url, content):
|
||||
self._memCache.set(hashlib.md5(url.encode('utf8')).hexdigest(), content)
|
||||
|
||||
def __readVolatileCache(self, url, cache_time):
|
||||
entry = self._memCache.get(hashlib.md5(url.encode('utf8')).hexdigest(), cache_time)
|
||||
if entry:
|
||||
logger.info('-> [requestHandler]: read html for %s from cache' % url)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def getFileAge(cacheFile):
|
||||
try:
|
||||
return time.time() - os.stat(cacheFile).st_mtime
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def clearCache(self, silent=False):
|
||||
# clear volatile cache
|
||||
if self.isMemoryCacheActive:
|
||||
self._memCache.clear()
|
||||
cRequestHandler.persistent_openers.clear()
|
||||
|
||||
# clear persistent cache
|
||||
files = os.listdir(self._cachePath)
|
||||
for file in files:
|
||||
os.remove(os.path.join(self._cachePath, file))
|
||||
if not silent and files:
|
||||
infoDialog(cConfig().getLocalizedString(30405), icon='INFO')
|
||||
|
||||
|
||||
class cBF:
|
||||
def resolve(self, url, html, cookie_jar, user_agent, sParameters):
|
||||
page = urlparse(url).scheme + '://' + urlparse(url).netloc
|
||||
j = re.compile('<script[^>]src="([^"]+)').findall(html)
|
||||
if j:
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
opener.open(page + j[0])
|
||||
a = re.compile(r'xhr\.open\("GET","([^,]+)",').findall(html)
|
||||
if a:
|
||||
import random
|
||||
aespage = page + a[0].replace('" + ww +"', str(random.randint(700, 1500)))
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
html = opener.open(aespage).read().decode('utf-8', 'replace')
|
||||
cval = self.aes_decode(html)
|
||||
cdata = re.compile('cookie="([^="]+).*?domain[^>]=([^;]+)').findall(html)
|
||||
if cval and cdata:
|
||||
c = Cookie(version=0, name=cdata[0][0], value=cval, port=None, port_specified=False, domain=cdata[0][1], domain_specified=True, domain_initial_dot=False, path="/", path_specified=True, secure=False, expires=time.time() + 21600, discard=False, comment=None, comment_url=None, rest={})
|
||||
cookie_jar.set_cookie(c)
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
return opener.open(url, sParameters if len(sParameters) > 0 else None).read().decode('utf-8', 'replace')
|
||||
|
||||
@staticmethod
|
||||
def aes_decode(html):
|
||||
try:
|
||||
import pyaes
|
||||
keys = re.compile(r'toNumbers\("([^"]+)"').findall(html)
|
||||
if keys:
|
||||
from binascii import hexlify, unhexlify
|
||||
msg = unhexlify(keys[2])
|
||||
key = unhexlify(keys[0])
|
||||
iv = unhexlify(keys[1])
|
||||
decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
|
||||
plain_text = decrypter.feed(msg)
|
||||
plain_text += decrypter.feed()
|
||||
return hexlify(plain_text).decode()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/jsunpacker.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/logger.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/player.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tmdb.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tmdbinfo.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tools.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/trailer.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/updateManager.py
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/utils.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/vavoo/__init__.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/vavoo/auth.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/vavoo/vavoo.py
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/wrappers/meinecloud.py
|
||||
Reference in New Issue
Block a user