Skip to content

Instantly share code, notes, and snippets.

@CarstenG2
Created March 16, 2026 17:26
Show Gist options
  • Select an option

  • Save CarstenG2/cb5e0aa51cc69ccd914347246a842ce5 to your computer and use it in GitHub Desktop.

Select an option

Save CarstenG2/cb5e0aa51cc69ccd914347246a842ce5 to your computer and use it in GitHub Desktop.
xShip Precache Design-Docs (DB-Schema, Dispatcher, Settings)

DB Schema (sourcecache.db)

Tabellen

source — Gecachte Streaming-Quellen

CREATE TABLE IF NOT EXISTS source (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    imdb_id TEXT NOT NULL,
    provider TEXT NOT NULL,
    hoster TEXT NOT NULL,
    quality TEXT,
    url TEXT NOT NULL,
    direct INTEGER DEFAULT 0,
    language TEXT DEFAULT 'de',
    info TEXT DEFAULT '',
    priority INTEGER DEFAULT 100,
    prio_hoster INTEGER DEFAULT 100,

    -- Pipeline: Resolve-Ergebnis (NULL = noch nicht resolved)
    resolved_url TEXT DEFAULT NULL,
    -- NULL = noch nicht resolved
    -- '' = Resolve fehlgeschlagen
    -- URL = aufgeloeste Stream-URL

    -- Probe-Daten (NULL = noch nicht geprobt)
    probe_width INTEGER,
    probe_height INTEGER,
    probe_bitrate TEXT,
    probe_audio_lang TEXT,
    probe_has_audio INTEGER DEFAULT 1,
    probe_duration TEXT,
    probe_codec TEXT,
    probe_failed INTEGER DEFAULT 0,  -- 0=offen, 1=echt failed/done, 2=Label-Skip (reversibel)
    probe_at INTEGER,

    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL,

    UNIQUE(imdb_id, provider, hoster, quality)
);

movie_status — Film-Scan-Status

CREATE TABLE IF NOT EXISTS movie_status (
    imdb_id TEXT PRIMARY KEY,
    scan_at INTEGER,
    scan_source_count INTEGER DEFAULT 0,
    probe_at INTEGER,
    probe_count INTEGER DEFAULT 0,
    probe_ok_count INTEGER DEFAULT 0
);

movie — Persistente Filmliste (DB-Driven Precache)

CREATE TABLE IF NOT EXISTS movie (
    imdb_id TEXT PRIMARY KEY,
    tmdb_id INTEGER,
    title TEXT NOT NULL,
    originaltitle TEXT DEFAULT '',
    year TEXT DEFAULT '',
    added_at INTEGER NOT 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

Siehe DB-DRIVEN.md fuer Details.

movie_provider — Junction: Film → aufgerufene Provider

CREATE TABLE IF NOT EXISTS movie_provider (
    imdb_id TEXT NOT NULL,
    provider TEXT NOT NULL,
    called_at INTEGER NOT NULL,
    hits INTEGER DEFAULT 0,        -- NULL = in-flight, 0 = kein Treffer, >0 = Anzahl Quellen
    PRIMARY KEY (imdb_id, provider)
);
  • hits IS NULL: Scan dispatcht aber noch kein Ergebnis (in-flight)
  • hits = 0: Provider aufgerufen, keine Quellen gefunden
  • hits > 0: Provider hat N Quellen gefunden

provider — Provider-Statistiken + Medientyp + Domain-Health

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,

    -- Medientyp: 'movie' (nur Filme), 'series' (nur Serien), 'all' (beides)
    mediatype TEXT DEFAULT 'all',

    -- Domain-Health: 'ok', 'cf_blocked', 'dead', 'unknown'
    domain_status TEXT DEFAULT 'unknown'
);
  • mediatype wird aus _MEDIATYPE_MAP oder scraper.mediatype Attribut gesetzt (via sync_providers())
    • Serien-Provider (burningseries, aniworld, serienstream) werden beim Movie-Precache uebersprungen
    • Movie-Provider (einschalten, netzkino) wuerden bei eventuellem Serien-Precache uebersprungen
    • Default 'all' = bedient beides
  • domain_status wird von service.py nach dem Domain-Check geschrieben (via update_domain_status())
    • 'dead' Provider werden immer uebersprungen
    • 'cf_blocked' Provider nur uebersprungen wenn KEIN FlareSolverr konfiguriert
    • 'unknown' und 'ok' Provider bleiben aktiv

Siehe PROVIDER-STATS.md fuer Score-Formel und Tracking.

hoster — Hoster-Statistiken + Busy-Tracking + Stream-Type

CREATE TABLE IF NOT EXISTS hoster (
    name TEXT PRIMARY KEY,
    last_used_at INTEGER DEFAULT 0,
    call_count INTEGER DEFAULT 0,
    ok_count INTEGER DEFAULT 0,
    fail_count INTEGER DEFAULT 0,
    busy_since INTEGER DEFAULT NULL,  -- NULL = frei, timestamp = Resolve in-flight
    stream_type TEXT DEFAULT NULL      -- NULL = unbekannt, 'mp4'/'hls'/'dash' = gelernt
);
  • 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.

CREATE TRIGGER IF NOT EXISTS source_deleted_cleanup
AFTER DELETE ON source
BEGIN
    DELETE FROM movie_provider
    WHERE imdb_id = OLD.imdb_id AND provider = OLD.provider
    AND NOT EXISTS (
        SELECT 1 FROM source WHERE imdb_id = OLD.imdb_id
        AND provider = OLD.provider AND 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).

CREATE VIEW IF NOT EXISTS v_movie_phase AS
SELECT m.imdb_id, m.tmdb_id, m.title, m.originaltitle, m.year,
       m.priority, m.added_at, m.source AS movie_source,
    CASE
        -- NUR Film-faehige Provider zaehlen (mediatype IN ('movie','all'))
        -- Serien-Provider werden ignoriert, blockieren keine Phase
        WHEN sources=0 AND alle_film_provider_aufgerufen THEN 'done'
        WHEN sources=0                                    THEN '1a'
        WHEN hd_sources > 0                               THEN 'done'
        WHEN unprobed_sources > 0                         THEN '1b'
        WHEN NOT alle_film_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)

  1. Keine Sources + alle Provider → done
  2. Keine Sources + Provider uebrig → 1a
  3. HD gefunden → done (VOR unprobed-Check!)
  4. Ungeprobte Sources (excl. probe_failed) → 1b
  5. Kein HD + Provider uebrig → 2
  6. 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 Film
get_phase(imdb_id)      # SELECT phase FROM v_movie_phase WHERE imdb_id = ?

Indizes

CREATE INDEX IF NOT EXISTS idx_source_imdb ON source(imdb_id);
CREATE INDEX IF NOT EXISTS idx_source_expires ON source(expires_at);
CREATE INDEX IF NOT EXISTS idx_source_probe ON source(imdb_id, probe_height);
CREATE INDEX IF NOT EXISTS idx_movie_tmdb ON movie(tmdb_id);
-- Covering-Index: alle 3 LEFT JOINs in v_movie_phase als Index-Only-Scan
CREATE INDEX IF 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:

  1. JSON → Relational: source_cache + scan_statussource + movie_status
  2. called_providers + phase — entfernt (Legacy, movie_provider + v_movie_phase ersetzen beides)
  3. hoster Stats: call_count, ok_count, fail_count Spalten
  4. hoster busy_since: busy_since INTEGER DEFAULT NULL
  5. hoster stream_type: stream_type TEXT DEFAULT NULL (Pipeline)
  6. source resolved_url: resolved_url TEXT DEFAULT NULL (Pipeline)
  7. movie_provider hits: hits INTEGER DEFAULT 0
  8. Probe-Reset: Sources ohne Dauer (alte Probes) werden zurueckgesetzt
  9. movie-Tabelle: Persistente Filmliste + v_movie_phase VIEW + idx_movie_tmdb Index
  10. provider mediatype: mediatype TEXT DEFAULT 'all' (Medientyp-Filter)
  11. provider domain_status: domain_status TEXT DEFAULT 'unknown' (Domain-Health)

Settings

Vollstaendige Liste aller 22 Settings: siehe variables_and_constants.md.

DB-relevante Settings:

ID Typ Default Beschreibung
precache.ttl slider (1-30) 14 Cache-Gueltigkeit in Tagen
precache.scan_cooldown slider (24-168) 72 Scan-Cooldown in Stunden
precache.max_movies slider (100-2000) 1000 Max. Filme in DB

probe_failed Werte

Wert Bedeutung Reversibel?
0 Offen (noch nicht geprobt oder zurueckgesetzt)
1 Echt fehlgeschlagen oder nach Probe bestaetigt + Skip Nein (permanent)
2 Per Provider-Label uebersprungen (probe=off, HD-Label gefunden) Ja (reset_label_skipped())
  • get_sources() zeigt probe_failed IN (0, 2) — label-skipped Sources sind sichtbar
  • get_dispatchable_*() nutzt probe_failed = 0 — nur offene Sources werden dispatched
  • reset_label_skipped(): setzt alle 2er auf 0 wenn Probing aktiviert wird
  • VIEW v_movie_phase: probe_failed = 0 fuer unprobed/hd Counts — 2er zaehlen weder als unprobed noch als HD

3-Queue Pipeline Dispatcher

Ueberblick

_run_loop() in precacher.py nutzt ThreadPoolExecutor(5) mit 3 spezialisierten Queues:

  1. Scan (Provider → Quellen finden)
  2. Resolve (Hoster-URL → Stream-URL via ResolveURL)
  3. MediaInfo (Stream-URL → Resolution/Bitrate/Audio)

Prioritaet: Resolve > MediaInfo > Scan. Pipeline vorwaerts: erst fertige Items durchpushen, restliche Slots fuer neue Scans.

Dispatch-Zyklus

while _is_active():
    1. Freie Slots = pool_size(5) - len(futures)
    2. Pool voll? → wait(FIRST_COMPLETED), continue
    3. Alle 3 Queues abfragen
    4. Resolves dispatchen (max free_slots) → free_slots -= dispatched
    5. MediaInfos dispatchen (max free_slots) → free_slots -= dispatched
    6. Scans dispatchen (max free_slots) → free_slots -= dispatched
    7. sleep(1)
    8. wait(FIRST_COMPLETED) → _process_result()
    9. Keine Futures + Queues leer? → idle_rounds++ → break nach 3
   10. Keine Futures + Queues nicht leer? → sleep(3) (Hosters temporaer busy)

Slot-Verwaltung

  • Pool-Groesse: Konfigurierbar via precache.pool_size (Default 5, Gedrosselt: pool_size_throttled Default 1)
  • Pause: Konfigurierbar via precache.pause (Default 3s, Gedrosselt: pause_throttled Default 10s)
  • Kein Over-Queuing: Nur free_slots Items pro Runde dispatcht
  • Slot-Filling: Sobald ein Slot frei wird (FIRST_COMPLETED), sofort naechstes Item dispatchen
  • Queue-Prioritaet: Resolve > MediaInfo > Scan (Pipeline vorwaerts, Scans bekommen Rest)
  • 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:

  • Dispatch: INSERT (imdb, provider, called_at=now, hits=NULL) — NULL = in-flight
  • Fertig: UPDATE hits = count (0 oder >0)
  • Busy-Check: hits IS NULL AND (now - called_at) <= 5 → Provider beschaeftigt (5s Scan-Timeout)
  • (now - called_at) > 5 mit hits IS NULL → Request verloren, Provider frei fuer ANDERE Filme
  • Called-Check: get_called_providers() gibt ALLE Provider zurueck (inkl. in-flight) — verhindert Re-Dispatch fuer denselben Film

Hoster-Busy (hoster.busy_since)

  • Dispatch: UPDATE busy_since = now (vor Resolve-Dispatch)
  • Busy-Check: busy_since IS NOT NULL AND (now - busy_since) <= 20 → Hoster beschaeftigt (20s Resolve-Timeout)
  • (now - busy_since) > 20 → Request verloren oder fertig, Hoster frei
  • 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)

def get_dispatchable_scans(movie_imdb_ids, available_providers,
                           mediatype='movie', has_flaresolverr=False):
    busy_providers = SELECT DISTINCT provider FROM movie_provider
                     WHERE hits IS NULL AND (now - called_at) <= 5
    free_providers = available - busy
    for imdb_id in movie_imdb_ids:
        phase = get_phase(imdb_id)  # nur 1a oder 2
        called = get_called_providers(imdb_id)  # ALLE (inkl. in-flight)
        # get_ordered_providers() filtert nach:
        # - mediatype: Serien-Provider raus bei Movie-Scan (und umgekehrt)
        # - domain_status: tote/CF-blockierte Provider raus (CF nur ohne FlareSolverr)
        for name in get_ordered_providers(mediatype, skip_dead=True, has_flaresolverr):
            if name in free_providers and name not in called:
                items.append((imdb_id, name))
                free_providers.discard(name)  # Provider nur 1x pro Runde
                break

HD-Skip: get_phase() gibt done zurueck wenn Film HD hat → Film nicht in Scan-Queue.

Provider-Filter

Filter Effekt
mediatype='movie' Serien-Provider (burningseries, aniworld, serienstream) uebersprungen
domain_status='dead' Tote Domains immer uebersprungen
domain_status='cf_blocked' CF-blockierte Domains nur uebersprungen ohne FlareSolverr
domain_status='unknown'/'ok' Provider aktiv

Resolve-Dispatch (Queue 2)

Pro Runde: 1 Resolve pro freien Hoster (max restliche free_slots)

def get_dispatchable_resolves():
    SELECT s.* FROM source s
    LEFT JOIN hoster h ON s.hoster = h.name
    WHERE s.resolved_url IS NULL AND s.probe_at IS NULL AND s.probe_failed = 0
    AND (h.busy_since IS NULL OR (now - h.busy_since) > 20)
    AND NOT EXISTS (  -- HD-Skip
        SELECT 1 FROM source s2 WHERE s2.imdb_id = s.imdb_id
        AND s2.probe_height >= 720 AND s2.expires_at > now)
    GROUP BY s.hoster  -- 1 pro Hoster

MediaInfo-Dispatch (Queue 3)

Pro Runde: alle resolved Sources die noch nicht geprobt sind (max restliche free_slots)

def get_dispatchable_mediainfos():
    SELECT s.* FROM source s
    WHERE s.resolved_url IS NOT NULL AND s.resolved_url != ''
    AND s.probe_at IS NULL AND s.probe_failed = 0
    AND NOT EXISTS (  -- HD-Skip
        SELECT 1 FROM source s2 WHERE s2.imdb_id = s.imdb_id
        AND s2.probe_height >= 720 AND s2.expires_at > now)
    ORDER BY s.created_at

Kein Hoster-Busy-Check — MediaInfo greift auf CDN-URL zu, nicht auf Hoster.

HD-Skip (Film-Level)

Alle 3 Queues pruefen: Hat der Film bereits eine HD-Quelle (probe_height >= 720)?

  • Scans: get_phase() gibt done zurueck → Film nicht in Scan-Queue
  • Resolves: NOT EXISTS (... probe_height >= 720 ...) in Query
  • MediaInfos: Gleicher Check in Query

Wenn erste Probe fuer Film X HD findet → alle weiteren Sources fuer Film X werden automatisch uebersprungen (naechste Dispatch-Runde sieht HD in DB).

Worker-Funktionen

_resolve_single(src, provider_sources)

Pipeline Schritt 1: Hoster-URL → Stream-URL.

  1. _resolve_source(provider_sources, src) via ResolveURL
  2. Erfolg: set_resolved_url(url) + hoster_touch(success=True)
  3. Fehler: set_resolved_url('') + update_probe(failed=True) + hoster_touch(success=False)

_mediainfo_single(src)

Pipeline Schritt 2: Stream-URL → Resolution/Bitrate.

  1. get_hoster_stream_type(hoster) — bekannter Typ? → skip Typ-Erkennung
  2. getMediaInfo(resolved_url, NullDialog, deadline, stream_type_hint=...)
  3. Parse: _parse_probe_info(info_str)
  4. Speichern: update_probe(probe_data)
  5. Stream-Type lernen: wenn Hoster-Typ unbekannt, aus Probe-Ergebnis setzen

Hoster Stream-Type (Lern-Cache)

hoster.stream_type TEXT DEFAULT NULL
-- NULL → normaler Probe-Ablauf (Typ-Erkennung via 8KB peek)
-- 'mp4'/'hls'/'dash' → direkt zum richtigen Prober

Lern-Ablauf

  1. Erster Probe fuer Hoster X → normaler Ablauf (8KB peek + Typ-Erkennung)
  2. MediaInfo erkennt Typ (z.B. 'mp4') → hoster.stream_type = 'mp4'
  3. Alle weiteren Probes fuer Hoster X → kein 8KB peek, direkt _probeDirect()
  4. Spart ~200-400ms pro Probe

Scan-Modus / Drosselung

_get_scan_mode() prueft 3 Situationen (Settings: precache.on_playback, precache.on_background, precache.on_other_addon). Jede hat 3 Zustaende: 0=Normal, 1=Gedrosselt, 2=Aus. Strengste gewinnt (max()).

  • Mode 0 (Normal): pool_size + pause (Default: 5 Worker, 3s Pause)
  • Mode 1 (Gedrosselt): pool_size_throttled + pause_throttled (Default: 1 Worker, 10s Pause)
  • Mode 2 (Aus): _is_active() = False → Loop stoppt sofort

Drosselung wird pro Runde geprueft — aendert sich dynamisch (z.B. Video startet → sofort gedrosselt).

Loop-Ende

Loop endet wenn ALLE 3 Queues leer:

  • Keine Scans: kein Film in Phase 1a/2
  • Keine Resolves: get_dispatchable_resolves() leer
  • Keine MediaInfos: get_dispatchable_mediainfos() leer
  • Idle 3 Runden → break
  • Oder: _is_active() = False (User hat xShip verlassen)
  • 5-Min Intervall ist nur Start-Trigger fuer naechsten Zyklus, KEIN Timeout

_process_result()

Verarbeitet abgeschlossene Futures:

  • Scan: save_sources() + complete_scan(hits=N) → macht Provider frei
    • Wenn probe.enabled=false + HD-Label in Sources + skip_remaining=true: skip_remaining_sources(imdb, label_only=True) → setzt probe_failed=2 (reversibel)
  • Resolve: Ergebnis schon in DB (set_resolved_url + hoster_touch in _resolve_single)
  • MediaInfo: update_probe() + ggf. skip_remaining_sources(imdb) mit label_only=False (permanent) wenn HD geprobt + Bitrate/Filesize OK (_usable)
  • Exception: complete_scan(hits=0) bei Scan-Fehler, Hoster-Timeout laeuft automatisch ab

probe.enabled=false

  • Resolve + MediaInfo Queues werden leer gesetzt → nur Scan-Queue laeuft
  • HD-Skip ueber Provider-Labels (quality in 720p/1080p/1440p/4K) statt Probe-Height
  • skip_remaining_sources(imdb, label_only=True)probe_failed=2
  • reset_label_skipped() setzt 2→0 beim naechsten Start mit probe=true

BG-Scan: Variablen & Konstanten

Alle Variablen, Konstanten, Schwellenwerte und Einstellungen im Quellen-Precaching-System.

Konfigurierbar (Kodi-Einstellungen)

Tier 1: Quellen-Cache (Master)

Setting-ID Typ Default Bereich Beschreibung
cache.enabled bool true Master-Schalter: Cache ein/aus (deaktiviert ALLES darunter)
precache.ttl slider 14 Tage 1–30 (Step 1) Source-Cache-Gueltigkeit
precache.max_movies slider 1000 100–2000 (Step 100) Max. Filme in der DB, aelteste werden evicted
precache.clear action Loest clearSourceCache in default.py aus

Tier 2: Hintergrund-Scan

Setting-ID Typ Default Bereich Beschreibung
precache.enabled bool false BG-Scan ein/aus

Scan-Modus wenn... (3-State Enums: 0=Normal, 1=Gedrosselt, 2=Aus)

Setting-ID Typ Default Bereich Beschreibung
precache.on_background enum 1 (Gedrosselt) Normal/Gedrosselt/Aus Kodi im Hintergrund
precache.on_other_addon enum 2 (Aus) Normal/Gedrosselt/Aus Anderes Addon aktiv
precache.on_playback enum 1 (Gedrosselt) Normal/Gedrosselt/Aus Video laeuft

Mehrere Bedingungen gleichzeitig: strengste gewinnt (max()). _is_active() stoppt bei 2, _is_throttled() wechselt Pool/Pause bei 1.

Zu scannende Listen

Setting-ID Typ Default Bereich Beschreibung
precache.select_lists action MultiSelect-Dialog fuer Listen-Auswahl
precache.lists text (hidden) "" Komma-separierte aktive Listen-Indizes
precache.refresh_interval slider 2 Std. 1–24 (Step 1) TMDB-Listen aktualisieren alle N Stunden (×3600)
precache.list_limit slider 20 10–100 (Step 5) Max. Filme pro Liste (max_pages = (limit+19)//20)

Pro Film

Setting-ID Typ Default Bereich Beschreibung
precache.stop_quality enum 1 (720p) 480p/720p/1080p/4K Gesuchte Mindest-Qualitaet
precache.skip_remaining bool true Weitere Anbieter nach HD-Treffer ueberspringen
precache.scan_cooldown slider 72 h 24–168 (Step 12) Bessere Quellen suchen nach N Stunden

Scan-Geschwindigkeit (Normal)

Setting-ID Typ Default Bereich Beschreibung
precache.pool_size slider 5 1–10 (Step 1) Max. gleichzeitige Anfragen
precache.pause slider 3 s 1–30 (Step 1) Pause nach jeder Anfrage

Scan-Geschwindigkeit (Gedrosselt)

Setting-ID Typ Default Bereich Beschreibung
precache.pool_size_throttled slider 1 1–5 (Step 1) Max. gleichzeitige Anfragen
precache.pause_throttled slider 10 s 5–60 (Step 5) Pause nach jeder Anfrage

Wiedergabe / Dateinanbieter-Filter

Setting-ID Typ Default Bereich Beschreibung
probe.enabled bool true Reale Aufloesung ermitteln (Resolve + MediaInfo)
probe.max_bitrate slider 0 1–20 (Step 1) Max. Bitrate in Mbit/s (0=kein Limit)
probe.max_filesize slider 0 1–50 (Step 1) Max. Dateigroesse in GB (0=kein Limit)

Probe-Filter wirkt an 2 Stellen:

  1. precacher.py: Quelle mit zu hoher Bitrate/Dateigroesse zaehlt NICHT als HD-Treffer fuer skip_remaining
  2. sources.py: Geprobte Quellen mit zu hoher Bitrate/Dateigroesse aus Anzeige entfernt

probe.enabled=false:

  • Precacher: nur Scan-Queue (kein Resolve/MediaInfo)
  • HD-Skip via Provider-Labels (quality in 720p/1080p/1440p/4K) statt Probe-Height
  • Sources werden mit probe_failed=2 markiert (reversibel, siehe unten)
  • reset_label_skipped() setzt alle 2er zurueck auf 0 wenn Probing aktiviert wird

Hardcoded — Timing & Intervalle

Konstante Wert Datei Beschreibung
Service-Poll-Intervall 10 s service.py monitor.waitForAbort(10) — wie oft start_if_needed() gerufen wird
Domain-Check HTTP-Timeout 2 s service.py requests.head(timeout=2) pro Provider
Probe-Zyklus-Intervall 300 s (5 min) precacher.py Hardcoded, kein Setting
TTL Fallback 14 Tage sourcecacheDB.py _DEFAULT_TTL_DAYS
Scan-Cooldown Fallback 72 h sourcecacheDB.py _DEFAULT_SCAN_COOLDOWN_HOURS

Hardcoded — Pool & Dispatcher

Konstante Wert Datei Beschreibung
Busy-Wait (Queues voll) 3 s precacher.py Sleep wenn alle Hoster/Provider belegt
Idle-Wait (kein Work) 2 s precacher.py Sleep pro Idle-Runde
Max Idle-Runden 3 precacher.py Nach 3× Idle wird der Loop beendet

Hardcoded — Timeouts & Schwellenwerte

Konstante Wert Datei Beschreibung
MediaInfo-Deadline 15 s precacher.py Harte Deadline pro Probe
HD-Schwelle 720 px Hoehe precacher.py, sourcecacheDB VIEW probe_height >= 720 = HD
Scan-Dispatch-Timeout 5 s sourcecacheDB.py In-Flight Scan gilt nach 5s als verloren
Probe-Busy-Timeout 20 s sourcecacheDB.py Belegter Hoster wird nach 20s freigegeben
Min. Calls fuer Provider-Scoring 3 sourcecacheDB.py Provider mit <3 Calls bekommen neutralen Score

Hardcoded — Fokus-Gate

Konstante Wert Datei Beschreibung
_IDLE_THRESHOLD 300 s (5 min) precacher.py Min. Kodi-Idle-Zeit bevor gescannt wird (ohne xShip-Fokus)

Logik: Scan laeuft nur wenn xShip aktiv ODER Kodi idle >= 5 Min. Trailer-Scan umgeht diesen Check.

Hardcoded — TMDB Discovery

Konstante Wert Datei Beschreibung
Filme-Neu Fenster 30 Tage precacher.py release_type=4|5|6 (Digital/Physical/TV)
Filme-Im-Trend Fenster 90 Tage precacher.py release_type=4|5|6, sortiert nach Popularitaet
Filme-Im-Kino Fenster 60 Tage precacher.py release_type=2|3 (Premiere+Kino)
Min. Stimmen (dynamisch) 20 precacher.py vote_count.gte=20 fuer Neu/Trend/Kino
Min. Stimmen (Top bewertet) 250 precacher.py vote_count.gte=250
TMDB API Key '86dd...' precacher.py Fest eingebrannt
Sprache/Region de-DE / DE precacher.py Discovery-Locale

Hardcoded — Film-Phasen (v_movie_phase VIEW)

Phase Bedingung Naechste Aktion
'1a' Keine Sources, min. 1 Provider noch nicht gerufen Scan: weitere Provider abfragen
'1b' Sources vorhanden, keine geprobt Probe: MediaInfo ausfuehren
'2' Kein HD, weitere Provider moeglich Fill-Gaps: zusaetzliche Provider
'done' HD gefunden ODER alle Provider gerufen Fertig, nie wieder dispatched

HD-Schwelle in der VIEW: probe_height >= 720 (3× referenziert).

Hardcoded — Prioritaeten

Element Wert Beschreibung
Source-Prioritaet Default 100 Falls Scraper keinen Wert setzt
Hoster-Prioritaet Default 100 Falls Scraper keinen Wert setzt
Trailer-Film-Prioritaet 1 Hoechste Prio (normal = 0)
Provider-Prioritaet Default 1 Basis-Prio fuer neue Provider

Hardcoded — Mediatype-Map (_MEDIATYPE_MAP in sourcecacheDB.py)

Wird von sync_providers() in die provider.mediatype Spalte geschrieben. Scraper mit eigenem mediatype-Attribut ueberschreiben die Map (zukunftssicher).

Provider Typ Effekt
burningseries series Wird beim Movie-Precache uebersprungen
aniworld series Wird beim Movie-Precache uebersprungen
serienstream series Wird beim Movie-Precache uebersprungen
einschalten movie Nur Filme (wuerde bei Serien-Precache uebersprungen)
netzkino movie Nur Filme (wuerde bei Serien-Precache uebersprungen)
Alle anderen all Default: Filme + Serien

Wirkung: get_ordered_providers(mediatype='movie') filtert Serien-Provider aus. v_movie_phase VIEW zaehlt nur mediatype IN ('movie','all') Provider fuer Phase-Berechnung.

Hardcoded — Domain-Check Status-Mapping (service.py → provider.domain_status)

_checkdomain() schreibt Ergebnis in domain_results dict, check_domains() ruft update_domain_status() auf.

HTTP-Status DB-Status Effekt
200, 300-399 ok Provider aktiv, wird gescannt
403, 503, 520 cf_blocked Cloudflare-blockiert, uebersprungen (ausser FlareSolverr aktiv)
Timeout/Exception dead Nicht erreichbar, uebersprungen
Redirect zu wrongDomain dead site-maps.cc, www.drei.at, notice.cuii.info
vavoo, movie4k (Hardcoded) ok Domain-Check uebersprungen, immer aktiv

DB-Schema Defaults

Spalte Default Beschreibung
source.probe_failed 0 0=offen, 1=echt fehlgeschlagen/fertig (permanent), 2=per Label uebersprungen (reversibel)
source.probe_has_audio 1 Audio vorhanden (Annahme bis Probe)
provider.domain_status 'unknown' Initialer Health-Status
provider.mediatype 'all' Default: bedient Filme + Serien
movie.priority 0 Normal; Trailer=1
movie.source 'discover' Label fuer nicht-benannte Listen

Window Properties (Cross-Invoker State)

Property Konstante Beschreibung
'xship.precache_running' _RUNNING_PROP Mutex: verhindert parallele Precache-Threads
'xship.precache_last_probe' _LAST_PROBE_PROP Unix-Timestamp: letzter Probe-Zyklus abgeschlossen
'xship.precache_last_refresh' _LAST_REFRESH_PROP Unix-Timestamp: letzter Refresh-Zyklus abgeschlossen
'xship.trailer_scan' _TRAILER_SCAN_PROP JSON-Payload fuer Trailer-Film-Scan (umgeht Idle-Check + Intervall)

Zusammenfassung: Konfigurierbar vs. Hardcoded

22 Einstellungen konfigurierbar via Kodi-Settings:

  • Tier 1 (Cache): cache.enabled, precache.ttl, precache.max_movies, precache.clear
  • Tier 2 (BG-Scan): precache.enabled
  • Scan-Modus: precache.on_background, precache.on_other_addon, precache.on_playback
  • Listen: precache.select_lists, precache.lists, precache.refresh_interval, precache.list_limit
  • Pro Film: precache.stop_quality, precache.skip_remaining, precache.scan_cooldown
  • Geschwindigkeit: precache.pool_size, precache.pause, precache.pool_size_throttled, precache.pause_throttled
  • Probe: probe.enabled, probe.max_bitrate, probe.max_filesize

~20 Werte hardcoded — Kandidaten fuer zukuenftige Einstellungen:

  • Idle-Threshold (300s)
  • MediaInfo-Deadline (15s)
  • HD-Schwelle (720px)
  • TMDB Zeitfenster (30/60/90 Tage)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment