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>
346 lines
11 KiB
Python
346 lines
11 KiB
Python
# -*- 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', '']
|