Zet periodieke PlayChannel-storm uit; vernieuw bij kijken met gedeelde DB-cache, in-flight coalescing en fallback op laatste URL.
158 lines
8.9 KiB
TypeScript
158 lines
8.9 KiB
TypeScript
/** 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" },
|
||
];
|