stremio/apps/master-api/src/odido/tweaks.ts
Jos Vooges | STH 7b25b266f8 Odido: refresh-on-watch i.p.v. fleet MPD-refresh.
Zet periodieke PlayChannel-storm uit; vernieuw bij kijken met gedeelde DB-cache, in-flight coalescing en fallback op laatste URL.
2026-09-26 05:11:10 +02:00

158 lines
8.9 KiB
TypeScript
Raw 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.

/** Alle Odido-gedrag dat via Scripts → Geavanceerd te tweaken is. */
export type OdidoTweaks = {
/** Max leeftijd gecachte MPD bij play (seconden). Default 1 uur. */
mpdTtlSeconds: number;
/** Periodieke achtergrond-refresh van alle Odido-MPD’s. */
mpdRefreshEnabled: boolean;
/** Hoe vaak de worker mag refresh’en (seconden). */
mpdRefreshIntervalSeconds: number;
/** Parallelle PlayChannel-calls tijdens refresh/sync. */
mpdRefreshConcurrency: number;
/** Pauzes tussen starts binnen een batch (ms). */
mpdRefreshStaggerMs: number;
/** Alleen enabled zenders refresh’en. */
mpdRefreshOnlyEnabled: boolean;
/** Bij play: verse cache gebruiken als jonger dan TTL. */
mpdPreferCacheOnPlay: boolean;
/** Bij play: als cache te oud is, meteen vernieuwen. */
mpdRefreshOnPlayIfStale: boolean;
/** Debug: altijd verse PlayChannel (negeert cache). */
mpdForceFreshOnPlay: boolean;
/** Optioneel: gecachte MPD even proberen vóór serveren. */
mpdProbeCachedBeforeServe: boolean;
mpdProbeTimeoutMs: number;
/** Bij mislukte probe: verse MPD ophalen. */
mpdFallbackFreshOnProbeFail: boolean;
/** Login-cookie cache (minuten). */
sessionCacheMinutes: number;
playTimeoutMs: number;
cdmTimeoutMs: number;
syncConcurrency: number;
/** Tijdens volledige sync ontbrekende ClearKeys via CDM ophalen. */
syncFetchKeysIfMissing: boolean;
/** Tijdens sync de MPD-cache vullen. */
syncRefreshMpdCache: boolean;
/** Tijdens sync automatisch EPG-id koppelen (alle XMLTV-bronnen). */
syncAutoMatchEpg: boolean;
/** Bestaande handmatige EPG-koppeling overschrijven bij sync. */
syncOverwriteEpgMatch: boolean;
/** Bij play ontbrekende keys alsnog via CDM. */
refetchKeysIfMissingOnPlay: boolean;
heartbeatEnabled: boolean;
heartbeatIntervalSeconds: number;
};
export const ODIDO_TWEAK_DEFAULTS: OdidoTweaks = {
/** Cache delen tussen kijkers; na TTL opnieuw PlayChannel bij watch. */
mpdTtlSeconds: 3600,
/** Fleet-refresh uit: alleen vernieuwen als iemand kijkt. */
mpdRefreshEnabled: false,
mpdRefreshIntervalSeconds: 3600,
mpdRefreshConcurrency: 4,
mpdRefreshStaggerMs: 75,
mpdRefreshOnlyEnabled: true,
mpdPreferCacheOnPlay: true,
mpdRefreshOnPlayIfStale: true,
mpdForceFreshOnPlay: false,
mpdProbeCachedBeforeServe: false,
mpdProbeTimeoutMs: 5000,
mpdFallbackFreshOnProbeFail: true,
sessionCacheMinutes: 20,
playTimeoutMs: 30_000,
cdmTimeoutMs: 60_000,
syncConcurrency: 4,
syncFetchKeysIfMissing: true,
/** Geen PlayChannel-storm tijdens sync; cache vult bij watch. */
syncRefreshMpdCache: false,
syncAutoMatchEpg: true,
syncOverwriteEpgMatch: false,
refetchKeysIfMissingOnPlay: true,
heartbeatEnabled: false,
heartbeatIntervalSeconds: 300,
};
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
const n = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(n)));
}
function asBool(value: unknown, fallback: boolean): boolean {
if (typeof value === "boolean") return value;
if (value === "true" || value === 1 || value === "1") return true;
if (value === "false" || value === 0 || value === "0") return false;
return fallback;
}
export function mergeOdidoTweaks(raw: Partial<OdidoTweaks> | null | undefined): OdidoTweaks {
const d = ODIDO_TWEAK_DEFAULTS;
const r = raw ?? {};
return {
mpdTtlSeconds: clampInt(r.mpdTtlSeconds, 60, 24 * 3600, d.mpdTtlSeconds),
mpdRefreshEnabled: asBool(r.mpdRefreshEnabled, d.mpdRefreshEnabled),
mpdRefreshIntervalSeconds: clampInt(r.mpdRefreshIntervalSeconds, 60, 24 * 3600, d.mpdRefreshIntervalSeconds),
mpdRefreshConcurrency: clampInt(r.mpdRefreshConcurrency, 1, 16, d.mpdRefreshConcurrency),
mpdRefreshStaggerMs: clampInt(r.mpdRefreshStaggerMs, 0, 5000, d.mpdRefreshStaggerMs),
mpdRefreshOnlyEnabled: asBool(r.mpdRefreshOnlyEnabled, d.mpdRefreshOnlyEnabled),
mpdPreferCacheOnPlay: asBool(r.mpdPreferCacheOnPlay, d.mpdPreferCacheOnPlay),
mpdRefreshOnPlayIfStale: asBool(r.mpdRefreshOnPlayIfStale, d.mpdRefreshOnPlayIfStale),
mpdForceFreshOnPlay: asBool(r.mpdForceFreshOnPlay, d.mpdForceFreshOnPlay),
mpdProbeCachedBeforeServe: asBool(r.mpdProbeCachedBeforeServe, d.mpdProbeCachedBeforeServe),
mpdProbeTimeoutMs: clampInt(r.mpdProbeTimeoutMs, 500, 30_000, d.mpdProbeTimeoutMs),
mpdFallbackFreshOnProbeFail: asBool(r.mpdFallbackFreshOnProbeFail, d.mpdFallbackFreshOnProbeFail),
sessionCacheMinutes: clampInt(r.sessionCacheMinutes, 1, 180, d.sessionCacheMinutes),
playTimeoutMs: clampInt(r.playTimeoutMs, 5000, 120_000, d.playTimeoutMs),
cdmTimeoutMs: clampInt(r.cdmTimeoutMs, 5000, 180_000, d.cdmTimeoutMs),
syncConcurrency: clampInt(r.syncConcurrency, 1, 16, d.syncConcurrency),
syncFetchKeysIfMissing: asBool(r.syncFetchKeysIfMissing, d.syncFetchKeysIfMissing),
syncRefreshMpdCache: asBool(r.syncRefreshMpdCache, d.syncRefreshMpdCache),
syncAutoMatchEpg: asBool(r.syncAutoMatchEpg, d.syncAutoMatchEpg),
syncOverwriteEpgMatch: asBool(r.syncOverwriteEpgMatch, d.syncOverwriteEpgMatch),
refetchKeysIfMissingOnPlay: asBool(r.refetchKeysIfMissingOnPlay, d.refetchKeysIfMissingOnPlay),
heartbeatEnabled: asBool(r.heartbeatEnabled, d.heartbeatEnabled),
heartbeatIntervalSeconds: clampInt(r.heartbeatIntervalSeconds, 30, 3600, d.heartbeatIntervalSeconds),
};
}
export function isMpdCacheFresh(
cachedAt: Date | string | null | undefined,
ttlSeconds: number,
nowMs = Date.now()
): boolean {
if (!cachedAt) return false;
const at = typeof cachedAt === "string" ? Date.parse(cachedAt) : cachedAt.getTime();
if (!Number.isFinite(at)) return false;
return nowMs - at < ttlSeconds * 1000;
}
export const ODIDO_TWEAK_META: Array<{
key: keyof OdidoTweaks;
label: string;
hint: string;
kind: "number" | "boolean";
unit?: string;
}> = [
{ key: "mpdTtlSeconds", label: "MPD cache-TTL", hint: "Gedeelde cache: jonger dan dit → direct play zonder PlayChannel", kind: "number", unit: "sec" },
{ key: "mpdRefreshEnabled", label: "Achtergrond MPD-refresh", hint: "Uit = refresh-on-watch (aanbevolen). Aan = periodiek alle zenders", kind: "boolean" },
{ key: "mpdRefreshIntervalSeconds", label: "Refresh-interval", hint: "Alleen relevant als achtergrond-refresh aan staat", kind: "number", unit: "sec" },
{ key: "mpdRefreshConcurrency", label: "Refresh parallel", hint: "Gelijktijdige PlayChannel-calls (fleet/manual)", kind: "number" },
{ key: "mpdRefreshStaggerMs", label: "Refresh stagger", hint: "Pauze tussen starts in een batch", kind: "number", unit: "ms" },
{ key: "mpdRefreshOnlyEnabled", label: "Alleen enabled zenders", hint: "Disabled kanalen overslaan bij fleet-refresh", kind: "boolean" },
{ key: "mpdPreferCacheOnPlay", label: "Cache bij play", hint: "Instant play als cache vers is (gedeeld tussen kijkers)", kind: "boolean" },
{ key: "mpdRefreshOnPlayIfStale", label: "Vernieuw bij stale play", hint: "Ouder dan TTL → verse PlayChannel bij watch", kind: "boolean" },
{ key: "mpdForceFreshOnPlay", label: "Altijd verse MPD", hint: "Debug: negeert cache volledig", kind: "boolean" },
{ key: "mpdProbeCachedBeforeServe", label: "Probe cache vóór play", hint: "Korte HTTP-check op gecachte MPD (extra latency)", kind: "boolean" },
{ key: "mpdProbeTimeoutMs", label: "Probe-timeout", hint: "Max wachttijd voor MPD-probe", kind: "number", unit: "ms" },
{ key: "mpdFallbackFreshOnProbeFail", label: "Verse MPD na mislukte probe", hint: "Bij 403/timeout opnieuw PlayChannel", kind: "boolean" },
{ key: "sessionCacheMinutes", label: "Login-sessie cache", hint: "Hergebruik Odido-cookies tussen kijkers", kind: "number", unit: "min" },
{ key: "playTimeoutMs", label: "PlayChannel-timeout", hint: "HTTP-timeout voor PlayChannel", kind: "number", unit: "ms" },
{ key: "cdmTimeoutMs", label: "CDM-timeout", hint: "Widevine license-extractie", kind: "number", unit: "ms" },
{ key: "syncConcurrency", label: "Sync parallel", hint: "Gelijktijdige zenders bij volledig ophalen", kind: "number" },
{ key: "syncFetchKeysIfMissing", label: "Sync: keys ophalen", hint: "CDM tijdens sync als keys ontbreken", kind: "boolean" },
{ key: "syncRefreshMpdCache", label: "Sync: MPD-cache vullen", hint: "Uit = cache vult bij watch i.p.v. alle zenders tijdens sync", kind: "boolean" },
{ key: "syncAutoMatchEpg", label: "Sync: EPG auto-match", hint: "Koppel XMLTV-id via alle EPG-bronnen", kind: "boolean" },
{ key: "syncOverwriteEpgMatch", label: "Sync: EPG overschrijven", hint: "Ook bestaande handmatige EPG-koppeling vervangen", kind: "boolean" },
{ key: "refetchKeysIfMissingOnPlay", label: "Play: keys als ontbrekend", hint: "CDM bij play zonder opgeslagen keys", kind: "boolean" },
{ key: "heartbeatEnabled", label: "Odido heartbeat", hint: "Sessie warm houden via OnLineHeartbeat", kind: "boolean" },
{ key: "heartbeatIntervalSeconds", label: "Heartbeat-interval", hint: "Alleen als heartbeat aan staat", kind: "number", unit: "sec" },
];