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