Files
Xstream-Standalone/webui/kodi_stubs.py
DasPoschi ed1ce03b26 DAP-75: Web UI für xStream-Suche erstellt
Standalone Flask-Web-UI, die die bestehenden xStream-Site-Plugins
außerhalb von Kodi ausführt. Kodi-APIs werden durch leichtgewichtige
Stubs ersetzt; die vorhandenen _search()-Funktionen aller 23 Sites
werden über ThreadPoolExecutor parallel aufgerufen.

- webui/kodi_stubs.py       – Stubs für xbmc, xbmcaddon, xbmcgui, xbmcplugin, xbmcvfs, resolveurl
- webui/app.py              – Flask-Server mit /api/search und /api/sites
- webui/templates/index.html – Dunkles Kodi-ähnliches Such-UI (Card-Grid, Filter, Detail-Overlay)
- webui/requirements.txt    – flask, certifi, pyaes, requests

Start: cd webui && pip install -r requirements.txt && python app.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-18 10:54:08 +02:00

346 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Kodi API stubs for standalone xStream web UI.
Call install() before importing any plugin code.
"""
import sys
import os
import logging
import types
import tempfile
_log = logging.getLogger('kodi')
_CACHE_DIR = os.path.join(os.path.dirname(__file__), 'cache')
os.makedirs(_CACHE_DIR, exist_ok=True)
# ── xbmc ─────────────────────────────────────────────────────────────────────
LOGDEBUG = 0
LOGINFO = 1
LOGWARNING = 2
LOGERROR = 4
LOGFATAL = 5
LOGNONE = 6
def _make_xbmc():
mod = types.ModuleType('xbmc')
mod.LOGDEBUG = LOGDEBUG
mod.LOGINFO = LOGINFO
mod.LOGWARNING = LOGWARNING
mod.LOGERROR = LOGERROR
mod.LOGFATAL = LOGFATAL
mod.LOGNONE = LOGNONE
def log(msg, level=LOGDEBUG):
if level >= LOGWARNING:
_log.warning('[xbmc] %s', msg)
else:
_log.debug('[xbmc] %s', msg)
mod.log = log
def getCondVisibility(cond):
if 'linux' in cond.lower() and 'raspberrypi' not in cond.lower():
return True
return False
mod.getCondVisibility = getCondVisibility
def sleep(ms):
import time
time.sleep(ms / 1000.0)
mod.sleep = sleep
def executebuiltin(s):
pass
mod.executebuiltin = executebuiltin
def translatePath(path):
path = path.replace('special://home/', os.path.expanduser('~') + '/')
path = path.replace('special://profile/', _CACHE_DIR + '/')
path = path.replace('special://temp/', tempfile.gettempdir() + '/')
return path
mod.translatePath = translatePath
class Monitor:
def abortRequested(self):
return False
def waitForAbort(self, timeout=0):
return False
mod.Monitor = Monitor
return mod
# ── xbmcaddon ────────────────────────────────────────────────────────────────
_ADDON_SETTINGS = {
'TMDBMETA': 'false',
'replacefanart': 'false',
'metaOverwrite': 'false',
'bypassDNSlock': 'false',
'volatileHtmlCache': 'false',
'hosterSelect': 'Auto',
'blockedHoster': '',
'2captcha.pass': '',
}
_PLUGIN_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'plugin.video.xstream')
)
def _make_xbmcaddon():
mod = types.ModuleType('xbmcaddon')
class Addon:
def __init__(self, addon_id='plugin.video.xstream'):
self._id = addon_id
def getSetting(self, key):
return _ADDON_SETTINGS.get(key, '')
def setSetting(self, key, value):
_ADDON_SETTINGS[key] = str(value)
def getAddonInfo(self, key):
infos = {
'id': 'plugin.video.xstream',
'version': '2026.02.11',
'name': 'xStream',
'path': _PLUGIN_DIR,
'profile': _CACHE_DIR,
'icon': '',
'fanart': '',
}
return infos.get(key, '')
def getLocalizedString(self, code):
_strings = {
30166: 'xStream',
30265: 'Gesperrt',
30260: 'Diese Seite ist gesperrt.',
30261: 'Bitte wende dich an deinen Provider.',
30275: 'Changelog',
30279: 'Nächste Seite',
30281: 'Suche',
30322: 'Entwickleroptionen',
30500: 'Neu',
30502: 'Filme',
30505: 'Dokumentationen',
30511: 'Serien',
30520: 'Suche',
}
return _strings.get(code, str(code))
def openSettings(self):
pass
mod.Addon = Addon
return mod
# ── xbmcgui ──────────────────────────────────────────────────────────────────
def _make_xbmcgui():
mod = types.ModuleType('xbmcgui')
mod.NOTIFICATION_INFO = 'info'
mod.NOTIFICATION_WARNING = 'warning'
mod.NOTIFICATION_ERROR = 'error'
class Dialog:
def ok(self, heading, message):
_log.info('[dialog.ok] %s: %s', heading, message)
def notification(self, heading, message, icon='', time=5000, sound=True):
_log.info('[dialog.notification] %s: %s', heading, message)
def yesno(self, heading, message):
return False
def select(self, heading, items):
return -1
def input(self, heading, defaultt=''):
return ''
class Window:
def __init__(self, win_id=0):
pass
def getControl(self, ctrl_id):
return None
class DialogProgress:
def create(self, heading, message=''):
pass
def update(self, percent, message=''):
pass
def close(self):
pass
def iscanceled(self):
return False
class DialogProgressBG:
def create(self, heading, message=''):
pass
def update(self, percent=0, heading='', message=''):
pass
def close(self):
pass
def isFinished(self):
return True
class ListItem:
def __init__(self, label='', label2='', path=''):
self.label = label
def setArt(self, art):
pass
def setInfo(self, type, info):
pass
def setProperty(self, key, value):
pass
def addContextMenuItems(self, items):
pass
def setContentLookup(self, enable):
pass
def setMimeType(self, mime):
pass
mod.Dialog = Dialog
mod.DialogProgress = DialogProgress
mod.DialogProgressBG = DialogProgressBG
mod.Window = Window
mod.ListItem = ListItem
return mod
# ── xbmcplugin ───────────────────────────────────────────────────────────────
def _make_xbmcplugin():
mod = types.ModuleType('xbmcplugin')
mod.SORT_METHOD_LABEL = 1
mod.SORT_METHOD_TITLE = 9
def addDirectoryItem(handle, url, listitem, isFolder=False, totalItems=0):
pass
def addDirectoryItems(handle, items, totalItems=0):
pass
def setContent(handle, content):
pass
def addSortMethod(handle, sortMethod):
pass
def setPluginCategory(handle, category):
pass
def endOfDirectory(handle, succeeded=True, updateListing=False, cacheToDisc=True):
pass
def setResolvedUrl(handle, succeeded, listitem):
pass
mod.addDirectoryItem = addDirectoryItem
mod.addDirectoryItems = addDirectoryItems
mod.setContent = setContent
mod.addSortMethod = addSortMethod
mod.setPluginCategory = setPluginCategory
mod.endOfDirectory = endOfDirectory
mod.setResolvedUrl = setResolvedUrl
return mod
# ── xbmcvfs ──────────────────────────────────────────────────────────────────
def _make_xbmcvfs():
mod = types.ModuleType('xbmcvfs')
def translatePath(path):
path = path.replace('special://home/', os.path.expanduser('~') + '/')
path = path.replace('special://profile/', _CACHE_DIR + '/')
path = path.replace('special://temp/', tempfile.gettempdir() + '/')
return path
mod.translatePath = translatePath
def exists(path):
return os.path.exists(path)
def mkdirs(path):
os.makedirs(path, exist_ok=True)
return True
def delete(path):
try:
os.remove(path)
return True
except Exception:
return False
def listdir(path):
try:
entries = os.listdir(path)
dirs = [e for e in entries if os.path.isdir(os.path.join(path, e))]
files = [e for e in entries if os.path.isfile(os.path.join(path, e))]
return dirs, files
except Exception:
return [], []
def File(path, mode='r'):
return open(path, mode)
mod.exists = exists
mod.mkdirs = mkdirs
mod.delete = delete
mod.listdir = listdir
mod.File = File
return mod
# ── resolveurl (stub no actual resolution) ─────────────────────────────────
def _make_resolveurl():
mod = types.ModuleType('resolveurl')
def relevant_resolvers(domain='', url=''):
return []
class HostedMediaFile:
def __init__(self, url='', host='', media_id=''):
self.url = url
def get_resolvers(self):
return []
def resolve(self):
return self.url
def resolve(url):
return url
mod.relevant_resolvers = relevant_resolvers
mod.HostedMediaFile = HostedMediaFile
mod.resolve = resolve
return mod
# ── pyaes (lightweight stub if not installed) ─────────────────────────────────
def _ensure_pyaes():
try:
import pyaes # noqa: F401
except ImportError:
mod = types.ModuleType('pyaes')
class AESModeOfOperationCBC:
def __init__(self, key, iv=None):
pass
def decrypt(self, data):
return data
class Decrypter:
def __init__(self, aes, padding=None):
pass
def feed(self, data):
return data
class PADDING_NONE:
pass
mod.AESModeOfOperationCBC = AESModeOfOperationCBC
mod.Decrypter = Decrypter
mod.PADDING_NONE = PADDING_NONE
sys.modules['pyaes'] = mod
def install():
"""Install all Kodi stubs into sys.modules. Call once at startup."""
sys.modules.setdefault('xbmc', _make_xbmc())
sys.modules.setdefault('xbmcaddon', _make_xbmcaddon())
sys.modules.setdefault('xbmcgui', _make_xbmcgui())
sys.modules.setdefault('xbmcplugin', _make_xbmcplugin())
sys.modules.setdefault('xbmcvfs', _make_xbmcvfs())
sys.modules.setdefault('resolveurl', _make_resolveurl())
_ensure_pyaes()
# Some modules import from sub-namespaces
sys.modules.setdefault('xbmc.xbmc', sys.modules['xbmc'])
sys.modules.setdefault('xbmcvfs', sys.modules['xbmcvfs'])
# Make sure sys.argv has the expected Kodi plugin format
if len(sys.argv) < 3:
sys.argv = ['plugin://plugin.video.xstream/', '1', '']