Initial commit: xStream Downloader

This commit is contained in:
2026-07-06 14:27:51 +02:00
commit 103a489985
139 changed files with 4153 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
*.egg-info/
dist/
build/
.coverage
htmlcov/
.env
venv/
.venv/
*.swp
*.swo
.DS_Store
+371
View File
@@ -0,0 +1,371 @@
import sys, os, time, json, logging, hashlib, threading, types
BASE = os.path.dirname(os.path.abspath(__file__))
# ─── Step 1: Set up mocks BEFORE importing anything from xstream ───────────────
# Descriptor that acts as both a base-class AND a factory.
# "class X(xbmc.Player)" needs a class. "xbmc.Player()" needs a callable.
class _PlayerFactory:
def __init__(self, cls, *args, **kwargs):
self._cls = cls
def __call__(self, *a, **k):
return self._cls(*a, **k)
def __get__(self, obj, objtype=None):
return self._cls
class MockPlayer(object):
def __init__(self, *args, **kwargs):
pass
def play(self, *a, **kw): pass
def isPlayingVideo(self): return False
def getPlayingFile(self): return None
def stop(self): pass
def pause(self): pass
def getTotalTime(self): return 0
def getTime(self): return 0, 0
class MockMonitor:
def __init__(self): pass
def abortRequested(self): return False
def waitForAbort(self, *a): time.sleep(a[0] if a else 1)
class MockPlayList:
def __init__(self): pass
def clear(self): pass
def add(self, *a): pass
class MockXbmcguiWindow:
def __init__(self): pass
def getProperty(self, k): return ''
def setProperty(self, k, v): pass
def clearProperty(self, k): pass
def clearProperties(self): pass
class MockXbmcgui:
Window = type('Window', (), {'10000': MockXbmcguiWindow()})()
NOTIFICATION_INFO = 'info'
NOTIFICATION_WARNING = 'warning'
NOTIFICATION_ERROR = 'error'
@staticmethod
def notification(*a, **k): pass
@staticmethod
def ok(*a, **k): return 1
@staticmethod
def select(*a, **k): return 0
@staticmethod
def textviewer(*a, **k): pass
Dialog = type('Dialog', (), {
'ok': staticmethod(lambda *a, **k: True),
'yesno': staticmethod(lambda *a, **k: True),
'select': staticmethod(lambda *a, **k: 0),
'browse': staticmethod(lambda *a, **k: ''),
'notification': staticmethod(lambda *a, **k: None),
'multiselect': staticmethod(lambda *a, **k: []),
})()
ListItem = type('ListItem', (), {'path': ''})
WindowDialog = type('WindowDialog', (), {'__init__': lambda s: None})
WindowXMLDialog = type('WindowXMLDialog', (), {
'__init__': lambda s, *a, **k: None,
'getProperty': lambda s, k: '',
'setProperty': lambda s, k, v: None,
})
ControlButton = type('ControlButton', (), {'__init__': lambda s, *a, **k: None})
ControlImage = type('ControlImage', (), {'__init__': lambda s, *a, **k: None})
ControlFadeLabel = type('ControlFadeLabel', (), {'__init__': lambda s, *a, **k: None})
ACTION_MOVE_DOWN = 4
ACTION_MOVE_UP = 3
ACTION_MOVE_LEFT = 1
ACTION_MOVE_RIGHT = 2
ACTION_NAV_BACK = 92
ACTION_SELECT_ITEM = 7
class MockXbmcplugin:
@staticmethod
def setResolvedUrl(*a): pass
class MockXbmcvfs:
@staticmethod
def translatePath(p): return p.replace('special://home', BASE)
@staticmethod
def mkdirs(p): os.makedirs(p, exist_ok=True)
@staticmethod
def exists(p): return os.path.exists(p)
@staticmethod
def delete(p): os.path.exists(p) and os.remove(p)
@staticmethod
def writeFile(p, d): open(p, 'w').write(d)
@staticmethod
def readFile(p): return open(p).read() if os.path.exists(p) else ''
class MockXbmcaddonAddon:
_settings = {}
def __init__(self, addon_id='plugin.video.xstream', *a, **k):
self._addon_id = addon_id
self._settings = {}
self._info = {
'plugin.video.xstream': {'name': 'xStream', 'id': 'plugin.video.xstream', 'version': '3.8.0', 'path': BASE, 'profile': os.path.join(BASE, '.profile')},
'script.module.resolveurl': {'name': 'ResolveURL', 'id': 'script.module.resolveurl', 'version': '5.1.190', 'path': BASE, 'profile': os.path.join(BASE, '.profile')},
'xbmc.addon': {'name': 'Kodi', 'id': 'xbmc.addon', 'version': '20.3', 'path': BASE, 'profile': os.path.join(BASE, '.profile')},
}
def getAddonInfo(self, k):
return self._info.get(self._addon_id, {'name': 'xStream', 'id': self._addon_id, 'version': '1.0.0', 'path': BASE, 'profile': os.path.join(BASE, '.profile')}).get(k, '')
def getSetting(self, k): return self._settings.get(k, '')
def setSetting(self, k, v): self._settings[k] = v
def getLocalizedString(self, k): return str(k)
def openSettings(self): pass
class MockXbmcaddon:
Addon = MockXbmcaddonAddon
# Create xbmc as a proper module-type object so "class X(xbmc.Player)" works
_xbmc_mod = types.ModuleType('xbmc')
_xbmc_mod.LOGDEBUG = 0
_xbmc_mod.LOGINFO = 1
_xbmc_mod.LOGWARNING = 2
_xbmc_mod.LOGERROR = 3
_xbmc_mod.LOGFATAL = 4
_xbmc_mod.getCondVisibility = lambda *a: False
_xbmc_mod.getInfoLabel = lambda *a: ''
_xbmc_mod.translatePath = lambda *a: os.path.join(BASE, '.xbmc')
_xbmc_mod.Player = _PlayerFactory(MockPlayer) # works as class AND as callable
_xbmc_mod.Monitor = MockMonitor
_xbmc_mod.PlayList = MockPlayList
_xbmc_mod.sleep = lambda ms: time.sleep(ms/1000)
_xbmc_mod.executebuiltin = lambda *a: None
_xbmc_mod.executeJSONRPC = lambda *a: '{}'
_xbmc_mod.log = lambda msg, level=None: None
_xbmc_mod.Keyboard = lambda *a, **k: type('Keyboard', (), {'doModal': lambda s: None, 'isConfirmed': lambda s: False, 'getText': lambda s: ''})()
_xbmc_mod.getSupportedMedia = lambda media: '|.mkv|.mp4|.avi|.mov|.wmv|.ts|.m4v|.flv|.webm|'
sys.modules['xbmc'] = _xbmc_mod
sys.modules['xbmcgui'] = MockXbmcgui
sys.modules['xbmcplugin'] = MockXbmcplugin
sys.modules['xbmcvfs'] = MockXbmcvfs
sys.modules['xbmcaddon'] = MockXbmcaddon
# kodi_six also imports xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs
# Provide them via a proxy module
import importlib.util
_kodi_six_lib = os.path.join(BASE, 'deps', 'kodi_six', 'libs')
sys.path.insert(0, _kodi_six_lib)
_spec = importlib.util.spec_from_file_location('kodi_six', os.path.join(_kodi_six_lib, 'kodi_six', '__init__.py'))
_real_ks = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_real_ks)
_kodi_six_mod = types.ModuleType('kodi_six')
_kodi_six_mod.PY2 = _real_ks.PY2
_kodi_six_mod.py2_encode = _real_ks.py2_encode
_kodi_six_mod.py2_decode = _real_ks.py2_decode
_kodi_six_mod.encode_decode = _real_ks.encode_decode
_kodi_six_mod.xbmc = _xbmc_mod
_kodi_six_mod.xbmcgui = MockXbmcgui
_kodi_six_mod.xbmcplugin = MockXbmcplugin
_kodi_six_mod.xbmcaddon = MockXbmcaddon
_kodi_six_mod.xbmcvfs = MockXbmcvfs
sys.modules['kodi_six'] = _kodi_six_mod
# ─── Step 2: Add dependency paths ──────────────────────────────────────────────
# Each script.module.X has its python package in lib/X/
_deps_map = {
'six': ('six', 'lib/six.py'),
'kodi_six': ('kodi_six', 'libs/kodi_six/__init__.py'),
'requests': ('requests', 'lib/requests/__init__.py'),
'resolveurl': ('resolveurl', 'lib/resolveurl/__init__.py'),
'pyaes': ('pyaes', 'lib/pyaes/__init__.py'),
}
for dep, (pkg_name, pkg_path) in _deps_map.items():
p = os.path.join(BASE, 'deps', dep)
if os.path.exists(p):
sys.path.insert(0, p)
lib_p = os.path.join(p, 'lib')
if os.path.exists(lib_p):
sys.path.insert(0, lib_p)
# ─── Step 3: Add xstream plugin path ──────────────────────────────────────────
sys.path.insert(0, os.path.join(BASE, 'xstream'))
# ─── Step 4: Now import xstream modules ────────────────────────────────────────
from resources.lib.config import cConfig
from resources.lib.logger import logger
from resources.lib.cache import cCache
from resources.lib.handler.ParameterHandler import ParameterHandler
from resources.lib.handler.requestHandler import cRequestHandler
from resources.lib.handler.pluginHandler import cPluginHandler
from resources.lib.tools import cParser, cUtil
from resources.lib.gui.guiElement import cGuiElement
from resources.lib.gui.gui import cGui
from resources.lib.gui.hoster import cHosterGui
# ─── Step 5: Flask app ────────────────────────────────────────────────────────
from flask import Flask, jsonify, request, send_file
from waitress import serve
app = Flask(__name__)
app.logger.setLevel(logging.WARNING)
# ─── Download Manager ─────────────────────────────────────────────────────────
class DownloadManager:
def __init__(self):
self.downloads = {}
self.lock = threading.Lock()
self.base_dir = os.path.join(BASE, 'downloads')
os.makedirs(self.base_dir, exist_ok=True)
def add(self, url, filename, resolved_url):
import requests as _req
with self.lock:
did = hashlib.md5(url.encode()).hexdigest()[:8]
self.downloads[did] = {
'id': did, 'title': filename, 'source_url': url, 'url': resolved_url,
'status': 'downloading', 'progress': 0, 'path': None, 'error': None,
'started': time.time(), 'finished': None,
}
t = threading.Thread(target=self._download, args=(did, resolved_url, filename), daemon=True)
t.start()
return did
def _download(self, did, url, filename):
import requests as _req
safe_name = "".join(c for c in filename if c not in '/\\:*?"<>|').strip() or 'video'
filepath = os.path.join(self.base_dir, safe_name)
try:
resp = _req.get(url, stream=True, timeout=60,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'})
resp.raise_for_status()
total = int(resp.headers.get('content-length', 0))
downloaded = 0
with open(filepath, 'wb') as f:
for chunk in resp.iter_content(chunk_size=65536):
if chunk:
f.write(chunk)
downloaded += len(chunk)
pct = int(100 * downloaded / total) if total else 0
with self.lock:
self.downloads[did]['progress'] = pct
with self.lock:
self.downloads[did].update(status='complete', progress=100, path=filepath, finished=time.time())
except Exception as e:
with self.lock:
self.downloads[did].update(status='error', error=str(e), finished=time.time())
def list(self):
with self.lock:
return list(self.downloads.values())
def get(self, did):
with self.lock:
return self.downloads.get(did)
dm = DownloadManager()
# ─── API Routes ────────────────────────────────────────────────────────────────
@app.route('/api/sites')
def api_sites():
try:
ph = cPluginHandler()
plugins = ph.getAvailablePlugins()
return jsonify(plugins)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/sites/<site_id>/entries')
def api_entries(site_id):
url = request.args.get('url', '')
search = request.args.get('search', '')
try:
mod = __import__(site_id, globals(), locals())
if search:
URL_SEARCH = getattr(mod, 'URL_SEARCH', None)
if URL_SEARCH:
url = URL_SEARCH % cParser.quotePlus(search)
func = getattr(mod, '_search', None) or getattr(mod, 'showSearch', None)
elif url:
func = getattr(mod, 'showEntries', None)
else:
func = getattr(mod, 'load', None)
if func:
result = func(url)
return jsonify({'entries': _entries_to_dict(result)})
return jsonify({'entries': []})
except Exception as e:
return jsonify({'error': str(e)}), 500
def _entries_to_dict(result):
"""Convert site function results to a list of entry dicts."""
if not result:
return []
entries = []
for item in result:
if isinstance(item, cGuiElement):
entries.append({
'title': item.getTitle(),
'thumb': item.getThumbnail(),
'url': getattr(item, '_cGuiElement__sMediaUrl', '') or '',
'year': item._sYear if hasattr(item, '_sYear') else '',
'quality': item.getQuality(),
'description': item.getDescription(),
})
elif isinstance(item, dict):
entries.append(item)
return entries
@app.route('/api/sites/<site_id>/hosters')
def api_hosters(site_id):
entry_url = request.args.get('entryUrl', '')
try:
mod = __import__(site_id, globals(), locals())
func = getattr(mod, 'showHosters', None)
if func:
result = func()
return jsonify({'hosters': result or []})
return jsonify({'hosters': []})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/resolve')
def api_resolve():
url = request.args.get('url', '')
title = request.args.get('title', 'video')
download = request.args.get('download', 'false') == 'true'
if not url:
return jsonify({'error': 'no url'}), 400
try:
import resolveurl as resolver
resolved = resolver.resolve(url)
if resolved:
if download:
did = dm.add(url, title, resolved)
return jsonify({'resolved': resolved, 'download_id': did})
return jsonify({'resolved': resolved})
return jsonify({'error': 'could not resolve'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/downloads')
def api_downloads():
return jsonify(dm.list())
@app.route('/api/downloads/<did>')
def api_download_get(did):
d = dm.get(did)
if not d:
return jsonify({'error': 'not found'}), 404
return jsonify(d)
@app.route('/api/downloads/<did>/file')
def api_download_file(did):
d = dm.get(did)
if not d or d['status'] != 'complete':
return jsonify({'error': 'not ready'}), 404
return send_file(d['path'], as_attachment=True)
@app.route('/')
def index():
return send_file('web/index.html')
if __name__ == '__main__':
print(f"xStream Downloader")
print(f" Base: {BASE}")
print(f" Listening on http://localhost:8765")
serve(app, host='0.0.0.0', port=8765)
View File
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../dependencies/script.module.kodi-six
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../dependencies/script.module.pyaes
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../dependencies/script.module.requests
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../dependencies/script.module.resolveurl
Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../dependencies/script.module.six
+182
View File
@@ -0,0 +1,182 @@
#!/bin/bash
BASE=/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
# 1. Fix tools.py - replace with fixed version
cat > "$BASE/xstream/resources/lib/tools.py" << 'ENDOFFILE'
# tools.py - Fixed: all string inputs normalized via _e()
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
def platform(): return 'Linux'
def changelog(): pass
def textBox(heading, announce): pass
def infoDialog(message, heading='', icon='', time=5000, sound=False): pass
class cParser:
@staticmethod
def _get_compiled_pattern(pattern, flags=0):
return re.compile(pattern, flags)
@staticmethod
def _replaceSpecialCharacters(s):
try:
for t in (('\\/', '/'), ('&amp;', '&'), ('\\u00c4', 'Ä'), ('\\u00e4', 'ä'),
('\\u00d6', 'Ö'), ('\\u00f6', 'ö'), ('\\u00dc', 'Ü'), ('\\u00fc', 'ü'),
('\\u00df', 'ß'), ('\\u2013', '-'), ('\\u00b2', '²'), ('\\u00b3', '³'),
('\\u00e9', 'é'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'),
('\\u00c9', 'É'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"),
('\\u0308', '̈'), ('\\u00e8', 'è'), ('#038;', ''), ('\\u00f8', 'ø'),
('', '/'), ('\\u00e1', 'á'), ('&#8211;', '-'), ('&#8220;', '"'), ('&#8222;', '"'),
('&#8217;', "'"), ('&#8230;', ''), ('\\u00bc', '¼'), ('\\u00bd', '½'), ('\\u00be', '¾'),
('\\u2153', ''), ('\\u002A', '*')):
s = s.replace(*t)
for h in (('\\/', '/'), ('&#x26;', '&'), ('&#039;', "'"), ("&#39;", "'"),
('&#xC4;', 'Ä'), ('&#xE4;', 'ä'), ('&#xD6;', 'Ö'), ('&#xF6;', 'ö'),
('&#xDC;', 'Ü'), ('&#xFC;', 'ü'), ('&#xDF;', 'ß'), ('&#xB2;', '²'),
('&#xDC;', '³'), ('&#xBC;', '¼'), ('&#xBD;', '½'), ('&#xBE;', '¾'),
('&#8531;', ''), ('&#8727;', '*')):
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(_e(sHtmlContent))
if matches:
if matches.lastindex is not None and matches.lastindex >= 1:
return True, cParser._replaceSpecialCharacters(matches.group(1))
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(_e(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, _e(sValue))
@staticmethod
def search(pattern, sValue, ignoreCase=True):
flags = re.IGNORECASE if ignoreCase else 0
return cParser._get_compiled_pattern(pattern, flags).search(_e(sValue))
@staticmethod
def escape(sValue): return re.escape(_e(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, _e(sValue))
@staticmethod
def unescape(text):
def fixup(m):
t = m.group(0)
if not t.endswith(';'): t += ';'
if t[:2] == '&#':
try:
if t[:3] == '&#x': return chr(int(t[3:-1], 16))
return chr(int(t[2:-1]))
except ValueError: pass
else:
try: return chr(name2codepoint[t[1:-1]])
except KeyError: pass
return t
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): return cUtil.removeHtmlTags(text if text else '')
@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) + 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
ENDOFFILE
echo "tools.py written"
# 2. Fix requestHandler.py
python3 << 'PYEOF'
with open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/xstream/resources/lib/handler/requestHandler.py') as f:
c = f.read()
old = ' self._sUrl = self.__cleanupUrl(sUrl)'
new = ' if not isinstance(sUrl, str): sUrl = ""\n self._sUrl = self.__cleanupUrl(sUrl)'
if old in c and 'not isinstance' not in c:
c = c.replace(old, new)
with open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/xstream/resources/lib/handler/requestHandler.py', 'w') as f:
f.write(c)
print('requestHandler fixed')
else:
print('requestHandler already fixed or pattern missing')
PYEOF
# 3. Kill and restart server
fuser -k 8765/tcp 2>/dev/null
sleep 1
cd /home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
python3 mini_app.py >> /tmp/server.log 2>&1 &
sleep 3
echo "=== Test ==="
curl -s http://127.0.0.1:8765/api/sites | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Sites OK: {len(d)}')" 2>&1
curl -s http://127.0.0.1:8765/api/sites/hdfilme/entries | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Entries: {len(d)}')" 2>&1
tail -8 /tmp/server.log
+177
View File
@@ -0,0 +1,177 @@
#!/bin/bash
BASE=/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
# 1. Write fixed tools.py
cat > "$BASE/xstream/resources/lib/tools.py" << 'HEREDOC'
# tools.py - Fixed: all string inputs normalized via _e()
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
def platform(): return 'Linux'
def changelog(): pass
def textBox(heading, announce): pass
def infoDialog(message, heading='', icon='', time=5000, sound=False): pass
class cParser:
@staticmethod
def _get_compiled_pattern(pattern, flags=0): return re.compile(pattern, flags)
@staticmethod
def _replaceSpecialCharacters(s):
try:
for t in (('\\/', '/'), ('&amp;', '&'), ('\\u00c4', 'A'), ('\\u00e4', 'a'),
('\\u00d6', 'O'), ('\\u00f6', 'o'), ('\\u00dc', 'U'), ('\\u00fc', 'u'),
('\\u00df', 's'), ('\\u2013', '-'), ('\\u00b2', '2'), ('\\u00b3', '3'),
('\\u00e9', 'e'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'),
('\\u00c9', 'E'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"),
('\\u0308', ''), ('\\u00e8', 'e'), ('#038;', ''), ('\\u00f8', 'o'),
('/', '/'), ('\\u00e1', 'a'), ('&#8211;', '-'), ('&#8220;', '"'), ('&#8222;', '"'),
('&#8217;', "'"), ('&#8230;', '...'), ('\\u00bc', '1/4'), ('\\u00bd', '1/2'), ('\\u00be', '3/4'),
('\\u2153', '1/3'), ('\\u002A', '*')):
s = s.replace(*t)
for h in (('\\/', '/'), ('&#x26;', '&'), ('&#039;', "'"), ("&#39;", "'"),
('&#xC4;', 'A'), ('&#xE4;', 'a'), ('&#xD6;', 'O'), ('&#xF6;', 'o'),
('&#xDC;', 'U'), ('&#xFC;', 'u'), ('&#xDF;', 's'), ('&#xB2;', '2'),
('&#xBC;', '1/4'), ('&#xBD;', '1/2'), ('&#xBE;', '3/4'),
('&#8531;', '1/3'), ('&#8727;', '*')):
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(_e(sHtmlContent))
if matches:
if matches.lastindex is not None and matches.lastindex >= 1:
return True, cParser._replaceSpecialCharacters(matches.group(1))
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(_e(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, _e(sValue))
@staticmethod
def search(pattern, sValue, ignoreCase=True):
flags = re.IGNORECASE if ignoreCase else 0
return cParser._get_compiled_pattern(pattern, flags).search(_e(sValue))
@staticmethod
def escape(sValue): return re.escape(_e(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, _e(sValue))
@staticmethod
def unescape(text):
def fixup(m):
t = m.group(0)
if not t.endswith(';'): t += ';'
if t[:2] == '&#':
try:
if t[:3] == '&#x': return chr(int(t[3:-1], 16))
return chr(int(t[2:-1]))
except ValueError: pass
else:
try: return chr(name2codepoint[t[1:-1]])
except KeyError: pass
return t
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): return cUtil.removeHtmlTags(text if text else '')
@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) + 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
HEREDOC
echo "tools.py written"
# 2. Fix requestHandler.py
python3 -c "
with open('$BASE/xstream/resources/lib/handler/requestHandler.py') as f: c = f.read()
old = ' self._sUrl = self.__cleanupUrl(sUrl)'
new = ' if not isinstance(sUrl, str): sUrl = \"\"\n self._sUrl = self.__cleanupUrl(sUrl)'
if old in c and 'not isinstance' not in c:
c = c.replace(old, new)
open('$BASE/xstream/resources/lib/handler/requestHandler.py','w').write(c)
print('requestHandler fixed')
else:
print('requestHandler already fixed or pattern missing')
"
# 3. Kill and restart server
fuser -k 8765/tcp 2>/dev/null
sleep 1
cd "$BASE"
python3 mini_app.py >> /tmp/server.log 2>&1 &
sleep 4
# 4. Test
echo "=== Tests ==="
curl -s http://127.0.0.1:8765/api/sites | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Sites: {len(d)}')" 2>&1
curl -s http://127.0.0.1:8765/api/sites/hdfilme/entries | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Entries: {len(d)}')" 2>&1
tail -10 /tmp/server.log
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
BASE=/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
# 1. Replace tools.py with fixed version
cp "$BASE/xstream/resources/lib/_tools_fixed.py" "$BASE/xstream/resources/lib/tools.py"
# 2. Fix requestHandler.py - guard sUrl
python3 -c "
with open('$BASE/xstream/resources/lib/handler/requestHandler.py') as f:
c = f.read()
old = ' self._sUrl = self.__cleanupUrl(sUrl)'
new = ' if not isinstance(sUrl, str): sUrl = \"\"\n self._sUrl = self.__cleanupUrl(sUrl)'
if old in c and 'not isinstance' not in c:
c = c.replace(old, new)
with open('$BASE/xstream/resources/lib/handler/requestHandler.py', 'w') as f:
f.write(c)
print('requestHandler fixed')
else:
print('requestHandler already fixed')
"
# 3. Fix mini_app.py - collecting_gui passthrough + openSettings
python3 -c "
with open('$BASE/mini_app.py') as f:
c = f.read()
# Fix collecting_gui=False -> collecting_gui
if 'site_mod.load(collecting_gui=False)' in c:
c = c.replace('site_mod.load(collecting_gui=False)', 'site_mod.load(collecting_gui=collecting_gui)')
print('Fixed collecting_gui passthrough')
else:
print('collecting_gui already fixed')
# Add openSettings to _MockAddon
if 'def openSettings' not in c:
c = c.replace(' def getAddonInfo(self, key):',
' def openSettings(self, *args, **kwargs):\n pass\n\n def getAddonInfo(self, key):')
print('Added openSettings')
with open('$BASE/mini_app.py', 'w') as f:
f.write(c)
"
# 4. Kill old server, start new
fuser -k 8765/tcp 2>/dev/null
sleep 1
cd "$BASE"
python3 mini_app.py > /tmp/server.log 2>&1 &
SERVER_PID=$!
sleep 3
# 5. Test
echo "=== Server PID: $SERVER_PID ==="
curl -s http://127.0.0.1:8765/api/sites | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Sites OK: {len(d)} sites')" 2>&1
curl -s http://127.0.0.1:8765/api/sites/hdfilme/entries | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Entries: {len(d)}')" 2>&1
echo "=== Log tail ==="
tail -5 /tmp/server.log
+711
View File
@@ -0,0 +1,711 @@
#!/usr/bin/env python3
"""
xStream Downloader — Flask App
Nutzt den echten xStream cRequestHandler für HTTP-Requests.
"""
import sys, os, re, io, gzip, hashlib, types, traceback, json, time
from pathlib import Path
from urllib.request import Request, urlopen, HTTPCookieProcessor, build_opener
from urllib.parse import quote, urlencode, urlparse
from urllib.error import HTTPError, URLError
import ssl
BASE = Path(__file__).parent.resolve()
sys.path.insert(0, str(BASE / 'deps'))
sys.path.insert(0, str(BASE))
# ═══════════════════════════════════════════════════════════════
# MOCK KODI MODULES (in sys.modules vorallen imports)
# ═══════════════════════════════════════════════════════════════
def _mkmod(name, **attrs):
class _M(types.ModuleType):
def __getattr__(self, n):
return lambda *a, **k: None
m = _M(name)
for k,v in attrs.items(): setattr(m, k, v)
return m
class _FakeAddon:
def __init__(self, *a, **kw):
self._id = a[0] if a else 'service.module.resolveurl'
def getAddonInfo(self, k):
return {'id': self._id, 'name': 'xStream', 'version': '1.0.0',
'icon': '', 'profile': '/tmp', 'author': '', 'description': ''}.get(k, '')
def openSettings(self, *a, **kw): pass
def getSetting(self, k): return ''
def setSetting(self, k, v): pass
def getLocalizedString(self, k): return str(k)
class _FakeDialog:
def ok(self, *a, **k): pass
def yesno(self, *a, **k): return True
def select(self, *a, **k): return 0
def notification(self, *a, **k): pass
def browse(self, *a, **k): return ''
class _FakeDialogBusy:
def create(self, *a, **k): pass
def update(self, *a, **k): pass
def close(self): pass
def isFinished(self): return True
class _FakeListItem:
def __init__(self, *a, **k):
self._title = str(a[0]) if a else ''
def __getattr__(self, name):
return lambda *a, **k: None
def setInfo(self, *a, **k): pass
def setThumbnail(self, *a, **k): pass
def setIsFolder(self, *a, **k): pass
def getLabel(self): return self._title
def addContextMenuItems(self, *a, **k): pass
class _FakeWin:
def __init__(self, *a, **k): pass
def doModal(self, *a, **k): pass
def close(self, *a, **k): pass
def clearProperty(self, *a, **k): pass
def setProperty(self, *a, **k): pass
def getProperty(self, *a, **k): return ''
class _FakePlayer:
def __init__(self): pass
def play(self, *a, **k): pass
def isPlaying(self): return False
def isPlayingVideo(self): return False
def getPlayingFile(self): return ''
def stop(self): pass
class _FakeMonitor:
def __init__(self, *a, **k): pass
def abortRequested(self): return False
def waitForAbort(self, t=None): return False
sys.modules['xbmcaddon'] = _mkmod('xbmcaddon', Addon=_FakeAddon)
sys.modules['xbmcgui'] = _mkmod('xbmcgui',
Dialog=_FakeDialog, DialogBusy=_FakeDialogBusy,
Window=_FakeWin, WindowDialog=_FakeWin, WindowXMLDialog=_FakeWin,
ControlButton=_FakeDialog, ControlImage=_FakeDialog, ControlFadeLabel=_FakeDialog,
ListItem=_FakeListItem,
ACTION_MOUSE_LEFT=100, ACTION_PREVIOUS_MENU=10, ACTION_NAV_BACK=92,
)
sys.modules['xbmcvfs'] = _mkmod('xbmcvfs',
translatePath=lambda s: '/tmp',
exists=lambda s: False, mkdir=lambda s: True, mkdirs=lambda s: True,
delete=lambda s: True, rename=lambda s,d: True, listdir=lambda s: ([],[]),
File=type('F',(),{'__enter__':lambda s:s,'__exit__':lambda s,*a:None,
'read':lambda s,n=-1:b'','write':lambda s,b:len(b),'close':lambda s:None})(),
)
sys.modules['xbmc'] = _mkmod('xbmc',
translatePath=lambda s: '/tmp', log=lambda s,*a: None,
executeJSONRPC=lambda s: '{}',
Player=_FakePlayer, Monitor=_FakeMonitor,
getCondVisibility=lambda s: 0, sleep=lambda s: None,
getInfoLabel=lambda s: '', executebuiltin=lambda s: None,
LOGDEBUG=0, LOGINFO=1, LOGWARNING=2, LOGERROR=3, LOGNONE=4, LOGFATAL=5,
)
sys.modules['xbmcplugin'] = _mkmod('xbmcplugin')
sys.modules['xbmcweb'] = _mkmod('xbmcweb')
# ═══════════════════════════════════════════════════════════════
# ECHTER cRequestHandler (nur Standard-Lib + minimale Anpassungen)
# ═══════════════════════════════════════════════════════════════
class _FakeConfig:
"""cConfig-Ersatz: gibt sichere Defaults zurück."""
_settings = {
'cacheTime': '1', 'requestTimeout': '10',
'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:
"""cCache-Ersatz: kein Memory-Cache, gibt immer None zurück."""
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
# ── Standalone cRequestHandler ──────────────────────────────────
class cRequestHandler:
"""Echter HTTP-Client, basiert auf dem xStream requestHandler."""
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.ignoreDiscard(False)
self.ignoreExpired(False)
self.caching = caching
self.method = method
self.data = data
self.ignoreErrors = ignoreErrors
self.compression = compression
self.jspost = jspost
self.requestTimeout = 15
self.removeBreakLines(True)
self.removeNewLines(True)
self.__setDefaultHeader()
self.isMemoryCacheActive = False
self._memCache = None
self.__bRemoveNewLines = True
self.__bRemoveBreakLines = True
def __setDefaultHeader(self):
self.addHeaderEntry('User-Agent', self._USER_AGENT)
self.addHeaderEntry('Accept', '*/*')
self.addHeaderEntry('Accept-Language', 'en-US,en;q=0.5')
self.addHeaderEntry('Accept-Encoding', 'gzip, deflate' if self.compression else 'identity')
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'):
"""Führt den Request aus und gibt den HTML-Body zurück."""
if not self._sUrl or self._sUrl == 'False':
return ''
try:
url = self._sUrl
if self._aParameters and self.method == 'GET':
url = url + ('&' if '?' in url else '?') + urlencode(self._aParameters)
req = Request(url, data=self.data, method=self.method)
for k, v in self._headerEntries.items():
req.add_header(k, v)
ctx = ssl.create_default_context() if not self._ssl_verify else None
if self._ssl_verify is False:
ctx = ssl._create_unverified_context()
opener = build_opener(HTTPCookieProcessor())
if ctx:
opener.handlers = [h for h in opener.handlers
if not isinstance(h, ssl.SSLHandler)] + [HTTPSHandler(context=ctx)]
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)
if self.ignoreErrors: return ''
return f'<!-- HTTPError {e.code} -->'
except URLError as e:
self._Status = str(e.reason)
if self.ignoreErrors: return ''
return f'<!-- URLError {e.reason} -->'
except Exception as e:
self._Status = str(e)
if self.ignoreErrors: return ''
return f'<!-- Error {e} -->'
def getJson(self):
import json
text = self.getHtml()
if text.strip().startswith('<!--'): return {}
try: return json.loads(text)
except: return {}
# ═══════════════════════════════════════════════════════════════
# ECHTES cParser (nur Standard-Lib)
# ═══════════════════════════════════════════════════════════════
class cParser:
@staticmethod
def urlEncode(params):
if isinstance(params, dict):
return urlencode(params)
return quote(str(params) if params else '')
@staticmethod
def parseDouble(source, pattern1, pattern2):
match1 = re.search(pattern1, source, re.S)
if not match1: return []
match2 = re.search(pattern2, source[match1.end():], re.S)
if not match2: return []
return [(match1.group(0), match2.group(0))]
@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):
if source is not None:
match = re.search(searchPattern, source, re.S)
elif result:
match = re.search(searchPattern, str(result), re.S)
else:
return None
if not match: return None
return match.group(1) if outputParam and outputParam <= match.lastindex else match.group(0)
@staticmethod
def htmlParse(html):
"""Extrahiert Text aus HTML-Tags."""
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 text.split(' ')
# ═══════════════════════════════════════════════════════════════
# ECHTES ParameterHandler
# ═══════════════════════════════════════════════════════════════
class ParameterHandler:
def __init__(self, url=''):
self._params = {}
if url:
if '?' in url:
_, qs = url.split('?', 1)
for part in qs.split('&'):
if '=' in part:
k, v = part.split('=', 1)
self._params[quote(k, safe='')] = quote(v, safe='')
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
# ═══════════════════════════════════════════════════════════════
# GUI COLLECTOR (sammelt addFolder-Aufrufe)
# ═══════════════════════════════════════════════════════════════
class _CollectingGui:
"""Ersetzt cGui und sammelt alle addFolder()-Einträge."""
def __init__(self):
self.entries = []
self.params_stack = [{}]
def addFolder(self, oGuiElement, params=None, bIsFolder=True, iTotal=0, isHoster=False, **kw):
# params kann ParameterHandler, dict, oder string sein
url = ''
name = ''
img = ''
fan = ''
desc = ''
if hasattr(oGuiElement, 'getTitle'):
name = oGuiElement.getTitle() or ''
if hasattr(oGuiElement, 'getMediaUrl'):
url = oGuiElement.getMediaUrl() or ''
elif hasattr(oGuiElement, '_sUrl'):
url = oGuiElement._sUrl or ''
if hasattr(oGuiElement, 'getThumbnailImage'):
img = oGuiElement.getThumbnailImage() or ''
if hasattr(oGuiElement, 'getFanart'):
fan = oGuiElement.getFanart() or ''
elif isinstance(oGuiElement, str):
name = oGuiElement
# URL aus params extrahieren
if not url and params:
if hasattr(params, 'getValue'):
url = params.getValue('entryUrl') or params.getValue('sUrl') or ''
elif isinstance(params, dict):
url = params.get('entryUrl') or params.get('sUrl') or ''
if url and not url.startswith(('http', '/')):
url = ''
if name and url:
self.entries.append({
'name': name.strip(),
'url': url,
'img': img,
'fan': fan,
'desc': desc,
})
def showInfo(self, *a, **k): pass
def showError(self, *a, **k): pass
def showKeyBoard(self, *a, **k): return ''
def showNofication(self, *a, **k): pass
def showLanguage(self, *a, **k): pass
def updateDirectory(self, *a, **k): pass
def setEndOfDirectory(self, *a, **k): pass
def setView(self, *a, **k): pass
def createListItem(self, *a, **k): return None
def getControl(self, *a, **k): return None
def doModal(self, *a, **k): pass
def close(self): pass
# ═══════════════════════════════════════════════════════════════
# RESOLVEURL LAZY LOADER
# ═══════════════════════════════════════════════════════════════
_resolver = None
def get_resolver():
global _resolver
if _resolver is None:
import resolveurl
_resolver = resolveurl
return _resolver
# ═══════════════════════════════════════════════════════════════
# SITE SCANNER
# ═══════════════════════════════════════════════════════════════
def scan_sites():
sites_dir = BASE / 'xstream' / 'sites'
result = []
for path in sorted(sites_dir.glob('*.py')):
if path.name.startswith('_'): continue
try:
txt = path.read_text(errors='ignore')
except: continue
ident_m = re.search(r"SITE_IDENTIFIER\s*=\s*['\"]([^'\"]+)['\"]", txt)
name_m = re.search(r"SITE_NAME\s*=\s*['\"]([^'\"]+)['\"]", txt)
url_m = re.search(r"URL_MAIN\s*=\s*['\"]([^'\"]+)['\"]", txt)
ident = ident_m.group(1) if ident_m else path.stem
name = name_m.group(1) if name_m else ident
url = url_m.group(1) if url_m else ''
has_search = bool(re.search(r'def\s+search\s*\(', txt))
result.append({'identifier': ident, 'name': name, 'url': url, 'hasSearch': has_search})
return result
# ═══════════════════════════════════════════════════════════════
# SITE EXEC IN SUB-PROCESS
# ═══════════════════════════════════════════════════════════════
import multiprocessing
def _run_site_in_subprocess(func_name, ident, url_arg='', timeout=20):
def worker(pipe, func_name, ident, url_arg):
# Mocks in diesem subprocess
sys.modules['xbmcaddon'] = _mkmod('xbmcaddon', Addon=_FakeAddon)
sys.modules['xbmcgui'] = _mkmod('xbmcgui',
Dialog=_FakeDialog, DialogBusy=_FakeDialogBusy,
Window=_FakeWin, WindowDialog=_FakeWin, WindowXMLDialog=_FakeWin,
ListItem=_FakeListItem,
ACTION_MOUSE_LEFT=100, ACTION_PREVIOUS_MENU=10, ACTION_NAV_BACK=92,
)
sys.modules['xbmcvfs'] = _mkmod('xbmcvfs', translatePath=lambda s: '/tmp',
exists=lambda s: False, mkdir=lambda s: True, mkdirs=lambda s: True,
delete=lambda s: True, rename=lambda s,d: True,
File=type('F',(),{'__enter__':lambda s:s,'__exit__':lambda s,*a:None,
'read':lambda s,n=-1:b'','write':lambda s,b:len(b),'close':lambda s:None})())
sys.modules['xbmc'] = _mkmod('xbmc',
translatePath=lambda s: '/tmp', log=lambda s,*a: None,
executeJSONRPC=lambda s: '{}',
Player=_FakePlayer, Monitor=_FakeMonitor,
getCondVisibility=lambda s: 0, sleep=lambda s: None,
getInfoLabel=lambda s: '', executebuiltin=lambda s: None,
LOGDEBUG=0, LOGINFO=1, LOGWARNING=2, LOGERROR=3, LOGNONE=4,
)
sys.modules['xbmcplugin'] = _mkmod('xbmcplugin')
sys.modules['xbmcweb'] = _mkmod('xbmcweb')
# Die echten Handler einsetzen
sys.modules['cRequestHandler'] = sys.modules.get('cRequestHandler', None)
sys.modules['cParser'] = sys.modules.get('cParser', None)
BASE_PATH = Path('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader')
site_path = BASE_PATH / 'xstream' / 'sites' / f'{ident}.py'
xstream_base = BASE_PATH / 'xstream'
handler_base = xstream_base / 'resources' / 'lib' / 'handler'
if not site_path.exists():
pipe.send({'error': 'site not found'}); pipe.close(); return
gui = _CollectingGui()
params = ParameterHandler(url_arg) if url_arg else ParameterHandler()
mod = {
'__name__': ident, '__file__': str(site_path),
'sys': sys, 'os': os, 'time': time, 're': re,
'xbmc': sys.modules['xbmc'], 'xbmcgui': sys.modules['xbmcgui'],
'xbmcaddon': sys.modules['xbmcaddon'], 'xbmcvfs': sys.modules['xbmcvfs'],
'xbmcplugin': sys.modules['xbmcplugin'],
'cGui': gui, 'ParameterHandler': ParameterHandler,
'cConfig': _FakeConfig, 'cCache': _FakeCache,
'logger': _FakeLogger(), 'infoDialog': _infoDialog,
'showEntries': None, 'showHosters': None, 'search': None,
}
old_path = list(sys.path)
# WICHTIG: handler_base VOR xstream_base, damit unser standalone cRequestHandler
# VOR dem original requestHandler.py gefunden wird
sys.path[:0] = [str(handler_base), str(xstream_base), str(xstream_base / 'resources' / 'lib')]
try:
code = site_path.read_text(errors='ignore')
exec(compile(code, str(site_path), 'exec'), mod)
func = mod.get(func_name)
if not func:
pipe.send({'error': f'{func_name} not found'}); pipe.close(); return
# Prüfe Signatur
import inspect
try:
sig = inspect.signature(func)
wants_url = any(p.name in ('entryUrl', 'url', 'sUrl', 'pageUrl') for p in sig.parameters.values())
except (ValueError, TypeError):
wants_url = bool(url_arg)
if wants_url and url_arg:
result = func(url_arg)
else:
result = func()
if isinstance(result, list):
pipe.send({'entries': result})
elif hasattr(result, 'entries'):
pipe.send({'entries': result.entries})
else:
pipe.send({'entries': gui.entries})
except Exception as e:
pipe.send({'error': str(e), 'trace': traceback.format_exc()})
finally:
sys.path[:] = old_path
pipe.close()
ctx = multiprocessing.get_context('fork')
parent_conn, child_conn = ctx.Pipe()
p = ctx.Process(target=worker, args=(child_conn, func_name, ident, url_arg))
p.daemon = True
p.start()
p.join(timeout=timeout)
if p.is_alive():
p.terminate(); p.join(timeout=2)
return {'error': 'timeout'}
if parent_conn.poll(timeout=2):
return parent_conn.recv()
return {'error': 'no response'}
# ═══════════════════════════════════════════════════════════════
# FLASK APP
# ═══════════════════════════════════════════════════════════════
from flask import Flask, send_from_directory, jsonify, request, send_file
import threading
app = Flask(__name__, static_folder='web')
DOWNLOADS = {}
dl_lock = threading.Lock()
def _resolve_real(url):
try:
r = get_resolver()
h = r.resolve(url)
return (h, None) if h else (None, 'resolve failed')
except Exception as e:
return (None, str(e))
def _start_download(url, title, dl_id):
import requests
def run():
try:
final_url, err = _resolve_real(url)
if err:
DOWNLOADS[dl_id]['error'] = err; DOWNLOADS[dl_id]['status'] = 'error'
return
r = requests.get(final_url, stream=True, timeout=30,
headers={'User-Agent': 'Mozilla/5.0'})
r.raise_for_status()
total = int(r.headers.get('Content-Length', 0))
dl_path = BASE / 'downloads' / f'{dl_id}.mp4'
dl_path.parent.mkdir(exist_ok=True)
downloaded = 0
with open(dl_path, 'wb') as f:
for chunk in r.iter_content(65536):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total:
DOWNLOADS[dl_id]['progress'] = downloaded / total
DOWNLOADS[dl_id]['status'] = 'done'
DOWNLOADS[dl_id]['path'] = str(dl_path)
DOWNLOADS[dl_id]['progress'] = 1.0
except Exception as e:
DOWNLOADS[dl_id]['error'] = str(e)
DOWNLOADS[dl_id]['status'] = 'error'
threading.Thread(target=run, daemon=True).start()
@app.route('/')
def index():
return send_from_directory('web', 'index.html')
@app.route('/<path:fn>')
def static_files(fn):
return send_from_directory('web', fn)
@app.route('/api/sites')
def api_sites():
return jsonify(scan_sites())
@app.route('/api/sites/<ident>')
def api_site_info(ident):
for s in scan_sites():
if s['identifier'] == ident:
return jsonify(s)
return jsonify({'error': 'site not found'}), 404
@app.route('/api/sites/<ident>/entries')
def api_entries(ident):
url = request.args.get('url', '')
DEFAULTS = {
'hdfilme': 'https://hdfilme1.co/kinofilme-online/',
'fhdfilme': 'https://fhdfilme.stream',
'aniworld': 'https://aniworld.to',
'serienstream': 'https://serienstream.to',
'burningseries': 'https://burningseries.cx',
'filmpalast': 'https://filmpalast.to',
'kinoger': 'https://kinoger.com',
'kkiste': 'https://kkiste.movie',
'netzkino': 'https://netzkino.com',
'moflix-stream': 'https://moflix-stream.com',
'topstreamfilm': 'https://topstreamfilm.com',
'streamcloud': 'https://streamcloud.store',
'megakino': 'https://megakino.com',
'einschalten': 'https://einschalten.io',
'internetarchive': 'https://archive.org',
'animetoast': 'https://animetoast.org',
}
if not url and ident in DEFAULTS:
url = DEFAULTS[ident]
for func in ('showEntries', 'showNeues', 'showLatest', 'showNewest'):
result = _run_site_in_subprocess(func, ident, url)
err = result.get('error', '')
skip = ('not found', 'timeout', 'site not found', 'no response')
if err and any(s in err for s in skip):
continue
break
if 'error' in result and len(result) == 1:
return jsonify(result), 500
return jsonify(result)
@app.route('/api/sites/<ident>/hosters')
def api_hosters(ident):
url = request.args.get('url', '')
if not url:
return jsonify({'error': 'url required'}), 400
result = _run_site_in_subprocess('showHosters', ident, url)
if 'error' in result and len(result) == 1:
return jsonify(result), 500
return jsonify(result)
@app.route('/api/resolve')
def api_resolve():
url = request.args.get('url', '')
if not url:
return jsonify({'error': 'url required'}), 400
final_url, err = _resolve_real(url)
if err:
return jsonify({'error': err}), 400
return jsonify({'url': final_url})
@app.route('/api/download')
def api_download():
url = request.args.get('url', '')
title = request.args.get('title', 'download')
if not url:
return jsonify({'error': 'url required'}), 400
dl_id = hashlib.md5(f'{url}{title}'.encode()).hexdigest()[:12]
with dl_lock:
if dl_id not in DOWNLOADS:
DOWNLOADS[dl_id] = {'id': dl_id, 'title': title, 'url': url,
'status': 'starting', 'progress': 0.0,
'path': None, 'error': None}
_start_download(url, title, dl_id)
dl = dict(DOWNLOADS[dl_id])
return jsonify(dl)
@app.route('/api/downloads')
def api_downloads():
with dl_lock:
return jsonify(list(DOWNLOADS.values()))
@app.route('/api/downloads/<dl_id>')
def api_download_info(dl_id):
with dl_lock:
if dl_id not in DOWNLOADS:
return jsonify({'error': 'not found'}), 404
return jsonify(dict(DOWNLOADS[dl_id]))
@app.route('/api/downloads/<dl_id>/file')
def api_download_file(dl_id):
with dl_lock:
dl = DOWNLOADS.get(dl_id, {})
path = dl.get('path')
if not path or not os.path.exists(path):
return jsonify({'error': 'not ready'}), 404
return send_file(path, as_attachment=True, download_name=os.path.basename(path))
if __name__ == '__main__':
(BASE / 'downloads').mkdir(exist_ok=True)
print(f'xStream Downloader — http://localhost:8765')
print(f'Sites: {len(scan_sites())}')
app.run(host='0.0.0.0', port=8765, debug=False, threaded=True, use_reloader=False)
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Fix: replace mock classes with lambda-callable factories in _run_site_in_subprocess"""
import re
path = '/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/mini_app.py'
with open(path) as f:
c = f.read()
# The old mock classes block in worker()
old = """ class _A:
def __init__(self, *a, **kw): self._id = a[0] if a else ''
def getAddonInfo(self, k): return {'id': self._id, 'name': 'xStream', 'version': '1.0.0', 'icon': '', 'profile': '/tmp'}.get(k, '')
def openSettings(self, *a, **kw): pass
def getSetting(self, k): return ''
def setSetting(self, k, v): pass
def getLocalizedString(self, k): return str(k)
class _D:
def ok(self, *a, **kw): pass
def yesno(self, *a, **kw): return True
def select(self, *a, **kw): return 0
def notification(self, *a, **kw): pass
def browse(self, *a, **kw): return ''
def _m(n, **a):
m = types.ModuleType(n)
for k, v in a.items(): setattr(m, k, v)
return m
sys.modules['xbmcaddon'] = _m('xbmcaddon', Addon=_A)
sys.modules['xbmcgui'] = _m('xbmcgui', Dialog=_D())"""
new = """ def _m(n, **a):
m = types.ModuleType(n)
for k, v in a.items(): setattr(m, k, v)
return m
def _A(*a, **kw):
inst = types.SimpleNamespace()
inst._id = a[0] if a else ''
inst.getAddonInfo = lambda k: {'id': inst._id, 'name': 'xStream', 'version': '1.0.0', 'icon': '', 'profile': '/tmp'}.get(k, '')
inst.openSettings = lambda *a, **kw: None
inst.getSetting = lambda k: ''
inst.setSetting = lambda k, v: None
inst.getLocalizedString = lambda k: str(k)
return inst
def _D(*a, **kw):
inst = types.SimpleNamespace()
inst.ok = lambda *a, **kw: None
inst.yesno = lambda *a, **kw: True
inst.select = lambda *a, **kw: 0
inst.notification = lambda *a, **kw: None
inst.browse = lambda *a, **kw: ''
return inst
def _PD(*a, **kw):
inst = types.SimpleNamespace()
inst.create = lambda *a, **k: None
inst.update = lambda *a, **k: None
inst.close = lambda: None
inst.isFinished = lambda: True
return inst
sys.modules['xbmcaddon'] = _m('xbmcaddon', Addon=_A)
sys.modules['xbmcgui'] = _m('xbmcgui',
Dialog=_D, DialogBusy=_PD,
WindowDialog=_D, WindowXMLDialog=_D,
ControlButton=_D, ControlImage=_D, ControlFadeLabel=_D,
ACTION_MOUSE_LEFT=100, ACTION_PREVIOUS_MENU=10,
ACTION_NAV_BACK=92,
)"""
if old in c:
c = c.replace(old, new)
open(path, 'w').write(c)
print("Patched OK")
else:
print("Pattern not found — already patched or structure changed")
# Show what's there
idx = c.find("class _A:")
if idx >= 0:
print(c[idx:idx+500])
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Patch mini_app.py: add default site URLs"""
import re
with open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/mini_app.py') as f:
c = f.read()
old = " url = request.args.get('url', '')\n result = _run_site_in_subprocess('showEntries', ident, url)"
new = """ url = request.args.get('url', '')
DEFAULTS = {
'hdfilme': 'https://hdfilme.stream/filme',
'fhdfilme': 'https://fhdfilme.stream',
'aniworld': 'https://aniworld.to',
'serienstream': 'https://serienstream.to',
'burningseries': 'https://burningseries.cx',
'filmpalast': 'https://filmpalast.to',
'kinoger': 'https://kinoger.com',
'kkiste': 'https://kkiste.movie',
'netzkino': 'https://netzkino.com',
'moflix-stream': 'https://moflix-stream.com',
'topstreamfilm': 'https://topstreamfilm.com',
'streamcloud': 'https://streamcloud.store',
'megakino': 'https://megakino.com',
'einschalten': 'https://einschalten.io',
'internetarchive': 'https://archive.org',
'animetoast': 'https://animetoast.org',
'tmdb_browser': '',
'api_sites': '',
}
if not url and ident in DEFAULTS:
url = DEFAULTS[ident]
result = _run_site_in_subprocess('showEntries', ident, url)"""
if old in c and 'DEFAULTS' not in c:
c = c.replace(old, new)
open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/mini_app.py', 'w').write(c)
print("Patched OK")
else:
print("Already patched or pattern not found")
# Show what's there
idx = c.find("url = request.args.get('url'")
if idx >= 0:
print("Current code around line:")
print(c[idx:idx+200])
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Patch: add xbmc.Monitor, fix xbmc.Player, add xbmcvfs.File to _XBMC block"""
import re
path = '/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/mini_app.py'
with open(path) as f:
c = f.read()
old = """ _XBMC.Player = type('Player', (), {'play': lambda s, *a, **k: None, 'isPlayingVideo': lambda s: False})()
sys.modules['xbmc'] = _XBMC"""
new = """ def _Player(*a, **kw):
inst = types.SimpleNamespace()
inst.play = lambda *a, **k: None
inst.isPlayingVideo = lambda: False
inst.isPlaying = lambda: False
inst.getPlayingFile = lambda: ''
inst.stop = lambda: None
return inst
def _Monitor(*a, **kw):
inst = types.SimpleNamespace()
inst.abortRequested = lambda: False
inst.waitForAbort = lambda t=None: False
return inst
_XBMC.Player = _Player
_XBMC.Monitor = _Monitor
sys.modules['xbmc'] = _XBMC"""
if old in c and 'Monitor' not in c:
c = c.replace(old, new)
open(path, 'w').write(c)
print("Patched OK")
else:
print("Already patched or pattern not found")
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Test the full entry loading flow"""
import sys, os
BASE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE)
sys.path.insert(0, os.path.join(BASE, 'xstream'))
sys.path.insert(0, os.path.join(BASE, 'deps'))
# Minimal mocks like mini_app.py
import types
_MockAddon = type('_MockAddon', (), {
'__init__': lambda s, *a, **kw: None,
'getAddonInfo': lambda s, k: '1.0.0',
'openSettings': lambda s: None,
'getSetting': lambda s, k: '',
'setSetting': lambda s, k, v: None,
})()
_MockDialog = type('_MockDialog', (), {
'ok': lambda s, *a, **kw: None,
'yesno': lambda s, *a, **kw: True,
'select': lambda s, *a, **kw: 0,
'notification': lambda s, *a, **kw: None,
})()
_MockDialogBusy = type('_DialogBusy', (), {
'create': lambda s, *a, **k: None,
'update': lambda s, *a, **k: None,
'close': lambda s: None,
'isFinished': lambda s: True,
})()
_MockWindowDialog = type('_WindowDialog', (), {'doModal': lambda s: None})()
_MockFile = type('_File', (), {
'__enter__': lambda s: s,
'__exit__': lambda s, *a: None,
'read': lambda s: b'',
'write': lambda s, b: 0,
'close': lambda s: None,
})()
_MockPlayer = type('_Player', (), {
'play': lambda s, *a, **k: None,
'isPlaying': lambda s: False,
'getPlayingFile': lambda s: '',
'stop': lambda s: None,
})()
def _make_mod(name, **attrs):
m = types.ModuleType(name)
for k, v in attrs.items():
setattr(m, k, v)
return m
sys.modules['xbmcaddon'] = _make_mod('xbmcaddon', Addon=_MockAddon)
sys.modules['xbmcgui'] = _make_mod('xbmcgui', Dialog=_MockDialog, DialogBusy=_MockDialogBusy, WindowDialog=_MockWindowDialog)
sys.modules['xbmcvfs'] = _make_mod('xbmcvfs', File=_MockFile, exists=lambda s: False, mkdir=lambda s: True, mkdirs=lambda s: True, delete=lambda s: True, rename=lambda s,d: True, translatePath=lambda s: '/tmp')
sys.modules['xbmc'] = _make_mod('xbmc', Player=lambda: _MockPlayer, executebuiltin=lambda s: None, sleep=lambda s: None, getCondVisibility=lambda s: False, getInfoLabel=lambda s: '', log=lambda s: None)
try:
# Try importing hdfilme
from xstream.sites import hdfilme
print("hdfilme imported OK")
print("has showEntries:", hasattr(hdfilme, 'showEntries'))
print("SITE_NAME:", getattr(hdfilme, 'SITE_NAME', 'NOT FOUND'))
except Exception as e:
print(f"Import error: {e}")
import traceback
traceback.print_exc()
+9
View File
@@ -0,0 +1,9 @@
import sys, os
sys.path.insert(0, os.getcwd())
sys.path.insert(0, os.path.join(os.getcwd(), 'xstream'))
sys.path.insert(0, os.path.join(os.getcwd(), 'deps'))
import importlib.util
spec = importlib.util.spec_from_file_location('hdfilme', os.path.join(os.getcwd(), 'xstream/sites/hdfilme.py'))
m = spec.loader.load_module()
print('loaded ok, showEntries:', hasattr(m, 'showEntries'))
+289
View File
@@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xStream Downloader</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, sans-serif; background: #1a1a2e; color: #eee; }
header { background: #16213e; padding: 12px 20px; display: flex; align-items: center; gap: 20px; border-bottom: 1px solid #0f3460; }
header h1 { color: #e94560; font-size: 1.1rem; }
header input { flex: 1; max-width: 400px; padding: 8px 12px; border-radius: 6px; border: 1px solid #333; background: #0f3460; color: #fff; }
header button { padding: 8px 16px; background: #e94560; border: none; border-radius: 6px; color: white; cursor: pointer; }
#app { display: flex; height: calc(100vh - 52px); }
#sidebar { width: 200px; background: #16213e; overflow-y: auto; padding: 8px; border-right: 1px solid #0f3460; }
#sidebar h3 { font-size: 0.75rem; text-transform: uppercase; color: #888; padding: 8px 4px 4px; }
.site-btn { display: block; width: 100%; padding: 8px 10px; margin: 2px 0; background: transparent; border: 1px solid transparent; border-radius: 6px; color: #ccc; text-align: left; cursor: pointer; font-size: 0.9rem; }
.site-btn:hover { background: #0f3460; }
.site-btn.active { background: #e94560; color: white; border-color: #e94560; }
#main { flex: 1; overflow-y: auto; padding: 16px; }
#breadcrumb { color: #888; margin-bottom: 16px; font-size: 0.85rem; }
#breadcrumb span { cursor: pointer; color: #e94560; }
#breadcrumb span:hover { text-decoration: underline; }
.entries { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.card { background: #16213e; border-radius: 8px; overflow: hidden; cursor: pointer; transition: transform 0.1s; }
.card:hover { transform: translateY(-2px); }
.card img { width: 100%; height: 240px; object-fit: cover; background: #0f3460; }
.card-body { padding: 10px; }
.card-title { font-size: 0.85rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-meta { font-size: 0.75rem; color: #888; margin-top: 4px; }
.hoster-list { max-width: 500px; margin: 20px auto; }
.hoster { display: flex; align-items: center; gap: 12px; padding: 12px; background: #16213e; border-radius: 8px; margin: 6px 0; }
.hoster-name { flex: 1; font-weight: 600; }
.hoster-quality { color: #e94560; font-size: 0.8rem; }
.hoster button { padding: 6px 16px; background: #0f3460; border: 1px solid #e94560; border-radius: 6px; color: #e94560; cursor: pointer; font-size: 0.85rem; }
.hoster button:hover { background: #e94560; color: white; }
.hoster button.downloading { background: #e94560; color: white; cursor: not-allowed; }
#downloads { max-width: 600px; margin: 20px auto; }
.download-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: #16213e; border-radius: 8px; margin: 4px 0; }
.download-item .name { flex: 1; font-size: 0.9rem; }
.download-item .progress-bar { flex: 2; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
.download-item .progress-bar .fill { height: 100%; background: #e94560; transition: width 0.3s; }
.download-item .status { font-size: 0.75rem; color: #888; width: 80px; text-align: right; }
.section-title { color: #e94560; font-size: 1rem; margin-bottom: 12px; cursor: pointer; }
.loading { text-align: center; padding: 40px; color: #888; }
.error { background: #3d0000; border: 1px solid #e94560; padding: 12px; border-radius: 8px; margin: 10px 0; }
.hidden { display: none; }
.tab-bar { display: flex; gap: 4px; margin-bottom: 16px; }
.tab { padding: 6px 14px; background: transparent; border: 1px solid #333; border-radius: 6px; color: #888; cursor: pointer; font-size: 0.85rem; }
.tab.active { background: #e94560; color: white; border-color: #e94560; }
</style>
</head>
<body>
<header>
<h1>🎬 xStream Downloader</h1>
<input type="text" id="searchInput" placeholder="Film oder Serie suchen..." onkeydown="if(event.key==='Enter')doSearch()">
<button onclick="doSearch()">🔍</button>
<button onclick="showDownloads()" style="background:#0f3460;">📥 Downloads</button>
</header>
<div id="app">
<div id="sidebar">
<h3>📺 Sites</h3>
<div id="siteList"></div>
</div>
<div id="main">
<div id="breadcrumb"></div>
<div id="content"></div>
</div>
</div>
<script>
const API = '/api';
let state = { view: 'home', sites: [], site: null, entries: [], hosters: [], currentEntry: null, downloadId: null };
let downloads = {};
async function api(url) {
const r = await fetch(API + url);
if (!r.ok) { const e = await r.json(); throw new Error(e.error || 'API error'); }
return r.json();
}
async function loadSites() {
try {
state.sites = await api('/sites');
state.sites = state.sites.map(s => ({ ...s, id: s.identifier }));
renderSidebar();
} catch(e) { console.error(e); }
}
function renderSidebar() {
const el = document.getElementById('siteList');
el.innerHTML = state.sites.map(s =>
`<button class="site-btn${state.site && state.site.id === s.id ? ' active' : ''}" onclick="loadSite('${s.id}')">${s.name}</button>`
).join('');
}
async function loadSite(id) {
state.site = state.sites.find(s => s.id === id);
if (!state.site) return;
state.view = 'site';
state.entries = [];
state.hosters = [];
setBC(['Sites', state.site.name]);
document.getElementById('content').innerHTML = '<div class="loading">Lade...</div>';
try {
const data = await api(`/sites/${id}/entries`);
state.entries = data.entries || [];
renderEntries();
} catch(e) {
document.getElementById('content').innerHTML = `<div class="error">${e.message}</div>`;
}
renderSidebar();
}
function renderEntries() {
const el = document.getElementById('content');
if (!state.entries.length) {
el.innerHTML = '<div class="loading">Keine Einträge gefunden</div>';
return;
}
el.innerHTML = `<div class="entries">${state.entries.map(e => `
<div class="card" onclick="selectEntry(${state.entries.indexOf(e)})">
${e.thumb ? `<img src="${e.thumb}" onerror="this.style.display='none'">` : ''}
<div class="card-body">
<div class="card-title">${e.title || 'Unnamed'}</div>
${e.year ? `<div class="card-meta">${e.year}</div>` : ''}
${e.quality ? `<div class="card-meta">${e.quality}</div>` : ''}
</div>
</div>`).join('')}</div>`;
}
async function selectEntry(idx) {
const entry = state.entries[idx];
state.currentEntry = entry;
state.hosters = [];
setBC([state.site.name, entry.title]);
const el = document.getElementById('content');
el.innerHTML = `<div class="loading">Lade Hosters für "${entry.title}"...</div>`;
try {
const params = new URLSearchParams({ entryUrl: entry.url || '', episode: entry.episode || '' });
const data = await api(`/sites/${state.site.id}/hosters?${params}`);
state.hosters = (data.hosters || []).filter(h => typeof h === 'object');
renderHosters(entry);
} catch(e) {
el.innerHTML = `<div class="error">${e.message}</div>`;
}
}
function renderHosters(entry) {
const el = document.getElementById('content');
if (!state.hosters.length) {
el.innerHTML = `<div class="error">Keine Hosters gefunden</div><button class="tab" onclick="loadSite('${state.site.id}')">← Zurück</button>`;
return;
}
el.innerHTML = `
<div class="hoster-list">
<h2 class="section-title">⬇️ "${entry.title}" herunterladen</h2>
${state.hosters.map((h, i) => `
<div class="hoster">
<span class="hoster-name">${h.displayedName || h.name}</span>
${h.quality ? `<span class="hoster-quality">${h.quality}</span>` : ''}
<button id="dlbtn${i}" onclick="resolveAndDownload(${i}, '${jsEscape(h.link || h.url)}', '${jsEscape(entry.title)}')">⬇ Download</button>
</div>`).join('')}
</div>
<button class="tab" onclick="loadSite('${state.site.id}')">← Zurück</button>
`;
}
function jsEscape(s) {
return (s || '').replace(/'/g, "\\'").replace(/"/g, '\\"');
}
async function resolveAndDownload(idx, url, title) {
const btn = document.getElementById(`dlbtn${idx}`);
btn.textContent = '⏳ Warte auf URL...';
btn.classList.add('downloading');
try {
const data = await api(`/resolve?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&download=true`);
if (data.download_id) {
downloads[data.download_id] = { title, progress: 0, status: 'queued' };
btn.textContent = '📥 queued';
pollDownload(data.download_id, idx, title);
} else if (data.resolved) {
window.open(data.resolved, '_blank');
btn.textContent = '✅ Stream';
}
} catch(e) {
btn.textContent = '❌ Fehler';
btn.title = e.message;
}
}
async function pollDownload(did, idx, title) {
const btn = document.getElementById(`dlbtn${idx}`);
const maxWait = 120;
const start = Date.now();
async function poll() {
try {
const d = await api(`/downloads/${did}`);
if (d.status === 'complete') {
btn.textContent = '✅ Fertig';
downloads[did] = d;
} else if (d.status === 'error') {
btn.textContent = '❌ ' + (d.error || 'Fehler');
btn.title = d.error;
} else {
btn.textContent = `📥 ${d.progress}%`;
if (Date.now() - start < maxWait * 1000) {
setTimeout(poll, 1000);
}
}
} catch(e) {
btn.textContent = '❌ API Fehler';
}
}
poll();
}
async function doSearch() {
const q = document.getElementById('searchInput').value.trim();
if (!q) return;
state.view = 'search';
state.entries = [];
setBC([`Suche: "${q}"`]);
document.getElementById('content').innerHTML = '<div class="loading">Suche läuft...</div>';
// Try global search across all sites by searching each
const results = [];
for (const site of state.sites) {
try {
const data = await api(`/sites/${site.id}/entries?search=${encodeURIComponent(q)}`);
if (data.entries && data.entries.length) {
data.entries.forEach(e => e._site = site);
results.push(...data.entries);
}
} catch(e) {}
}
state.entries = results;
renderEntries();
}
async function showDownloads() {
state.view = 'downloads';
setBC(['Downloads']);
const el = document.getElementById('content');
el.innerHTML = '<div id="downloads"><div class="loading">Lade...</div></div>';
try {
const list = await api('/downloads');
el.innerHTML = `<div id="downloads">
<h2 class="section-title">📥 Aktive Downloads</h2>
${list.length ? list.map(d => `
<div class="download-item">
<span class="name">${d.title}</span>
<div class="progress-bar"><div class="fill" style="width:${d.progress}%"></div></div>
<span class="status">${d.status === 'complete' ? '✅' : d.status === 'error' ? '❌' : d.progress + '%'}</span>
${d.status === 'complete' ? `<button class="tab" onclick="location.href='/api/downloads/${d.id}/file'">💾 Speichern</button>` : ''}
</div>`).join('') : '<div class="loading">Keine Downloads</div>'}
</div>`;
} catch(e) {
el.innerHTML = `<div class="error">${e.message}</div>`;
}
// Auto-refresh
if (state.view === 'downloads') setTimeout(showDownloads, 3000);
}
function setBC(parts) {
const el = document.getElementById('breadcrumb');
let html = '<span onclick="goHome()">Home</span>';
let path = '';
parts.forEach((p, i) => {
path += '/' + (state.sites.find(s => s.name === p) ? state.sites.find(s => s.name === p).id : '');
html += ` &gt; <span>${p}</span>`;
});
el.innerHTML = html;
}
function goHome() {
state.view = 'home';
state.site = null;
state.entries = [];
setBC([]);
document.getElementById('content').innerHTML = '<div class="loading">Wähle eine Site aus dem Menü oder suche nach einem Film.</div>';
}
// Init
loadSites();
goHome();
</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
#LWP-Cookies-2.0
Set-Cookie3: _gp_sid="c70b4ae6-a8d7-4251-aee2-88f84111522c"; path="/"; domain=".aniworld.com"; path_spec; expires="2026-07-06 20:04:15Z"; HttpOnly=None; SameSite=Lax; version=0
Set-Cookie3: visit837d7375ce5b8c23d89aa03307f9ff00=1; path="/"; domain="slotoro-redirect.com"; expires="2026-08-04 20:04:16Z"; version=0
View File
+2
View File
@@ -0,0 +1,2 @@
#LWP-Cookies-2.0
Set-Cookie3: sid="a7551d1d-78ac-11f1-8e0e-1efc4216b63b"; path="/"; domain=".hdfilme.com"; path_spec; domain_dot; secure; expires="2094-07-23 23:18:04Z"; HttpOnly=None; version=0
@@ -0,0 +1 @@
<html><head><title>hdfilme.com</title><script type="text/javascript">var redirect_link = 'http://hyrala.com/f2.php?e=9MRCRIND8WYlhMuWLWGrRH49fmhDbUlERmg3SFdJUVlISVlQSmE0QmNLZGNpVWVJdC91N205WnlpU0ZkSXlDQ0F1QkJaNTNOQlRnSmpwcUptNFpLY0Jja0RTT1ZYWFQyZG01NnFPWDVlTFpqb1VZNXUrUVJ3T3VhdzU3MTlFOTZlYldaY3VNd216aWpVRkp5elhnekh4bzdUcjBNUFFqQnl4V0ZvR3RDV3lKSWt6dzdHQW44S2lMeEw2SCtIWFdBSUdEVHpnK2l1dHVmc25RVm9kTDZpSkpXY0EvZDVJQWp0Zk1GejdiSmJ6ZHhsRzF6Q1NNcEhXdVVNenlFcVlrQVlnL3NjMUN4dVM1QVZzT21OUUEwNW55UVhtY0N2dEQ4R0xVRWhBZDN5SFh5SlN4TkhXS1M3OW5SVnlrQ3RQcXo4THlYS1psaEY3UUFqbGNkQmZDK1ZQRjVRWEJQdXBUbEVaVVlFMFZFa2ZnK1JZWWlVTkY3N0hKd05RNTNkTldtYndVeVg5TDVBZVByOVZ2b01NbjZNTEdVUGhyeGFtcVlXbW5obmZEVTQwUk9Xei82OEhlbjJDNnloUWhlS1VJVmtoNHBuREJsOFdOVjJoWDIvb3hxS0puemZ2OHVLMjgzUS9ocjNmK0ZnPT0%3D';var vs = '&vs='+window.innerWidth + ':'+window.innerHeight;var ds = '&ds='+window.screen.width + ':'+window.screen.height;var sl = '&sl='+window.screenX + ':'+window.screenY;var os = '&os=f';if(window.screenX >= window.screen.width) { os = '&os=t'; }if(window.screenY >= window.screen.height) { os = '&os=t'; }var nos = '&nos=f';if(window.screenX >= window.screen.width-(window.screen.width*0.1) || window.screenX + window.innerWidth <window.innerWidth*0.1) { nos = '&nos=t'; }if(window.screenY >= window.screen.height-(window.screen.height*0.1) || window.screenY + window.innerHeight <window.innerHeight*0.1) { nos = '&nos=t'; }window.location.replace(redirect_link+vs+ds+sl+os+nos);</script></head><body bgcolor="#ffffff" text="#000000"></body></html>
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/domain_check.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/no_cover.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/plugin_info.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/resolveurl_settings.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/resolveurl_update.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/search.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/settings.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/animetoast.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/aniworld.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/api_sites.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/burningseries.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/einschalten.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/fhdfilme.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/filmpalast.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/hdfilme.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/internetarchive.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/kinoger.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/kkiste.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/megakino.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/moflix-stream.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/moviedream.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/netzkino.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/serienstream.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/streamcloud.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/tmdb_browser.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/sites/topstreamfilm.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/tmdb_browser.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/vavoo.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/art/xstream_settings.png
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/__init__.py
+182
View File
@@ -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 (('\\/', '/'), ('&amp;', '&'), ('\\u00c4', 'Ä'), ('\\u00e4', 'ä'),
('\\u00d6', 'Ö'), ('\\u00f6', 'ö'), ('\\u00dc', 'Ü'), ('\\u00fc', 'ü'),
('\\u00df', 'ß'), ('\\u2013', '-'), ('\\u00b2', '²'), ('\\u00b3', '³'),
('\\u00e9', 'é'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'),
('\\u00c9', 'É'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"),
('\\u0308', '̈'), ('\\u00e8', 'è'), ('#038;', ''), ('\\u00f8', 'ø'),
('', '/'), ('\\u00e1', 'á'), ('&#8211;', '-'), ('&#8220;', '"'), ('&#8222;', '"'),
('&#8217;', "'"), ('&#8230;', ''), ('\\u00bc', '¼'), ('\\u00bd', '½'), ('\\u00be', '¾'),
('\\u2153', ''), ('\\u002A', '*')):
s = s.replace(*t)
for h in (('\\/', '/'), ('&#x26;', '&'), ('&#039;', "'"), ("&#39;", "'"),
('&#xC4;', 'Ä'), ('&#xE4;', 'ä'), ('&#xD6;', 'Ö'), ('&#xF6;', 'ö'),
('&#xDC;', 'Ü'), ('&#xFC;', 'ü'), ('&#xDF;', 'ß'), ('&#xB2;', '²'),
('&#xDC;', '³'), ('&#xBC;', '¼'), ('&#xBD;', '½'), ('&#xBE;', '¾'),
('&#8531;', ''), ('&#8727;', '*')):
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
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/cache.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/captcha/captcha_helper.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/captcha/captcha_solver.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/config.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/__init__.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/contextElement.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/gui.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/guiElement.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/gui/hoster.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/ParameterHandler.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/__init__.py
@@ -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
+1
View File
@@ -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('&nbsp;', '')
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
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/jsunpacker.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/logger.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/player.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tmdb.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tmdbinfo.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/tools.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/trailer.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/updateManager.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/utils.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/vavoo/__init__.py
+1
View File
@@ -0,0 +1 @@
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/vavoo/auth.py

Some files were not shown because too many files have changed in this diff Show More