#!/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'' except URLError as e: self._Status = str(e.reason) if self.ignoreErrors: return '' return f'' except Exception as e: self._Status = str(e) if self.ignoreErrors: return '' return f'' def getJson(self): import json text = self.getHtml() if text.strip().startswith('