From 103a4899852ab7f87069c22f429c22ba6c43e799 Mon Sep 17 00:00:00 2001 From: Matthias Berner Date: Mon, 6 Jul 2026 14:27:51 +0200 Subject: [PATCH] Initial commit: xStream Downloader --- .gitignore | 15 + app.py | 371 ++++++ cookies/.txt | 0 deps/kodi_six | 1 + deps/pyaes | 1 + deps/requests | 1 + deps/resolveurl | 1 + deps/six | 1 + do_fix.py | 182 +++ do_fix.sh | 177 +++ fix_and_run.sh | 57 + mini_app.py | 711 ++++++++++ patch_dialog.py | 83 ++ patch_entries.py | 44 + patch_monitor.py | 34 + resources/settings.xml | 1163 +++++++++++++++++ test_full.py | 63 + test_import.py | 9 + web/index.html | 289 ++++ xStream/cookies/aniworld_com.txt | 3 + xStream/cookies/fhdfilme_com.txt | 0 xStream/cookies/hdfilme_com.txt | 2 + .../0259d957c611736fc62beae79d977c7c | 1 + .../6ef724e989c10545e0d861575ead939d | 1 + xstream/resources/art/domain_check.png | 1 + xstream/resources/art/no_cover.png | 1 + xstream/resources/art/plugin_info.png | 1 + xstream/resources/art/resolveurl_settings.png | 1 + xstream/resources/art/resolveurl_update.png | 1 + xstream/resources/art/search.png | 1 + xstream/resources/art/settings.png | 1 + xstream/resources/art/sites/animetoast.png | 1 + xstream/resources/art/sites/aniworld.png | 1 + xstream/resources/art/sites/api_sites.png | 1 + xstream/resources/art/sites/burningseries.png | 1 + xstream/resources/art/sites/einschalten.png | 1 + xstream/resources/art/sites/fhdfilme.png | 1 + xstream/resources/art/sites/filmpalast.png | 1 + xstream/resources/art/sites/hdfilme.png | 1 + .../resources/art/sites/internetarchive.png | 1 + xstream/resources/art/sites/kinoger.png | 1 + xstream/resources/art/sites/kkiste.png | 1 + xstream/resources/art/sites/megakino.png | 1 + xstream/resources/art/sites/moflix-stream.png | 1 + xstream/resources/art/sites/moviedream.png | 1 + xstream/resources/art/sites/netzkino.png | 1 + xstream/resources/art/sites/serienstream.png | 1 + xstream/resources/art/sites/streamcloud.png | 1 + xstream/resources/art/sites/tmdb_browser.png | 1 + xstream/resources/art/sites/topstreamfilm.png | 1 + xstream/resources/art/tmdb_browser.png | 1 + xstream/resources/art/vavoo.png | 1 + xstream/resources/art/xstream_settings.png | 1 + xstream/resources/lib/__init__.py | 1 + .../lib/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 199 bytes .../lib/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 187 bytes .../lib/__pycache__/cache.cpython-311.pyc | Bin 0 -> 1988 bytes .../lib/__pycache__/config.cpython-311.pyc | Bin 0 -> 4911 bytes .../lib/__pycache__/config.cpython-312.pyc | Bin 0 -> 4383 bytes .../lib/__pycache__/logger.cpython-311.pyc | Bin 0 -> 3258 bytes .../lib/__pycache__/player.cpython-311.pyc | Bin 0 -> 4742 bytes .../lib/__pycache__/tools.cpython-311.pyc | Bin 0 -> 15428 bytes .../lib/__pycache__/utils.cpython-311.pyc | Bin 0 -> 2794 bytes xstream/resources/lib/_tools_fixed.py | 182 +++ xstream/resources/lib/cache.py | 1 + .../captcha_helper.cpython-311.pyc | Bin 0 -> 3105 bytes .../captcha_solver.cpython-311.pyc | Bin 0 -> 9157 bytes .../resources/lib/captcha/captcha_helper.py | 1 + .../resources/lib/captcha/captcha_solver.py | 1 + xstream/resources/lib/config.py | 1 + xstream/resources/lib/gui/__init__.py | 1 + .../gui/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 203 bytes .../contextElement.cpython-311.pyc | Bin 0 -> 2670 bytes .../lib/gui/__pycache__/gui.cpython-311.pyc | Bin 0 -> 28153 bytes .../__pycache__/guiElement.cpython-311.pyc | Bin 0 -> 23093 bytes .../gui/__pycache__/hoster.cpython-311.pyc | Bin 0 -> 26577 bytes xstream/resources/lib/gui/contextElement.py | 1 + xstream/resources/lib/gui/gui.py | 1 + xstream/resources/lib/gui/guiElement.py | 1 + xstream/resources/lib/gui/hoster.py | 1 + .../resources/lib/handler/ParameterHandler.py | 1 + xstream/resources/lib/handler/__init__.py | 1 + .../ParameterHandler.cpython-311.pyc | Bin 0 -> 4028 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 207 bytes .../__pycache__/pluginHandler.cpython-311.pyc | Bin 0 -> 38466 bytes .../requestHandler.cpython-311.pyc | Bin 0 -> 36449 bytes .../resources/lib/handler/cRequestHandler.py | 193 +++ .../resources/lib/handler/pluginHandler.py | 1 + .../resources/lib/handler/requestHandler.py | 495 +++++++ xstream/resources/lib/jsunpacker.py | 1 + xstream/resources/lib/logger.py | 1 + xstream/resources/lib/player.py | 1 + xstream/resources/lib/tmdb.py | 1 + xstream/resources/lib/tmdbinfo.py | 1 + xstream/resources/lib/tools.py | 1 + xstream/resources/lib/trailer.py | 1 + xstream/resources/lib/updateManager.py | 1 + xstream/resources/lib/utils.py | 1 + xstream/resources/lib/vavoo/__init__.py | 1 + xstream/resources/lib/vavoo/auth.py | 1 + xstream/resources/lib/vavoo/vavoo.py | 1 + .../__pycache__/meinecloud.cpython-311.pyc | Bin 0 -> 17983 bytes xstream/resources/lib/wrappers/meinecloud.py | 1 + .../__pycache__/animetoast.cpython-311.pyc | Bin 0 -> 44461 bytes .../__pycache__/aniworld.cpython-311.pyc | Bin 0 -> 36815 bytes .../__pycache__/api_sites.cpython-311.pyc | Bin 0 -> 24335 bytes .../__pycache__/burningseries.cpython-311.pyc | Bin 0 -> 27239 bytes .../__pycache__/einschalten.cpython-311.pyc | Bin 0 -> 15101 bytes .../__pycache__/fhdfilme.cpython-311.pyc | Bin 0 -> 24225 bytes .../__pycache__/filmpalast.cpython-311.pyc | Bin 0 -> 22645 bytes .../sites/__pycache__/hdfilme.cpython-311.pyc | Bin 0 -> 20081 bytes .../internetarchive.cpython-311.pyc | Bin 0 -> 12756 bytes .../sites/__pycache__/kinoger.cpython-311.pyc | Bin 0 -> 18358 bytes .../sites/__pycache__/kkiste.cpython-311.pyc | Bin 0 -> 23680 bytes .../__pycache__/megakino.cpython-311.pyc | Bin 0 -> 26213 bytes .../__pycache__/moflix-stream.cpython-311.pyc | Bin 0 -> 25349 bytes .../__pycache__/netzkino.cpython-311.pyc | Bin 0 -> 17302 bytes .../__pycache__/serienstream.cpython-311.pyc | Bin 0 -> 71722 bytes .../__pycache__/streamcloud.cpython-311.pyc | Bin 0 -> 28722 bytes .../__pycache__/tmdb_browser.cpython-311.pyc | Bin 0 -> 32634 bytes .../__pycache__/topstreamfilm.cpython-311.pyc | Bin 0 -> 24957 bytes xstream/sites/animetoast.py | 1 + xstream/sites/aniworld.py | 1 + xstream/sites/api_sites.py | 1 + xstream/sites/burningseries.py | 1 + xstream/sites/einschalten.py | 1 + xstream/sites/fhdfilme.py | 1 + xstream/sites/filmpalast.py | 1 + xstream/sites/hdfilme.py | 1 + xstream/sites/internetarchive.py | 1 + xstream/sites/kinoger.py | 1 + xstream/sites/kkiste.py | 1 + xstream/sites/megakino.py | 1 + xstream/sites/moflix-stream.py | 1 + xstream/sites/netzkino.py | 1 + xstream/sites/serienstream.py | 1 + xstream/sites/streamcloud.py | 1 + xstream/sites/tmdb_browser.py | 1 + xstream/sites/topstreamfilm.py | 1 + 139 files changed, 4153 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 cookies/.txt create mode 120000 deps/kodi_six create mode 120000 deps/pyaes create mode 120000 deps/requests create mode 120000 deps/resolveurl create mode 120000 deps/six create mode 100644 do_fix.py create mode 100644 do_fix.sh create mode 100644 fix_and_run.sh create mode 100644 mini_app.py create mode 100644 patch_dialog.py create mode 100644 patch_entries.py create mode 100644 patch_monitor.py create mode 100644 resources/settings.xml create mode 100644 test_full.py create mode 100644 test_import.py create mode 100644 web/index.html create mode 100644 xStream/cookies/aniworld_com.txt create mode 100644 xStream/cookies/fhdfilme_com.txt create mode 100644 xStream/cookies/hdfilme_com.txt create mode 100644 xStream/htmlcache/0259d957c611736fc62beae79d977c7c create mode 100644 xStream/htmlcache/6ef724e989c10545e0d861575ead939d create mode 120000 xstream/resources/art/domain_check.png create mode 120000 xstream/resources/art/no_cover.png create mode 120000 xstream/resources/art/plugin_info.png create mode 120000 xstream/resources/art/resolveurl_settings.png create mode 120000 xstream/resources/art/resolveurl_update.png create mode 120000 xstream/resources/art/search.png create mode 120000 xstream/resources/art/settings.png create mode 120000 xstream/resources/art/sites/animetoast.png create mode 120000 xstream/resources/art/sites/aniworld.png create mode 120000 xstream/resources/art/sites/api_sites.png create mode 120000 xstream/resources/art/sites/burningseries.png create mode 120000 xstream/resources/art/sites/einschalten.png create mode 120000 xstream/resources/art/sites/fhdfilme.png create mode 120000 xstream/resources/art/sites/filmpalast.png create mode 120000 xstream/resources/art/sites/hdfilme.png create mode 120000 xstream/resources/art/sites/internetarchive.png create mode 120000 xstream/resources/art/sites/kinoger.png create mode 120000 xstream/resources/art/sites/kkiste.png create mode 120000 xstream/resources/art/sites/megakino.png create mode 120000 xstream/resources/art/sites/moflix-stream.png create mode 120000 xstream/resources/art/sites/moviedream.png create mode 120000 xstream/resources/art/sites/netzkino.png create mode 120000 xstream/resources/art/sites/serienstream.png create mode 120000 xstream/resources/art/sites/streamcloud.png create mode 120000 xstream/resources/art/sites/tmdb_browser.png create mode 120000 xstream/resources/art/sites/topstreamfilm.png create mode 120000 xstream/resources/art/tmdb_browser.png create mode 120000 xstream/resources/art/vavoo.png create mode 120000 xstream/resources/art/xstream_settings.png create mode 120000 xstream/resources/lib/__init__.py create mode 100644 xstream/resources/lib/__pycache__/__init__.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/__init__.cpython-312.pyc create mode 100644 xstream/resources/lib/__pycache__/cache.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/config.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/config.cpython-312.pyc create mode 100644 xstream/resources/lib/__pycache__/logger.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/player.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/tools.cpython-311.pyc create mode 100644 xstream/resources/lib/__pycache__/utils.cpython-311.pyc create mode 100644 xstream/resources/lib/_tools_fixed.py create mode 120000 xstream/resources/lib/cache.py create mode 100644 xstream/resources/lib/captcha/__pycache__/captcha_helper.cpython-311.pyc create mode 100644 xstream/resources/lib/captcha/__pycache__/captcha_solver.cpython-311.pyc create mode 120000 xstream/resources/lib/captcha/captcha_helper.py create mode 120000 xstream/resources/lib/captcha/captcha_solver.py create mode 120000 xstream/resources/lib/config.py create mode 120000 xstream/resources/lib/gui/__init__.py create mode 100644 xstream/resources/lib/gui/__pycache__/__init__.cpython-311.pyc create mode 100644 xstream/resources/lib/gui/__pycache__/contextElement.cpython-311.pyc create mode 100644 xstream/resources/lib/gui/__pycache__/gui.cpython-311.pyc create mode 100644 xstream/resources/lib/gui/__pycache__/guiElement.cpython-311.pyc create mode 100644 xstream/resources/lib/gui/__pycache__/hoster.cpython-311.pyc create mode 120000 xstream/resources/lib/gui/contextElement.py create mode 120000 xstream/resources/lib/gui/gui.py create mode 120000 xstream/resources/lib/gui/guiElement.py create mode 120000 xstream/resources/lib/gui/hoster.py create mode 120000 xstream/resources/lib/handler/ParameterHandler.py create mode 120000 xstream/resources/lib/handler/__init__.py create mode 100644 xstream/resources/lib/handler/__pycache__/ParameterHandler.cpython-311.pyc create mode 100644 xstream/resources/lib/handler/__pycache__/__init__.cpython-311.pyc create mode 100644 xstream/resources/lib/handler/__pycache__/pluginHandler.cpython-311.pyc create mode 100644 xstream/resources/lib/handler/__pycache__/requestHandler.cpython-311.pyc create mode 100644 xstream/resources/lib/handler/cRequestHandler.py create mode 120000 xstream/resources/lib/handler/pluginHandler.py create mode 100644 xstream/resources/lib/handler/requestHandler.py create mode 120000 xstream/resources/lib/jsunpacker.py create mode 120000 xstream/resources/lib/logger.py create mode 120000 xstream/resources/lib/player.py create mode 120000 xstream/resources/lib/tmdb.py create mode 120000 xstream/resources/lib/tmdbinfo.py create mode 120000 xstream/resources/lib/tools.py create mode 120000 xstream/resources/lib/trailer.py create mode 120000 xstream/resources/lib/updateManager.py create mode 120000 xstream/resources/lib/utils.py create mode 120000 xstream/resources/lib/vavoo/__init__.py create mode 120000 xstream/resources/lib/vavoo/auth.py create mode 120000 xstream/resources/lib/vavoo/vavoo.py create mode 100644 xstream/resources/lib/wrappers/__pycache__/meinecloud.cpython-311.pyc create mode 120000 xstream/resources/lib/wrappers/meinecloud.py create mode 100644 xstream/sites/__pycache__/animetoast.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/aniworld.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/api_sites.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/burningseries.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/einschalten.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/fhdfilme.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/filmpalast.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/hdfilme.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/internetarchive.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/kinoger.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/kkiste.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/megakino.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/moflix-stream.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/netzkino.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/serienstream.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/streamcloud.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/tmdb_browser.cpython-311.pyc create mode 100644 xstream/sites/__pycache__/topstreamfilm.cpython-311.pyc create mode 120000 xstream/sites/animetoast.py create mode 120000 xstream/sites/aniworld.py create mode 120000 xstream/sites/api_sites.py create mode 120000 xstream/sites/burningseries.py create mode 120000 xstream/sites/einschalten.py create mode 120000 xstream/sites/fhdfilme.py create mode 120000 xstream/sites/filmpalast.py create mode 120000 xstream/sites/hdfilme.py create mode 120000 xstream/sites/internetarchive.py create mode 120000 xstream/sites/kinoger.py create mode 120000 xstream/sites/kkiste.py create mode 120000 xstream/sites/megakino.py create mode 120000 xstream/sites/moflix-stream.py create mode 120000 xstream/sites/netzkino.py create mode 120000 xstream/sites/serienstream.py create mode 120000 xstream/sites/streamcloud.py create mode 120000 xstream/sites/tmdb_browser.py create mode 120000 xstream/sites/topstreamfilm.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c0541c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +*.egg-info/ +dist/ +build/ +.coverage +htmlcov/ +.env +venv/ +.venv/ +*.swp +*.swo +.DS_Store diff --git a/app.py b/app.py new file mode 100644 index 0000000..d39cc9e --- /dev/null +++ b/app.py @@ -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//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//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/') +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//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) diff --git a/cookies/.txt b/cookies/.txt new file mode 100644 index 0000000..e69de29 diff --git a/deps/kodi_six b/deps/kodi_six new file mode 120000 index 0000000..d088fd6 --- /dev/null +++ b/deps/kodi_six @@ -0,0 +1 @@ +../../dependencies/script.module.kodi-six \ No newline at end of file diff --git a/deps/pyaes b/deps/pyaes new file mode 120000 index 0000000..13b20e1 --- /dev/null +++ b/deps/pyaes @@ -0,0 +1 @@ +../../dependencies/script.module.pyaes \ No newline at end of file diff --git a/deps/requests b/deps/requests new file mode 120000 index 0000000..bd0939d --- /dev/null +++ b/deps/requests @@ -0,0 +1 @@ +../../dependencies/script.module.requests \ No newline at end of file diff --git a/deps/resolveurl b/deps/resolveurl new file mode 120000 index 0000000..1c7aa33 --- /dev/null +++ b/deps/resolveurl @@ -0,0 +1 @@ +../../dependencies/script.module.resolveurl \ No newline at end of file diff --git a/deps/six b/deps/six new file mode 120000 index 0000000..610e88a --- /dev/null +++ b/deps/six @@ -0,0 +1 @@ +../../dependencies/script.module.six \ No newline at end of file diff --git a/do_fix.py b/do_fix.py new file mode 100644 index 0000000..e75b106 --- /dev/null +++ b/do_fix.py @@ -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 diff --git a/do_fix.sh b/do_fix.sh new file mode 100644 index 0000000..7b18ca4 --- /dev/null +++ b/do_fix.sh @@ -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 diff --git a/fix_and_run.sh b/fix_and_run.sh new file mode 100644 index 0000000..0e2026b --- /dev/null +++ b/fix_and_run.sh @@ -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 diff --git a/mini_app.py b/mini_app.py new file mode 100644 index 0000000..4c2e8c4 --- /dev/null +++ b/mini_app.py @@ -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'' + 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('' if not self.ignoreErrors else '' + except URLError as e: + self._Status = str(e.reason) + return f'' if not self.ignoreErrors else '' + except Exception as e: + self._Status = str(e) + return f'' if not self.ignoreErrors else '' + + def getJson(self): + import json + text = self.getHtml() + if text.strip().startswith('JBB}B6TCQ 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 Od6zVy1Unu4w)`JzAS=uO literal 0 HcmV?d00001 diff --git a/xstream/sites/__pycache__/tmdb_browser.cpython-311.pyc b/xstream/sites/__pycache__/tmdb_browser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82bbab8e9f745532983d83875ec7df5edb58c8f2 GIT binary patch literal 32634 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 literal 0 HcmV?d00001 diff --git a/xstream/sites/__pycache__/topstreamfilm.cpython-311.pyc b/xstream/sites/__pycache__/topstreamfilm.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f87c97c83141912ecb3e6887acf4b94a27b1394 GIT binary patch literal 24957 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 literal 0 HcmV?d00001 diff --git a/xstream/sites/animetoast.py b/xstream/sites/animetoast.py new file mode 120000 index 0000000..3d18073 --- /dev/null +++ b/xstream/sites/animetoast.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/animetoast.py \ No newline at end of file diff --git a/xstream/sites/aniworld.py b/xstream/sites/aniworld.py new file mode 120000 index 0000000..0436721 --- /dev/null +++ b/xstream/sites/aniworld.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/aniworld.py \ No newline at end of file diff --git a/xstream/sites/api_sites.py b/xstream/sites/api_sites.py new file mode 120000 index 0000000..79fdf9f --- /dev/null +++ b/xstream/sites/api_sites.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/api_sites.py \ No newline at end of file diff --git a/xstream/sites/burningseries.py b/xstream/sites/burningseries.py new file mode 120000 index 0000000..5d37627 --- /dev/null +++ b/xstream/sites/burningseries.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/burningseries.py \ No newline at end of file diff --git a/xstream/sites/einschalten.py b/xstream/sites/einschalten.py new file mode 120000 index 0000000..edb13a1 --- /dev/null +++ b/xstream/sites/einschalten.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/einschalten.py \ No newline at end of file diff --git a/xstream/sites/fhdfilme.py b/xstream/sites/fhdfilme.py new file mode 120000 index 0000000..b5811dd --- /dev/null +++ b/xstream/sites/fhdfilme.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/fhdfilme.py \ No newline at end of file diff --git a/xstream/sites/filmpalast.py b/xstream/sites/filmpalast.py new file mode 120000 index 0000000..95c9843 --- /dev/null +++ b/xstream/sites/filmpalast.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/filmpalast.py \ No newline at end of file diff --git a/xstream/sites/hdfilme.py b/xstream/sites/hdfilme.py new file mode 120000 index 0000000..aaa54fa --- /dev/null +++ b/xstream/sites/hdfilme.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/hdfilme.py \ No newline at end of file diff --git a/xstream/sites/internetarchive.py b/xstream/sites/internetarchive.py new file mode 120000 index 0000000..af15964 --- /dev/null +++ b/xstream/sites/internetarchive.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/internetarchive.py \ No newline at end of file diff --git a/xstream/sites/kinoger.py b/xstream/sites/kinoger.py new file mode 120000 index 0000000..8014919 --- /dev/null +++ b/xstream/sites/kinoger.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/kinoger.py \ No newline at end of file diff --git a/xstream/sites/kkiste.py b/xstream/sites/kkiste.py new file mode 120000 index 0000000..63e8243 --- /dev/null +++ b/xstream/sites/kkiste.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/kkiste.py \ No newline at end of file diff --git a/xstream/sites/megakino.py b/xstream/sites/megakino.py new file mode 120000 index 0000000..c5834bf --- /dev/null +++ b/xstream/sites/megakino.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/megakino.py \ No newline at end of file diff --git a/xstream/sites/moflix-stream.py b/xstream/sites/moflix-stream.py new file mode 120000 index 0000000..fb0cc4f --- /dev/null +++ b/xstream/sites/moflix-stream.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/moflix-stream.py \ No newline at end of file diff --git a/xstream/sites/netzkino.py b/xstream/sites/netzkino.py new file mode 120000 index 0000000..3e9e13f --- /dev/null +++ b/xstream/sites/netzkino.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/netzkino.py \ No newline at end of file diff --git a/xstream/sites/serienstream.py b/xstream/sites/serienstream.py new file mode 120000 index 0000000..1db9d3f --- /dev/null +++ b/xstream/sites/serienstream.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/serienstream.py \ No newline at end of file diff --git a/xstream/sites/streamcloud.py b/xstream/sites/streamcloud.py new file mode 120000 index 0000000..04d15ee --- /dev/null +++ b/xstream/sites/streamcloud.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/streamcloud.py \ No newline at end of file diff --git a/xstream/sites/tmdb_browser.py b/xstream/sites/tmdb_browser.py new file mode 120000 index 0000000..f1dc015 --- /dev/null +++ b/xstream/sites/tmdb_browser.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/tmdb_browser.py \ No newline at end of file diff --git a/xstream/sites/topstreamfilm.py b/xstream/sites/topstreamfilm.py new file mode 120000 index 0000000..afe4d07 --- /dev/null +++ b/xstream/sites/topstreamfilm.py @@ -0,0 +1 @@ +/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/sites/topstreamfilm.py \ No newline at end of file