Rename Licenties to DRM and ingest event API ClearKeys into drm_keys.
Schedule feed kids/keys are persisted on fetch/play; admin backfill covers live lists and events.
This commit is contained in:
parent
62a7ddec63
commit
fafe97e729
6 changed files with 191 additions and 20 deletions
|
|
@ -130,7 +130,10 @@ export default function DrmPage() {
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!r.ok) throw new Error(d.message || "Backfill mislukt");
|
if (!r.ok) throw new Error(d.message || "Backfill mislukt");
|
||||||
setMsg(
|
setMsg(
|
||||||
`Backfill: ${d.imported ?? 0} nieuw, ${d.skipped ?? 0} overgeslagen, ${d.failed ?? 0} mislukt`
|
`Import: ${d.imported ?? 0} nieuw · ${d.skipped ?? 0} bekend · ${d.failed ?? 0} fout` +
|
||||||
|
(d.liveChannels || d.scheduleEvents
|
||||||
|
? ` (live ${d.liveChannels?.imported ?? 0}/${d.liveChannels?.skipped ?? 0}, events ${d.scheduleEvents?.imported ?? 0}/${d.scheduleEvents?.skipped ?? 0})`
|
||||||
|
: "")
|
||||||
);
|
);
|
||||||
await load(search, source);
|
await load(search, source);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const desktopLinks = [
|
||||||
{ href: "/downloads", label: "Downloads" },
|
{ href: "/downloads", label: "Downloads" },
|
||||||
{ href: "/streams", label: "Streams" },
|
{ href: "/streams", label: "Streams" },
|
||||||
{ href: "/live-lists", label: "Live-lijsten" },
|
{ href: "/live-lists", label: "Live-lijsten" },
|
||||||
{ href: "/drm", label: "Licenties" },
|
{ href: "/drm", label: "DRM" },
|
||||||
{ href: "/scripts", label: "Scripts" },
|
{ href: "/scripts", label: "Scripts" },
|
||||||
{ href: "/agent", label: "Agent" },
|
{ href: "/agent", label: "Agent" },
|
||||||
{ href: "/viewers", label: "Gebruikers" },
|
{ href: "/viewers", label: "Gebruikers" },
|
||||||
|
|
|
||||||
|
|
@ -1964,8 +1964,11 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/v1/admin/drm-keys/backfill", { preHandler: requireAdmin }, async () => {
|
app.post("/api/v1/admin/drm-keys/backfill", { preHandler: requireAdmin }, async () => {
|
||||||
const { backfillDrmKeysFromLiveChannels } = await import("../drm/keys");
|
const { backfillAllDrmKeys } = await import("../drm/keys");
|
||||||
return backfillDrmKeysFromLiveChannels(prisma, config.SESSION_SECRET);
|
const { harvestScheduleEventDrmKeys } = await import("../viewer/schedule-events");
|
||||||
|
return backfillAllDrmKeys(prisma, config.SESSION_SECRET, () =>
|
||||||
|
harvestScheduleEventDrmKeys(config)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/v1/admin/drm-keys", { preHandler: requireAdmin }, async (request) => {
|
app.post("/api/v1/admin/drm-keys", { preHandler: requireAdmin }, async (request) => {
|
||||||
|
|
|
||||||
|
|
@ -292,3 +292,37 @@ export async function backfillDrmKeysFromLiveChannels(
|
||||||
}
|
}
|
||||||
return { imported, skipped, failed };
|
return { imported, skipped, failed };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DrmBackfillResult = {
|
||||||
|
imported: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
liveChannels: { imported: number; skipped: number; failed: number };
|
||||||
|
scheduleEvents: { imported: number; skipped: number; failed: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Live-lijsten + schedule/events-feed (kids/keys in de API). */
|
||||||
|
export async function backfillAllDrmKeys(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
sessionSecret: string,
|
||||||
|
harvestScheduleEvents: () => Promise<{
|
||||||
|
imported: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
}>
|
||||||
|
): Promise<DrmBackfillResult> {
|
||||||
|
const liveChannels = await backfillDrmKeysFromLiveChannels(prisma, sessionSecret);
|
||||||
|
let scheduleEvents = { imported: 0, skipped: 0, failed: 0 };
|
||||||
|
try {
|
||||||
|
scheduleEvents = await harvestScheduleEvents();
|
||||||
|
} catch {
|
||||||
|
scheduleEvents = { imported: 0, skipped: 0, failed: 1 };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
imported: liveChannels.imported + scheduleEvents.imported,
|
||||||
|
skipped: liveChannels.skipped + scheduleEvents.skipped,
|
||||||
|
failed: liveChannels.failed + scheduleEvents.failed,
|
||||||
|
liveChannels,
|
||||||
|
scheduleEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,24 @@ export async function createLiveChannel(
|
||||||
enabled: body.enabled !== false,
|
enabled: body.enabled !== false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (hasKid && hasKey) {
|
||||||
|
try {
|
||||||
|
const { upsertDrmKey } = await import("../drm/keys");
|
||||||
|
const listMeta = await prisma.liveChannelList.findUnique({
|
||||||
|
where: { id: listId },
|
||||||
|
select: { provider: true },
|
||||||
|
});
|
||||||
|
await upsertDrmKey(prisma, sessionSecret, {
|
||||||
|
kid: normalizeHexKey(kid!, "kid"),
|
||||||
|
key: normalizeHexKey(key!, "key"),
|
||||||
|
mpdUrl,
|
||||||
|
channelName: name,
|
||||||
|
source: listMeta?.provider || "live-channel",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* DRM-db mag create niet breken */
|
||||||
|
}
|
||||||
|
}
|
||||||
return getLiveChannelListAdmin(listId);
|
return getLiveChannelListAdmin(listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,6 +308,25 @@ export async function updateLiveChannel(
|
||||||
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (hasKid && hasKey) {
|
||||||
|
try {
|
||||||
|
const { upsertDrmKey } = await import("../drm/keys");
|
||||||
|
const listMeta = await prisma.liveChannelList.findUnique({
|
||||||
|
where: { id: existing.listId },
|
||||||
|
select: { provider: true },
|
||||||
|
});
|
||||||
|
await upsertDrmKey(prisma, sessionSecret, {
|
||||||
|
kid: normalizeHexKey(kidRaw!, "kid"),
|
||||||
|
key: normalizeHexKey(keyRaw!, "key"),
|
||||||
|
mpdUrl: body.mpdUrl?.trim() || existing.mpdUrl,
|
||||||
|
channelName: body.name?.trim() || existing.name,
|
||||||
|
source: listMeta?.provider || "live-channel",
|
||||||
|
externalId: existing.externalId,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
return getLiveChannelListAdmin(existing.listId);
|
return getLiveChannelListAdmin(existing.listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,6 +517,24 @@ export async function getCustomPlayUrl(
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (drm?.keys?.length) {
|
||||||
|
try {
|
||||||
|
const { upsertDrmKey } = await import("../drm/keys");
|
||||||
|
for (const k of drm.keys) {
|
||||||
|
await upsertDrmKey(prisma, sessionSecret, {
|
||||||
|
kid: k.kid,
|
||||||
|
key: k.key,
|
||||||
|
mpdUrl: streamUrl || channel.mpdUrl,
|
||||||
|
channelName: channel.name,
|
||||||
|
source: channel.list?.provider || channel.list?.name || "live-channel",
|
||||||
|
externalId: channel.externalId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sid = customStreamId(channel.id);
|
const sid = customStreamId(channel.id);
|
||||||
let watchSessionId: string | null = null;
|
let watchSessionId: string | null = null;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -552,34 +552,101 @@ function phaseFor(start: string, stop: string, isLiveFlag: boolean, nowMs: numbe
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractKids(raw: RawEvent): Array<{ kid: string; key: string }> {
|
function extractKids(raw: RawEvent): Array<{ kid: string; key: string }> {
|
||||||
const fromKids = Array.isArray(raw.kids) ? raw.kids : [];
|
|
||||||
const out: Array<{ kid: string; key: string }> = [];
|
const out: Array<{ kid: string; key: string }> = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const item of fromKids) {
|
|
||||||
const k = item as RawKid;
|
const push = (kidRaw: unknown, keyRaw: unknown) => {
|
||||||
const kid = str(k.kid).toLowerCase();
|
const kid = str(kidRaw).toLowerCase().replace(/[^0-9a-f]/g, "");
|
||||||
const key = str(k.key).toLowerCase();
|
const key = str(keyRaw).toLowerCase().replace(/[^0-9a-f]/g, "");
|
||||||
if (!/^[0-9a-f]{32}$/.test(kid) || !/^[0-9a-f]{32}$/.test(key)) continue;
|
if (!/^[0-9a-f]{32}$/.test(kid) || !/^[0-9a-f]{32}$/.test(key)) return;
|
||||||
if (seen.has(kid)) continue;
|
if (seen.has(kid)) return;
|
||||||
seen.add(kid);
|
seen.add(kid);
|
||||||
out.push({ kid, key });
|
out.push({ kid, key });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromKids = Array.isArray(raw.kids) ? raw.kids : [];
|
||||||
|
for (const item of fromKids) {
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
const k = item as RawKid;
|
||||||
|
push(k.kid, k.key);
|
||||||
|
} else {
|
||||||
|
const s = str(item);
|
||||||
|
const m = s.match(/^([0-9a-fA-F]{32}):([0-9a-fA-F]{32})$/);
|
||||||
|
if (m) push(m[1], m[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (out.length) return out;
|
|
||||||
|
|
||||||
const keyPairs = Array.isArray(raw.keys) ? raw.keys : [];
|
const keyPairs = Array.isArray(raw.keys) ? raw.keys : [];
|
||||||
for (const item of keyPairs) {
|
for (const item of keyPairs) {
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
const k = item as RawKid;
|
||||||
|
push(k.kid, k.key);
|
||||||
|
} else {
|
||||||
const s = str(item);
|
const s = str(item);
|
||||||
const m = s.match(/^([0-9a-fA-F]{32}):([0-9a-fA-F]{32})$/);
|
const m = s.match(/^([0-9a-fA-F]{32}):([0-9a-fA-F]{32})$/);
|
||||||
if (!m) continue;
|
if (m) push(m[1], m[2]);
|
||||||
const kid = m[1].toLowerCase();
|
|
||||||
const key = m[2].toLowerCase();
|
|
||||||
if (seen.has(kid)) continue;
|
|
||||||
seen.add(kid);
|
|
||||||
out.push({ kid, key });
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sommige feeds zetten één string op top-level
|
||||||
|
for (const field of [raw.keys, (raw as { key?: unknown }).key] as unknown[]) {
|
||||||
|
if (typeof field === "string") {
|
||||||
|
const m = field.match(/^([0-9a-fA-F]{32}):([0-9a-fA-F]{32})$/);
|
||||||
|
if (m) push(m[1], m[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistKidsFromRawEvents(
|
||||||
|
config: Config,
|
||||||
|
events: RawEvent[]
|
||||||
|
): Promise<{ imported: number; skipped: number; failed: number }> {
|
||||||
|
const { upsertDrmKey } = await import("../drm/keys");
|
||||||
|
let imported = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let failed = 0;
|
||||||
|
for (const raw of events) {
|
||||||
|
const keys = extractKids(raw);
|
||||||
|
if (!keys.length) continue;
|
||||||
|
const eventName = str(raw.name) || str(raw.epg?.title) || null;
|
||||||
|
const channelName = cleanChannelLabel(
|
||||||
|
str(raw.epg?.channel) || str(raw.metadata?.zender)
|
||||||
|
);
|
||||||
|
const mpdUrl = str(raw.mpd) || str(raw.manifest_url) || null;
|
||||||
|
const provider = (str(raw.provider) || "event").toLowerCase();
|
||||||
|
const externalId = str(raw.id) || null;
|
||||||
|
for (const k of keys) {
|
||||||
|
try {
|
||||||
|
const result = await upsertDrmKey(prisma, config.SESSION_SECRET, {
|
||||||
|
kid: k.kid,
|
||||||
|
key: k.key,
|
||||||
|
mpdUrl,
|
||||||
|
eventName,
|
||||||
|
channelName,
|
||||||
|
source: provider,
|
||||||
|
externalId,
|
||||||
|
});
|
||||||
|
if (result.created) imported += 1;
|
||||||
|
else skipped += 1;
|
||||||
|
} catch {
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { imported, skipped, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force-fetch schedule feed and ingest all kids/keys into drm_keys. */
|
||||||
|
export async function harvestScheduleEventDrmKeys(
|
||||||
|
config: Config
|
||||||
|
): Promise<{ imported: number; skipped: number; failed: number }> {
|
||||||
|
cache = null;
|
||||||
|
const raw = await loadRawEvents(config);
|
||||||
|
return persistKidsFromRawEvents(config, raw.events);
|
||||||
|
}
|
||||||
|
|
||||||
function isPlayable(raw: RawEvent): boolean {
|
function isPlayable(raw: RawEvent): boolean {
|
||||||
const mpd = str(raw.mpd) || str(raw.manifest_url);
|
const mpd = str(raw.mpd) || str(raw.manifest_url);
|
||||||
if (!mpd) return false;
|
if (!mpd) return false;
|
||||||
|
|
@ -774,6 +841,10 @@ async function loadRawEvents(config: Config): Promise<CacheEntry> {
|
||||||
events,
|
events,
|
||||||
generatedAt: str(json.Meta?.generated_at) || null,
|
generatedAt: str(json.Meta?.generated_at) || null,
|
||||||
};
|
};
|
||||||
|
// Keys uit de feed persistent maken (async, mag list niet blokkeren)
|
||||||
|
void persistKidsFromRawEvents(config, events).catch(() => {
|
||||||
|
/* best-effort */
|
||||||
|
});
|
||||||
return cache;
|
return cache;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cache && cache.url === url) return cache;
|
if (cache && cache.url === url) return cache;
|
||||||
|
|
@ -1101,6 +1172,11 @@ export async function getScheduleEventPlay(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zorg dat gespeelde event-keys altijd in drm_keys belanden
|
||||||
|
void persistKidsFromRawEvents(config, [found]).catch(() => {
|
||||||
|
/* best-effort */
|
||||||
|
});
|
||||||
|
|
||||||
// ESPN/Disney e.d. leveren HLS/CMAF (.m3u8); Prime/Viaplay meestal DASH (.mpd).
|
// ESPN/Disney e.d. leveren HLS/CMAF (.m3u8); Prime/Viaplay meestal DASH (.mpd).
|
||||||
// FAAB-veld heet vaak `mpd` ook als de URL een m3u8 is — nooit blind "dash" aannemen.
|
// FAAB-veld heet vaak `mpd` ook als de URL een m3u8 is — nooit blind "dash" aannemen.
|
||||||
const format = detectLiveManifestFormat(streamUrl);
|
const format = detectLiveManifestFormat(streamUrl);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue