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.
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)
Provider werden dynamisch sortiert: beste zuerst.
CREATE TABLE IF NOT EXISTS provider (
name TEXT PRIMARY KEY,
priority INTEGER DEFAULT 1,
call_count INTEGER DEFAULT 0,
hit_count INTEGER DEFAULT 0,
total_duration_ms INTEGER DEFAULT 0,
last_seen_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);call_count: Gesamtzahlrun()-Aufrufe (auch ohne Ergebnis)hit_count: Aufrufe mit >= 1 Quelletotal_duration_ms: Kumulative Dauer aller Aufrufe- Abgeleitet:
avg_speed = total_duration_ms / call_count,hit_rate = hit_count / call_count
Wenn call_count >= 3 (genug Daten):
score = (priority * 10) + (speed_factor * 3) + (hit_penalty * 5)
speed_factor = min(avg_speed_ms / 3000, 10) # 0-10 Skala
hit_penalty = (1 - hit_rate) * 5 # 100% hit = 0, 0% hit = 5
Wenn call_count < 3 (neue/kaum getestete Provider):
score = (priority * 10) + 5 # Mitte ihrer Priority-Klasse
Warum diese Gewichtung:
priority * 10dominiert: Priority-2 (Score >= 20) schlaegt nie Priority-1 durch Geschwindigkeit alleinspeed_factor * 3: 3s avg = +3, 15s avg = +15hit_penalty * 5: 80% Hit-Rate = +1, 20% Hit-Rate = +4- Neue Provider (< 3 Calls) bekommen Score in der Mitte
| Funktion | Beschreibung |
|---|---|
sync_providers(provider_list) |
UPSERT aller geladenen Scraper (priority + timestamps) |
update_provider_stats(name, duration_ms, hit) |
Atomares Update nach jedem scraper.run() |
get_ordered_providers() |
Provider-Namen sortiert nach Score (beste zuerst) |
get_provider_stats() |
Debug-Logging: calls, hits, avg_speed, hit_rate% |
| Datei | Stelle | Was |
|---|---|---|
precacher.py |
_scan_single() |
Timing + Hit nach jedem Scraper-Aufruf |
sources.py |
_getSource() |
Timing + Hit im Foreground-Scan |
sources.py |
_bg_scan_single() |
Timing + Hit im Background-Scan |
-- Teil der hoster-Tabelle (siehe DB-SCHEMA.md)
call_count, ok_count, fail_count, consecutive_fails, backoff_until, avg_response_mshoster_touch(name, success, duration_ms): Aktualisiert nach jeder Probe/Resolveget_hoster_stats(): Debug-Logging (inkl. consecutive_fails, backoff_until, avg_response_ms)
Dynamischer Durchschnitt statt kumulativem Mittel:
new_avg = 0.9 * old_avg + 0.1 * last_response_ms
- Reagiert in ~10 Calls auf aktuelle Aenderungen
- Seed: erster Call setzt avg_response_ms direkt auf den gemessenen Wert
- Spalte
avg_response_msinhosterUNDproviderTabelle - Provider: EMA in
update_provider_stats(), Score inget_ordered_providers() - Hoster: EMA in
hoster_touch(duration_ms=...), Dispatch-Sortierung in SQL total_duration_ms(Provider) bleibt fuer historische Daten, wird weiter aktualisiert
ORDER BY
movie.priority ASC,
CASE WHEN h.avg_response_ms > 0 THEN h.avg_response_ms ELSE 5000 END ASC,
s.created_atFilm-Prioritaet dominiert, innerhalb gleicher Prio schnellste Hoster zuerst. Neue Hoster (avg=0) bekommen neutralen Wert 5000ms.
Neue Spalte quality_trust REAL DEFAULT 1.0 in provider:
- Nach jeder Probe:
actual_height / claimed_heightberechnen - Provider die systematisch SD als HD labeln: Trust sinkt (z.B. 0.44)
- Trust fliesst in Score:
score *= (2 - trust)→ Trust 1.0 = neutral, Trust 0.5 = Score verdoppelt - Threshold:
actual >= 80% claimed= accurate
Hoster mit wiederholten Fehlern bekommen progressive Sperre:
1st fail -> 5s, 2nd -> 10s, 3rd -> 20s, 4th -> 30s, 5th -> 60s, 6th -> 300s, 7th+ -> 3600s (max)
Success resettet consecutive_fails auf 0.
consecutive_fails INTEGER DEFAULT 0-- Anzahl Fehler in Folge ohne Erfolgbackoff_until INTEGER DEFAULT 0-- Unix-Timestamp bis wann der Hoster gesperrt ist
| Kontext | Funktion | Logik |
|---|---|---|
| Vordergrund-Scan (sourcesFilter, Progress, Early-Exit, Cache-Dots) | hoster_healthy(name) |
consecutive_fails == 0 ODER backoff_until abgelaufen |
| BG-Precacher (Dispatch-Queries) | backoff_until <= now in SQL |
Zeitbasiert -- probiert nach Ablauf nochmal |
| BG-Probe (sources.py) | hoster_available(name, cooldown=0) |
Zeitbasiert wie Precacher |
hoster_healthy() prüft: keine Fehler ODER Backoff-Timer abgelaufen (neue Chance).
Damit funktioniert das System auch ohne BG-Precacher -- nach Ablauf des Timers
(max 1 Stunde) wird der Hoster automatisch wieder angezeigt.
Wenn nach Filter 0 Quellen übrig: "Keine Quellen gefunden" Notification mit Filmtitel.
hoster_touch(name, success) wird aufgerufen in:
| Datei | Stelle | Kontext |
|---|---|---|
precacher.py |
_resolve_single() |
BG-Precacher Resolve OK/Fail |
precacher.py |
_mediainfo_single() |
BG-Precacher Probe OK/Fail |
sources.py |
sourcesResolve() |
Vordergrund-Klick, Autoplay, Trailer |
sources.py |
_probe_single() |
BG-Probe nach Foreground-Scan |
Problem: Bei Netzwerk-Ausfall schlagen alle Hoster gleichzeitig fehl -> alle im Backoff -> naechster Scan zeigt 0 Quellen obwohl kein Hoster defekt ist.
Loesung: _last_success_at Modul-Level-Timestamp in sourcecacheDB.py.
- Wird bei jedem
hoster_touch(success=True)aufnowgesetzt hoster_touch(success=False)prueft: wenn_last_success_ataelter als 30s, war wahrscheinlich das Netz down -> Fail wird NICHT gezaehlt (kein Backoff-Increment)- Sobald das Netz wieder da ist und ein Hoster Erfolg hat, laeuft alles normal weiter
[xShip-Backoff] name: N consecutive fails, backoff Xs-- bei 4+ consecutive fails[sourcesFilter] N Quellen entfernt (Hoster fehlerhaft)-- Vordergrund-Filter[BG-Probe] N Quellen uebersprungen (Hoster im Backoff)-- BG-Probe-Filter- Hoster-Stats am Ende jedes Precache-Zyklus zeigen
backoff N fails
| Dot | Farbe | Bedingung |
|---|---|---|
| Grün | FF4CAF50 |
Gesunde Source mit probe_height >= 720 (oder HD-Label wenn Probing aus) |
| Gelb | FFFFEB3B |
Gesunde Sources vorhanden, aber kein HD (oder ungeprobt bei Probing an) |
| Rot | FFF44336 |
Scan fertig + keine Quellen, ODER alle Hoster im Backoff |
| Kein Dot | Film nicht gescannt / keine Sources in DB |
get_cached_tmdb_ids() gibt 3 Sets zurück:
hd_ids: mindestens 1 gesunde Source mit HD (Backoff-Hoster gefiltert)sd_ids: gesunde Sources vorhanden, aber kein HDbackoff_ids: Sources vorhanden, aber ALLE Hoster im Backoff
Probing-Logik für Dot-Farbe:
- Probing an + geprobt: reale Auflösung entscheidet (probe_height >= 720 = HD)
- Probing an + nicht geprobt: als SD behandeln (gelber Dot, unsicher)
- Probing aus: Provider-Label als Fallback (HD-Label = grüner Dot)
hoster_healthy() für Dots und Filter:
consecutive_fails == 0→ gesundconsecutive_fails > 0aberbackoff_untilabgelaufen → gesund (neue Chance)consecutive_fails > 0undbackoff_untilaktiv → nicht gesund
Damit funktioniert das System auch ohne BG-Precacher -- nach Ablauf des Timers (max 1 Stunde) wird der Hoster automatisch wieder angezeigt und bekommt eine neue Chance.
Vor der "Gespeicherte Quellen laden" Notification: prüfe ob gesunde Sources vorhanden.
Wenn alle Hoster im Backoff (keine gesunde Source im Cache):
- Cache-Hit überspringen (kein Popup, kein altes Ergebnis)
_force_scan = Truesetzen → alle Provider abfragen (kein skip_providers)- Early-Exit zählt nur gesunde Hoster (Backoff-Sources nicht mitzählen)
- Progress-Dialog zeigt nur gesunde Hoster-Sources
sourcesFilter()filtert Backoff-Hoster aus Ergebnisliste- Wenn 0 Quellen übrig: "Keine Quellen gefunden" mit Filmtitel als Heading
Dadurch verhält sich ein normaler Klick auf einen Film mit nur Backoff-Hostern automatisch wie ein Force-Scan -- ohne dass der User es explizit auslösen muss.
Wenn der User einen Film anklickt:
sources.py getSources():
+-- sourcecacheDB.get_sources(imdb) → cached sources
+-- Cache Hit: sofortige Anzeige (~15ms)
| +-- Ungeprobte Quellen? → _needs_bg_scan=True (erst proben)
| +-- Alle geprobt + HD (probe_height >= 720)? → fertig, kein BG-Scan
| +-- Alle geprobt + kein HD + is_fully_scanned()? → fertig
| +-- Alle geprobt + kein HD + nicht fully_scanned? → _needs_bg_scan=True
+-- Cache Miss: Foreground-Scan mit Progress-Dialog
+-- Ueberspringt gecachte Provider
+-- Early Exit nach 3 Quellen
KEINE Entscheidung auf Basis von Provider-Labels (720p, HD) — NUR Probe-Daten zaehlen.
| Zustand | _needs_bg_scan | Grund |
|---|---|---|
| Ungeprobte Quellen vorhanden | True | Erst proben, dann entscheiden |
| Alle geprobt + HD | False | Genug Qualitaet |
| Alle geprobt + kein HD + fully_scanned | False | Alle Provider liefen, kein HD vorhanden |
| Alle geprobt + kein HD + nicht fully_scanned | True | Weitere Provider koennten HD haben |
sources.py addItem() → Progressive List:
+-- control.idle() (closes busy circle)
+-- Render cached sources as directory listing
+-- If _needs_bg_scan + not already running:
| +-- Start daemon thread _run_background_scan()
| +-- DialogProgressBG with colored resolution counts
+-- endOfDirectory(updateListing=is_refresh)
_run_background_scan() (daemon thread):
+-- Seed all_found from existing cached sources
+-- Skip providers already in cache (cached_providers set)
+-- Load remaining scrapers, run with ThreadPoolExecutor(max_workers=10)
+-- Early exit after new_found >= 3
+-- For each completed scraper:
| +-- save_sources() to DB immediately (incremental)
| +-- Merge with existing sources (URL + provider/hoster dedup)
| +-- sourcesFilter() → rebuild labels
| +-- Container.Refresh every 5-10s (mit FolderPath Guard)
+-- On completion: is_fully_scanned() dynamisch
Sowohl BG-Probe als auch BG-Scan pruefen vor jedem Container.Refresh ob die Quellenliste
noch aktiv ist:
folder_path = xbmc.getInfoLabel('Container.FolderPath')
---
## Backoff-Vorab-Check im Cache-Hit-Pfad
Vor der "Gespeicherte Quellen laden" Notification: prüfe ob gesunde Sources vorhanden.
Wenn alle Hoster im Backoff (keine gesunde Source im Cache):
1. Cache-Hit überspringen (kein Popup, kein altes Ergebnis)
2. `_force_scan = True` setzen → alle Provider abfragen (kein skip_providers)
3. Early-Exit zählt nur gesunde Hoster (Backoff-Sources nicht mitzählen)
4. Progress-Dialog zeigt nur gesunde Hoster-Sources
5. `sourcesFilter()` filtert Backoff-Hoster aus Ergebnisliste
6. Wenn 0 Quellen übrig: "Keine Quellen gefunden" mit Filmtitel als Heading
Dadurch verhält sich ein normaler Klick auf einen Film mit nur Backoff-Hostern
automatisch wie ein Force-Scan -- ohne dass der User es explizit auslösen muss.
## Cache-Hit Path
Wenn der User einen Film anklickt:sources.py getSources(): +-- sourcecacheDB.get_sources(imdb) → cached sources +-- Cache Hit: sofortige Anzeige (~15ms) | +-- Ungeprobte Quellen? → _needs_bg_scan=True (erst proben) | +-- Alle geprobt + HD (probe_height >= 720)? → fertig, kein BG-Scan | +-- Alle geprobt + kein HD + is_fully_scanned()? → fertig | +-- Alle geprobt + kein HD + nicht fully_scanned? → _needs_bg_scan=True +-- Cache Miss: Foreground-Scan mit Progress-Dialog +-- Ueberspringt gecachte Provider +-- Early Exit nach 3 Quellen
## Entscheidungslogik (_needs_bg_scan)
KEINE Entscheidung auf Basis von Provider-Labels (`720p`, `HD`) — NUR Probe-Daten zaehlen.
| Zustand | _needs_bg_scan | Grund |
|---------|---------------|-------|
| Ungeprobte Quellen vorhanden | True | Erst proben, dann entscheiden |
| Alle geprobt + HD | False | Genug Qualitaet |
| Alle geprobt + kein HD + fully_scanned | False | Alle Provider liefen, kein HD vorhanden |
| Alle geprobt + kein HD + nicht fully_scanned | True | Weitere Provider koennten HD haben |
## BG-Scan (addItem)
sources.py addItem() → Progressive List: +-- control.idle() (closes busy circle) +-- Render cached sources as directory listing +-- If _needs_bg_scan + not already running: | +-- Start daemon thread _run_background_scan()
Einstiegspunkt: default.py action=scanSources -> scan_on_open=true -> play()
Verhalten in getSources() wenn _force_scan=True:
- Kein Cache-Hit -- gecachte Quellen werden NICHT angezeigt, frischer Scan sofort
- Kein skip_providers -- alle Provider werden abgefragt, auch bereits gecachte
- Early-Exit greift normal -- nicht alle Provider muessen antworten
- Probe-Daten bleiben -- bestehende Probe-Ergebnisse aus DB werden an neue Sources angehaengt
- Orphan-Cleanup -- nach dem Scan werden pro Provider (nur die tatsaechlich gelaufen sind) Sources aus der DB entfernt die nicht mehr geliefert wurden
scanSources
-> scan_on_open=true
-> play() -> getSources()
-> _force_scan=True: Cache-Hit ueberspringen
-> clearProperty('scan_on_open') (verhindert Doppel-Scan)
-> clearProperty('probe_done') (Probe-Phase neu starten)
-> Alle Provider scannen (kein skip_providers)
-> Early-Exit bei scrapers.early_exit Treffern
-> save_sources() pro Provider
-> remove_orphaned_sources() NUR fuer _completed_providers
(Provider die nicht gelaufen sind wegen Early-Exit: Sources bleiben)
remove_orphaned_sources(tmdb, provider, found_keys):
- Vergleicht DB-Sources mit frisch gefundenen Sources (hoster+quality als Key)
- Loescht Sources die der Provider nicht mehr liefert
- Provider mit 0 Sources: alle alten Sources dieses Providers geloescht
- Provider nicht gelaufen (Early-Exit): keine Aenderung
_quality_score(source): 3-stufiges Scoring:
- Geprobte Quellen:
100000 + bitrate_kbits + priority_bonus(immer vor ungeprobten!) - Ungeprobte Quellen:
priority_bonus(0..1960) - Fehlgeschlagene Probes: Score 1
priority_bonus = max(0, 100 - priority) * 20- Deterministischer Tiebreaker:
(-score, provider, source)