You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
movie — Persistente Filmliste (DB-Driven Precache)
CREATETABLEIF NOT EXISTS movie (
imdb_id TEXTPRIMARY KEY,
tmdb_id INTEGER,
title TEXTNOT NULL,
originaltitle TEXT DEFAULT '',
year TEXT DEFAULT '',
added_at INTEGERNOT NULL, -- Wann in DB eingefuegt
source TEXT DEFAULT 'discover', -- 'discover', 'trailer', 'play'
priority INTEGER DEFAULT 0-- 0=normal, 1=trailer (hoehere Prio)
);
source: Woher der Film kam (discover=TMDB Trend/Neu, trailer=Trailer-Klick, play=User-Klick)
priority: Trailer-Filme bekommen priority=1, werden zuerst gescannt
added_at: Fuer Sortierung (neueste zuerst bei gleicher Prioritaet)
add_movie(): INSERT OR IGNORE — bekannte Filme nicht ueberschreiben
add_movie_priority(): ON CONFLICT DO UPDATE SET priority, source — fuer Trailer
busy_since wird bei dispatch_probe() gesetzt (vor Resolve-Dispatch)
Timeout: 20s (Resolve braucht bis zu 15s)
Timestamp wird NIE geloescht — bleibt als Beweis, wird bei naechstem Dispatch ueberschrieben
stream_type wird beim ersten erfolgreichen MediaInfo-Probe gelernt
Danach: Typ-Erkennung (8KB peek) ueberspringen, direkt zum richtigen Prober
Spart ~200-400ms pro Probe
Trigger
source_deleted_cleanup
Raeumt movie_provider auf wenn Sources geloescht werden (z.B. TTL-Ablauf).
Wenn die letzte Source eines Providers fuer einen Film geloescht wird, wird der Provider-Eintrag entfernt.
Dadurch wird is_fully_scanned() und get_phase() automatisch aktualisiert.
CREATETRIGGERIF NOT EXISTS source_deleted_cleanup
AFTER DELETEON source
BEGINDELETEFROM movie_provider
WHERE imdb_id =OLD.imdb_idAND provider =OLD.providerAND NOT EXISTS (
SELECT1FROM source WHERE imdb_id =OLD.imdb_idAND provider =OLD.providerAND expires_at > strftime('%s','now')
);
END;
VIEW: v_movie_phase — Dynamische Phase-Berechnung
Phase wird nie gespeichert sondern per SQL VIEW live berechnet.
Die VIEW wird bei jedem _ensure_table() neu erstellt (DROP + CREATE).
CREATEVIEWIF NOT EXISTS v_movie_phase ASSELECTm.imdb_id, m.tmdb_id, m.title, m.originaltitle, m.year,
m.priority, m.added_at, m.sourceAS movie_source,
CASE
WHEN sources=0AND alle_provider_aufgerufen THEN 'done'
WHEN sources=0 THEN '1a'
WHEN hd_sources >0 THEN 'done'
WHEN unprobed_sources >0 THEN '1b'
WHEN NOT alle_provider_aufgerufen THEN '2'
ELSE 'done'
END AS phase,
source_count, unprobed_count, hd_count
FROM movie m
LEFT JOIN (...aggregierte Source-Counts...) ...;
Phase-Reihenfolge (WICHTIG)
Keine Sources + alle Provider → done
Keine Sources + Provider uebrig → 1a
HD gefunden → done (VOR unprobed-Check!)
Ungeprobte Sources (excl. probe_failed) → 1b
Kein HD + Provider uebrig → 2
Alle Provider, kein HD → done
Kritisch: HD-Check MUSS vor unprobed-Check kommen. Ein Film mit HD-Source
ist fertig, auch wenn noch ungeprobte Sources existieren.
Source-Counts (nur nicht-abgelaufene)
source_count: Alle Sources mit expires_at > now
unprobed_count: Sources mit probe_at IS NULL AND probe_failed = 0
hd_count: Sources mit probe_height >= 720 AND probe_failed = 0
Nutzung
# Alle scanbaren Filme (eine einzige Query statt N+1)get_scannable_movies() # SELECT * FROM v_movie_phase WHERE phase != 'done'# Phase fuer einzelnen Filmget_phase(imdb_id) # SELECT phase FROM v_movie_phase WHERE imdb_id = ?
Indizes
CREATEINDEXIF NOT EXISTS idx_source_imdb ON source(imdb_id);
CREATEINDEXIF NOT EXISTS idx_source_expires ON source(expires_at);
CREATEINDEXIF NOT EXISTS idx_source_probe ON source(imdb_id, probe_height);
CREATEINDEXIF NOT EXISTS idx_movie_tmdb ON movie(tmdb_id);
-- Covering-Index: alle 3 LEFT JOINs in v_movie_phase als Index-Only-ScanCREATEINDEXIF NOT EXISTS idx_source_phase ON source(imdb_id, expires_at, probe_at, probe_failed, probe_height);
idx_source_phase: Covering-Index fuer die VIEW-Subqueries. SQLite kann Source-Count, Unprobed-Count und HD-Count komplett aus dem Index lesen ohne die source-Tabelle zu oeffnen.
Migrationen (_ensure_table)
Migrationen laufen automatisch beim Import von sourcecacheDB.py:
Feature: Background Source Precaching for xShip
Status: IMPLEMENTIERT (Pool-basierter Dispatcher)
Datum: 2026-03-14
Base path: xS-Home/plugin.video.xship/
1. Problem
Der Quellen-Scan in xShip dauert 5-30 Sekunden pro Film. User klickt "Play" und wartet auf den Progress-Dialog, bis ~16 Scraper parallel alle Streaming-Seiten durchsucht haben. Erst danach erscheint die Quellenliste.
2. Loesung
Hintergrund-Precaching: service.py prueft alle 10s ob xShip aktiv ist und startet einen Daemon-Thread der populaere Filme scannt. Ergebnisse in SQLite cachen. Bei Cache-Hit wird der komplette Scan uebersprungen — Quellen sind sofort da (<15ms).
Scraper geben Hoster-Page-URLs zurueck (z.B. voe.sx/e/abc123), nicht finale Stream-URLs. Diese sind wochen- bis monatelang stabil. Aufloesung zum Stream erst beim Abspielen via ResolveURL. Gecacht wird nur die stabile Hoster-URL (via ResolvedUrl str-Subklasse in utils.py).
3. Phasen pro Film
Jeder Film durchlaeuft eigenstaendig 4 Phasen. Phase wird dynamisch aus DB-Zustand berechnet (get_phase() via movie_provider + source + provider Tabellen). Kein gespeicherter Status — immer aktuell.
Phase
Ziel
Aktion
Exit
1a
Schnell eine Quelle finden
1 Provider aufrufen
Treffer → 1b, kein Treffer → naechster Provider
1b
Qualitaet pruefen
Ungeprobte Quellen proben (MediaInfo)
HD ≥720p → done, sonst → 2
2
Luecken fuellen
Weitere Provider scannen + sofort proben
HD gefunden → done, alle Provider durch → done
done
Fertig
Kein weiterer Scan noetig
Cache-Ablauf oder Force-Rescan
4. Architektur
Der BG-Scan nutzt einen Pool-basierten Dispatcher (ThreadPoolExecutor). Scans und Probes laufen parallel, DB trackt Busy-State. Details: → DISPATCHER.md
Einfaches Fill-Up: Kein proportionales Balancing noetig — bei Cold-Start dominieren Scans (keine Resolves/MediaInfos), danach gleichen sich die Queues natuerlich aus
Busy-Tracking in DB
Provider-Busy (movie_provider.hits)
Kein neues Feld noetig. Bestehende movie_provider-Tabelle:
busy_since wird NIE geloescht — bleibt als Beweis, wird bei naechstem Dispatch ueberschrieben
MediaInfo braucht KEIN Busy-Tracking — greift auf CDN-URL zu, nicht auf Hoster
Timeouts
Typ
Timeout
Grund
Scan
5s
Scraper-Calls normalerweise < 5s
Resolve
20s
ResolveURL braucht bis zu 15s
MediaInfo
—
Kein Tracking noetig (CDN-URL, kein Hoster-Limit)
Kein Cleanup noetig. Timestamps bleiben stehen. Busy-Check nutzt Zeitfenster.
Scan-Dispatch (Queue 1)
Pro Runde: 1 Scan pro Film, 1 Film pro Provider (max free_slots Items)
defget_dispatchable_scans(movie_imdb_ids, available_providers):
busy_providers=SELECTDISTINCTproviderFROMmovie_providerWHEREhitsISNULLAND (now-called_at) <=5free_providers=available-busyforimdb_idinmovie_imdb_ids:
phase=get_phase(imdb_id) # nur 1a oder 2called=get_called_providers(imdb_id) # ALLE (inkl. in-flight)fornameinget_ordered_providers():
ifnameinfree_providersandnamenotincalled:
items.append((imdb_id, name))
free_providers.discard(name) # Provider nur 1x pro Rundebreak
HD-Skip: get_phase() gibt done zurueck wenn Film HD hat → Film nicht in Scan-Queue.
Resolve-Dispatch (Queue 2)
Pro Runde: 1 Resolve pro freien Hoster (max restliche free_slots)
Frischerer Cache (Quellen werden vor Ablauf erneuert)
Freie Kapazitaet fuer zusaetzliche Filmlisten
Bessere User-Erfahrung (immer aktuelle Quellen)
3. Vorschlaege
3.1 TTL-gesteuerte Erneuerung
Statt auf Ablauf zu warten: Sources proaktiv erneuern wenn sie "bald" ablaufen.
Prinzip: Bei TTL = 14 Tage taeglich 1/14 der Sources erneuern.
REFRESH_WINDOW=2*86400# 2 Tage vor Ablaufdefget_expiring_movies(window_days=2):
"""Filme deren Sources bald ablaufen, aelteste zuerst."""now=int(time.time())
threshold=now+ (window_days*86400)
returncursor.execute(""" SELECT DISTINCT imdb_id, MIN(expires_at) as earliest_expiry FROM source WHERE expires_at > ? AND expires_at <= ? GROUP BY imdb_id ORDER BY earliest_expiry ASC """, (now, threshold)).fetchall()
Priorisierung:
Sources die in < 2 Tagen ablaufen (dringend)
Filme die der User haeufig angeklickt hat (wertvoll)
Filme mit nur SD-Quellen (Hoffnung auf HD bei Rescan)
Rest nach Alter (aelteste zuerst)
3.2 Zusaetzliche Filmlisten
Aktuell nur 2 Listen: "Neu im Kino" + "Im Trend". Weitere TMDB-Listen:
xShip speichert Watch-History in playcountDB.py (SQLite playcount.db):
defis_watched(imdb_id):
"""Prueft ob Film als gesehen markiert ist."""fromresources.lib.playcountDBimportgetPlaycountpc=getPlaycount('movie', 'imdb_id', imdb_id)
returnpcisnotNoneandpc>0
<settingid="precache.skip_watched"type="bool"label="Gesehene Filme ueberspringen"default="true" />
Einschraenkung: playcountDB trackt nur Filme die ueber xShip abgespielt wurden. Kodis native videodb waere umfassender, aber der Zugriff ist komplexer (JSON-RPC oder direkte DB).
3.4 Wartungsmodus nach Erstbefuellung
Zwei Modi in _run_cycle():
Modus
Trigger
Verhalten
Initial
Keine/wenige Sources im Cache
Voller Scan aller Listen (wie bisher)
Wartung
Cache gefuellt, nur Erneuerung noetig
Nur ablaufende Sources refreshen
def_run_cycle():
cleanup_expired()
# Modus bestimmenstats=get_stats()
expiring=get_expiring_movies(window_days=2)
ifstats['unique_movies'] <10:
# Initial: voller Scanmovie_ids=_fetch_all_lists()
elifexpiring:
# Wartung: nur ablaufende Filmemovie_ids= [row['imdb_id'] forrowinexpiring]
else:
# Alles frisch — nichts zu tunreturn# ... Rest wie bisher (Metadaten, Scraper, _run_loop)
Throttle im Wartungsmodus:
Max 5 Filme pro Zyklus refreshen (nicht alles auf einmal)
Kuerzeres Intervall moeglich (z.B. alle 30 Min statt 5 Min)
Bei Wiedergabe: Wartung komplett pausieren
3.5 Listen-Rotation
Nicht alle Listen in jedem Zyklus scannen:
Zyklus 1: Neu im Kino + Im Trend (Pflichtlisten, immer)
Zyklus 2: Neu + Trend + Beliebteste (1 Extra-Liste rotieren)
Zyklus 3: Neu + Trend + Box Office
Zyklus 4: Neu + Trend + Bestbewertet
Zyklus 5: Neu + Trend + Genre (Action)
Umsetzung: Rotations-Index in DB oder Window Property.
Nicht alle Filme gleich behandeln — dynamische Refresh-Prioritaet:
Faktor
Effekt auf Refresh-Prio
User hat Film angeklickt
Hoehere Prio (haeufiger refreshen)
Film hat HD-Quelle
Niedrigere Prio (kein dringender Bedarf)
Film hat nur SD
Hoehere Prio (bei Rescan evtl. HD verfuegbar)
Source fast abgelaufen
Hoehere Prio (dringend)
Film in mehreren Listen
Hoehere Prio (populaerer)
-- Refresh-Score pro Film (hoeher = dringender)SELECTs.imdb_id,
-- Ablauf-Dringlichkeit (0-100, hoeher = bald ablaufend)MAX(0, 100- ((MIN(s.expires_at) - ?) /86400.0*7)) as urgency,
-- HD-Penalty (HD vorhanden = weniger dringend)
CASE WHEN MAX(s.probe_height) >=720 THEN 0 ELSE 30 END as quality_bonus,
-- Klick-Bonus (aus bookmarkDB oder kuenftigem Click-Tracking)0as click_bonus
FROM source s
WHEREs.expires_at> ?
GROUP BYs.imdb_idORDER BY (urgency + quality_bonus + click_bonus) DESC
3.7 Smart Refresh (qualitaetsabhaengig)
Refresh ist nicht gleich Initial-Scan. Wir haben movie_provider-Historie — wir wissen welche Provider was geliefert haben. Das nutzen wir.
Drei Strategien im Vergleich:
Strategie
Aufwand
Vorteil
Nachteil
Full Rescan (alle Provider)
Hoch (13+ Calls/Film)
Findet neue/bessere Quellen
80% Provider liefern 0 — Verschwendung
Known-Good only (nur hits>0)
Niedrig (2-3 Calls/Film)
Schnell, verifiziert bestehende
Verpasst neue Provider
Smart: Known-Good + 1 exploratory
Mittel (3-4 Calls/Film)
Schnell + Chance auf Neues
Etwas mehr Traffic
Empfehlung: Smart Refresh, differenziert nach Qualitaet:
Film hat HD-Quelle
Besten ungetesteten Provider zuerst, known-good HD-Provider als Fallback.
movie_provider-Historie:
megakino (prio 8) → hits=2 (davon 1x HD via Probe)
filmpalast (prio 1) → nie getestet fuer diesen Film
Refresh-Reihenfolge:
1. filmpalast (bester ungetesteter, prio 1) → HD? → neuer Top-Source!
2. Falls kein HD: megakino (known-good HD) re-verifizieren
3. Falls megakino fail: naechster ungetesteter
Rationale: Known-good HD-Provider hat evtl. schlechte Prio. Besserer Provider koennte gleiche/bessere Qualitaet haben. Im schlimmsten Fall: 2 Calls. Im besten: 1 Call mit besserem Ergebnis.
Film hat nur SD-Quellen
Known-Good Provider + 1 neuen/ungetesteten Provider pro Refresh-Zyklus.
movie_provider-Historie:
einschalten → hits=1 (SD)
filmpro → hits=1 (SD)
filmpalast → hits=0
Refresh: einschalten + filmpro (known-good) + kinoger (neu, noch nie probiert)
Chance auf HD-Upgrade durch neuen Provider.
Film hatte 0 Hits
1 Provider pro Zyklus rotieren. Film wurde vielleicht inzwischen bei einem Provider hinzugefuegt.
Letzter Scan: filmpalast (0), einschalten (0), filmpro (0)
Refresh: moflix (naechster in Rotation) — nur 1 Call
Was behalten nach Refresh
Top 3 nach echter Probe-Qualitaet (nicht Provider-Label), mit mindestens 2 verschiedenen Providern wenn moeglich. Rest loeschen.
Gruende:
Ein Film braucht keine 15 Quellen wenn 3 davon HD sind
Weniger DB-Platz, weniger Probe-Zeit beim naechsten Refresh
Provider-Diversitaet als Fallback (Provider A down → Provider B hat auch HD)
Provider-Reihenfolge via provider.priority (dynamisch)
Neuer Provider = nicht in movie_provider fuer diesen Film
Kein neues Schema noetig. Nur die Refresh-Logik in _run_cycle() muss zwischen Initial und Refresh unterscheiden.
3.8 Refresh-Tiers und Intervalle
Nicht alle Filme gleich oft refreshen. 3 Tiers nach Qualitaet:
Tier
Bedingung
Refresh-Intervall
Scan-Strategie
A — HD
probe_height >= 720
1× pro TTL (14d)
Bester ungetesteter + known-good Fallback
B — nur SD
Sources vorhanden, kein HD
1× pro TTL (14d)
Known-good + 1 neuer Provider
C — keine Sources
Full-Scan 0 Hits
1× pro 2× TTL (28d)
1 Provider rotierend
Intervall-Logik (zwei Modi):
Modus
Trigger
Intervall
Zweck
Arbeits-Modus
Queues haben Items
5 Min
Unterbrochene Arbeit schnell fortsetzen
Idle-Modus
Alle Queues leer
60 Min
Periodischer Check auf ablaufende Sources
# In start_if_needed():has_work=get_pending_work_count() >0# Scan/Resolve/MediaInfo Queuesinterval=5*60ifhas_workelse60*60# 5min vs 60min
5-Min Intervall ist nur fuer den Fall dass xShip verlassen wird und wir weitermachen muessen. Wenn alle Queues fertig sind, reicht 1× pro Stunde zum Pruefen ob Sources bald ablaufen.
3.9 Sourceless Filme in der Liste kennzeichnen
Filme ohne Quellen nach Full-Scan in der UI markieren — User weiss VOR dem Klick dass nichts verfuegbar ist.
Umsetzung (in sources.py / listitem.py):
# Bei Cache-Hit: Pruefen ob Film sourceless + fully_scannedifnotcached_sourcesandsourcecacheDB.is_fully_scanned(imdb):
# Titel markierenlabel='[COLOR grey]%s[/COLOR] [keine Quellen]'%title# Oder im Plot ergaenzenplot='Keine Streaming-Quellen gefunden.\n\n'+original_plot
Trotzdem Foreground-Scan starten (vielleicht hat sich was geaendert)
Oder sofort "Keine Quellen verfuegbar" Dialog zeigen (spart Zeit)
Konfigurierbar via Setting?
4. Offene Fragen
Watched-Erkennung: playcountDB nur fuer xShip-Wiedergaben. Kodis videodb per JSON-RPC abfragen (VideoLibrary.GetMovies mit filter: playcount > 0)? Oder reicht playcountDB?
TTL pro Source vs pro Film: Aktuell TTL pro Source (jede hat eigenes expires_at). Pro Film waere einfacher (ein Datum fuer alle Sources), aber weniger flexibel.
Aggressivitaet: Wie viele Filme pro Wartungs-Zyklus? Zu wenige = Cache laeuft trotzdem ab. Zu viele = wieder Burst-artig.
Separater Modus oder integriert?: Eigener _run_maintenance() oder in _run_cycle() einbauen? Letzteres ist einfacher, aber weniger uebersichtlich.
Neue Sources bei Rescan: Wenn ein Provider beim Rescan neue Quellen liefert (die beim Erstscann nicht da waren) — voller Probe-Zyklus oder nur neue Sources proben?
Rotations-Persistenz: Window Property geht bei Kodi-Neustart verloren. Besser in DB speichern?
5. Naechste Schritte
get_expiring_movies() in sourcecacheDB.py — Query fuer bald ablaufende Filme
Wartungs-Modus in _run_cycle() — Initial vs. Wartung unterscheiden
precache.skip_watched Setting — playcountDB-Abfrage in Scan-Queue einbauen
Throttle fuer Wartung — Max Filme pro Zyklus konfigurierbar
Extra-Listen TMDB-Fetcher — _fetch_movie_ids_popular(), _fetch_movie_ids_toprated() etc.
Listen-Rotation — Rotations-Index + Setting pro Liste
Refresh-Score Query — Prioritaetsbasierte Reihenfolge in Wartungsmodus
Testing — Kuerzere TTL (z.B. 1 Tag) zum Testen des Wartungsmodus
Aktuell: _probe_single_pooled() blockiert einen Pool-Slot fuer die gesamte Dauer (Resolve 5-15s + MediaInfo 1-3s = 6-18s). Bei Pool-Groesse 5 heisst das max 5 gleichzeitige Probes. Resolve ist der Bottleneck (80% der Zeit), MediaInfo ist schnell.
Loesung: 3 Queues
Statt eines einzelnen Pools: 3 spezialisierte Queues mit eigenem Busy-Tracking.
Queue 1: SCAN (Provider aufrufen → Quellen finden)
Pool: 5 Worker
Input: (imdb_id, provider_name)
Output: Sources in DB (save_sources)
Busy: movie_provider.hits IS NULL (5s Timeout)
Queue 2: RESOLVE (Hoster-URL → Stream-URL)
Pool: 5 Worker
Input: source aus DB (hoster-URL)
Output: resolved_url in source-Tabelle (neue Spalte)
Busy: hoster.busy_since (20s Timeout)
Queue 3: MEDIAINFO (Stream-URL → Resolution/Bitrate)
Pool: 3 Worker
Input: source mit resolved_url
Output: probe_width/height/bitrate/codec in source-Tabelle
Busy: Kein DB-Tracking noetig (schnell, kein Hoster-Limit)
Dispatch-Zyklus (neu)
while _is_active():
free = pool_size - len(futures)
if free <= 0: wait(FIRST_COMPLETED), continue
# Alle 3 Queues abfragen
scans = get_dispatchable_scans()
resolves = get_dispatchable_resolves()
mediainfos = get_dispatchable_mediainfos()
# Pipeline vorwaerts: erst fertige Items durchpushen
# 1. Resolves zuerst (warten auf Scan-Ergebnisse)
for src in resolves[:free]:
dispatch_resolve(src.hoster)
submit(_resolve_single, src)
free -= 1
# 2. MediaInfos (warten auf Resolve-Ergebnisse)
for src in mediainfos[:free]:
submit(_mediainfo_single, src)
free -= 1
# 3. Scans (neue Sources erzeugen — restliche Slots)
for scan in scans[:free]:
submit(_scan_single, ...)
free -= 1
sleep(1)
# Ergebnisse verarbeiten
wait(FIRST_COMPLETED) → _process_result()
Prioritaet: Resolve > MediaInfo > Scan
Pipeline vorwaerts: erst fertige Items durchpushen, restliche Slots fuer neue Scans. Bei Cold-Start sind Resolve/MediaInfo-Queues leer, alle Slots gehen an Scans. Sobald erste Scan-Ergebnisse da sind, fuellen Resolves und MediaInfos die Slots natuerlich auf. Kein kompliziertes Balancing noetig.
DB-Aenderungen
source-Tabelle: resolved_url Spalte
ALTERTABLE source ADD COLUMN resolved_url TEXT DEFAULT NULL;
-- NULL = noch nicht resolved-- '' = Resolve fehlgeschlagen-- URL = aufgeloeste Stream-URL
Beim ersten erfolgreichen MediaInfo-Probe eines Hosters: stream_type setzen.
Danach: Typ-Erkennung (8KB peek / HEAD) ueberspringen, direkt zum richtigen Prober.
Dispatch-Queries
get_dispatchable_resolves()
SELECT s.*FROM source s
LEFT JOIN hoster h ONs.hoster=h.nameWHEREs.expires_at> ?
ANDs.resolved_url IS NULL-- noch nicht resolvedANDs.probe_failed=0-- nicht fehlgeschlagenAND (h.busy_since IS NULLOR (? -h.busy_since) >20) -- Hoster frei-- HD-Skip: Filme mit HD-Quelle ueberspringenAND NOT EXISTS (
SELECT1FROM source s2 WHEREs2.imdb_id=s.imdb_idANDs2.probe_height>=720ANDs2.expires_at> ?
)
GROUP BYs.hoster-- 1 pro HosterORDER BYs.created_at
get_dispatchable_mediainfos()
SELECT s.*FROM source s
WHEREs.expires_at> ?
ANDs.resolved_urlIS NOT NULL-- resolvedANDs.resolved_url!=''-- nicht fehlgeschlagenANDs.probe_at IS NULL-- noch nicht geprobtANDs.probe_failed=0-- HD-Skip: Filme mit HD-Quelle ueberspringenAND NOT EXISTS (
SELECT1FROM source s2 WHEREs2.imdb_id=s.imdb_idANDs2.probe_height>=720ANDs2.expires_at> ?
)
ORDER BYs.created_atLIMIT ? -- max free_slots
Kein Hoster-Busy-Check noetig — MediaInfo greift nicht auf den Hoster zu (nur auf die CDN-URL).
get_dispatchable_probes() (Legacy)
Bleibt als Fallback fuer Quellen die NOCH den alten Weg nutzen (combined resolve+mediainfo).
Langfristig: entfernen.
Alle Priority-1 Provider sind gleichwertig — der erste in der Scraper-Load-Order wird immer zuerst aufgerufen. Kein Mechanismus um schnelle/zuverlaessige Provider zu bevorzugen.
Loesung
provider Tabelle in sourcecache.db trackt pro Provider:
Priority (aus Scraper, aktualisiert bei jedem Sync)
Durchschnittliche Geschwindigkeit (wie lange scraper.run() dauert)
Hit-Rate (wie oft der Provider mindestens 1 Quelle liefert)
Szenarien: Was passiert wenn der User einen Film anklickt?
Szenario 1: Cache-Hit mit HD — Sofort fertig
Ausgangslage: Film wurde bereits gescannt + geprobt, HD-Quelle vorhanden.
User klickt Film
→ get_sources() → 14 Sources in DB
→ sourcesFilter() → Top-3 (alle mit Probe-Daten)
→ Anzeige: 3 Sources mit echter Resolution (z.B. 1920x1080)
→ bg_scan = False (HD vorhanden)
→ BG-Probe: alle 3 haben _probe → "Alle bereits geprobt, ueberspringe"
→ Fertig. Keine Wartezeit.
Dauer: ~15ms (DB lookup)
Szenario 2: Cache-Hit ohne HD, ungeprobte Quellen
Ausgangslage: Film hat gecachte Sources, aber keine geprobte HD-Quelle.
Einige Sources wurden vorher geprobt, andere nicht.
User klickt Film
→ get_sources() → 14 Sources in DB
→ sourcesFilter():
- probe_failed Sources werden entfernt
- Score-Sortierung (geprobte oben, ungeprobte unten)
- Top-3
→ Anzeige: z.B. 1 mit Resolution, 2 mit Scraper-Label ("1080p")
→ bg_scan = True (kein HD + ungeprobte Quellen)
BG-Probe startet (Hintergrund-Thread):
→ Filtert bereits geprobte Sources raus
→ Probt nur die 2 ungeprobten (spart ~5s pro bereits geprobte)
→ Jede Probe: Resolve (2-15s) + MediaInfo (1-3s)
→ Ergebnis in DB speichern
Falls Probe fehlschlaegt:
→ Source bekommt _probe.failed = True
→ Wird nach Container.Refresh nicht mehr angezeigt
→ Platz frei fuer naechste Source aus DB
Container.Refresh:
→ Liste wird neu geladen mit echten Resolutions
→ Failed Sources verschwinden
→ User sieht nur funktionierende Quellen
Dauer: ~5-35s (abhaengig von Hoster-Antwortzeiten)
Szenario 3: Cache-Hit, kein HD, alle geprobt, Provider uebrig
Ausgangslage: Alle Sources geprobt, keine HD, aber noch Provider die nicht gescannt wurden.
User klickt Film
→ get_sources() → Sources aus DB (alle < 720p)
→ Anzeige: Top-3 mit echten Resolutions (z.B. 640x360)
→ bg_scan = True (kein HD + nicht fully_scanned)
BG-Scan startet:
→ DialogProgressBG oben rechts ("Suche Quellen...")
→ Neue Provider werden gescannt (ThreadPoolExecutor, max 10 Worker)
→ Gecachte Provider uebersprungen
→ Neue Sources in DB + Merge mit bestehenden
→ Container.Refresh alle 5-10s (Liste waechst)
→ Early Exit nach 3 neuen Quellen
BG-Probe fuer neue Sources:
→ Resolve + MediaInfo fuer ungeprobte
→ HD gefunden? → Fertig
→ Container.Refresh mit aktualisierten Daten
Dauer: 10-60s (abhaengig von Anzahl uebrige Provider)
Szenario 4: Cache-Hit, kein HD, alle Provider durch
Ausgangslage: Alle Provider gescannt, alle Sources geprobt, keine HD.
User klickt Film
→ get_sources() → Sources aus DB
→ Anzeige: Top-3 (beste verfuegbare Qualitaet, z.B. SD)
→ bg_scan = False (fully_scanned + kein HD = done)
→ BG-Probe: bereits geprobte werden uebersprungen
→ Fertig. Film hat einfach keine HD-Quellen.
Dauer: ~15ms
Szenario 5: Cache-Miss — Frischer Scan
Ausgangslage: Film wurde noch nie gescannt (kein Cache-Eintrag).
User klickt Film
→ get_sources() → 0 Sources in DB
→ Foreground-Scan mit Progress-Dialog (Ladekreis)
→ 16 Scraper parallel (ThreadPoolExecutor)
→ Early Exit nach 3 Quellen
→ Sources in DB speichern
→ sourcesFilter() → Top-3
→ Anzeige: 3 Sources mit Scraper-Labels
BG-Probe startet:
→ Probt alle 3 (keine hat _probe Daten)
→ Container.Refresh nach letzter Probe
→ Echte Resolutions sichtbar
→ Failed Sources verschwinden
Dauer: 5-30s (Foreground-Scan) + 5-35s (BG-Probe)
Szenario 6: Probe schlaegt fehl (Hoster down)
Ausgangslage: Source wird geprobt, aber Resolve oder MediaInfo schlaegt fehl.
BG-Probe [2/3] Resolve: moflix / FileLions
→ 31s Timeout → Keine MediaInfo
→ _probe = {failed: True, height: 0}
→ In DB gespeichert (probe_failed = 1)
Container.Refresh:
→ sourcesFilter() entfernt probe_failed Sources
→ Liste schrumpft (z.B. 3 → 2)
→ Naechster Cache-Hit: FileLions nicht mehr in Top-N
(Score 1, wird durch bessere Source ersetzt)
Szenario 7: Precacher im Hintergrund (service.py)
Ausgangslage: User browst in xShip, Precacher laeuft unsichtbar.
service.py (alle 10s):
→ xShip aktiv? → Precacher starten
→ TMDB Discover → Populaere Filme in movie-Tabelle
→ v_movie_phase VIEW berechnet Phase pro Film:
- Phase 1a: Kein Source → 1 Provider scannen
- Phase 1b: Ungeprobte Sources → Proben
- Phase 2: Kein HD + Provider uebrig → Weitere scannen
- done: HD gefunden oder alle Provider durch
→ Pool-Dispatcher arbeitet Filme nach Phase ab
→ Wenn User spaeter Film anklickt: Cache-Hit (Szenario 1 oder 2)
Dauer: Laeuft kontinuierlich im Hintergrund
Filter-Reihenfolge in sourcesFilter()
Sources aus DB (z.B. 14)
→ Qualitaets-Filter (4K/1080p/720p je nach Setting)
→ Dedup (1 pro provider+hoster+quality)
→ Score-Sortierung (geprobte oben, ungeprobte unten)
→ probe_failed entfernen (keine MediaInfo = unbrauchbar)
→ Top-N abschneiden (default 3)
→ Anzeige
BG-Probe Optimierung
Bereits geprobte Sources (mit _probe Daten) werden uebersprungen
Nur ungeprobte Sources werden resolved + geprobt
Spart ~5-15s pro bereits geprobte Source
Log: [BG-Probe] Start: 2/3 Quellen proben (1 bereits geprobt)
Wenn alle bereits geprobt: [BG-Probe] Alle 3 Quellen bereits geprobt, ueberspringe