Allow live channels without ClearKey KID/KEY.
Clear DASH streams can be saved with name + MPD only; DRM remains optional.
This commit is contained in:
parent
df9782c8f5
commit
d8f018bd07
4 changed files with 67 additions and 33 deletions
|
|
@ -235,8 +235,10 @@ export default function LiveListsPage() {
|
|||
if (channelForm.key.trim()) payload.key = channelForm.key.trim();
|
||||
|
||||
const isNew = editingChannelId === "new";
|
||||
if (isNew && (!payload.kid || !payload.key)) {
|
||||
throw new Error("KID en KEY zijn verplicht voor een nieuwe zender");
|
||||
const kidFilled = !!channelForm.kid.trim();
|
||||
const keyFilled = !!channelForm.key.trim();
|
||||
if (kidFilled !== keyFilled) {
|
||||
throw new Error("KID en KEY samen invullen, of beide leeg laten");
|
||||
}
|
||||
|
||||
const r = await fetch(
|
||||
|
|
@ -417,24 +419,28 @@ export default function LiveListsPage() {
|
|||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
KID (32 hex)
|
||||
KID (32 hex, optioneel)
|
||||
<input
|
||||
value={channelForm.kid}
|
||||
onChange={(e) => setChannelForm((f) => ({ ...f, kid: e.target.value }))}
|
||||
placeholder={
|
||||
editingChannelId !== "new" ? "•••••••• (leeg = behouden)" : "ClearKey KID"
|
||||
editingChannelId !== "new"
|
||||
? "•••••••• (leeg = behouden; wis KEY ook om DRM te verwijderen)"
|
||||
: "Leeg = geen ClearKey"
|
||||
}
|
||||
disabled={busy}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
KEY (32 hex)
|
||||
KEY (32 hex, optioneel)
|
||||
<input
|
||||
value={channelForm.key}
|
||||
onChange={(e) => setChannelForm((f) => ({ ...f, key: e.target.value }))}
|
||||
placeholder={
|
||||
editingChannelId !== "new" ? "•••••••• (leeg = behouden)" : "ClearKey KEY"
|
||||
editingChannelId !== "new"
|
||||
? "•••••••• (leeg = behouden)"
|
||||
: "Leeg = geen ClearKey"
|
||||
}
|
||||
disabled={busy}
|
||||
autoComplete="off"
|
||||
|
|
@ -537,7 +543,7 @@ export default function LiveListsPage() {
|
|||
</div>
|
||||
</td>
|
||||
<td>{c.epgChannelId || "—"}</td>
|
||||
<td>{c.hasKid && c.hasKey ? "opgeslagen" : "ontbreekt"}</td>
|
||||
<td>{c.hasKid && c.hasKey ? "ClearKey" : "geen DRM"}</td>
|
||||
<td>{c.enabled ? "aan" : "uit"}</td>
|
||||
<td style={{ whiteSpace: "nowrap" }}>
|
||||
<button type="button" disabled={busy} onClick={() => startEditChannel(c)}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
-- ClearKey KID/KEY optioneel (clear DASH zonder DRM)
|
||||
ALTER TABLE "live_channels" ALTER COLUMN "kid_enc" DROP NOT NULL;
|
||||
ALTER TABLE "live_channels" ALTER COLUMN "key_enc" DROP NOT NULL;
|
||||
|
|
@ -323,10 +323,10 @@ model LiveChannel {
|
|||
name String
|
||||
logoUrl String? @map("logo_url")
|
||||
mpdUrl String @map("mpd_url") @db.Text
|
||||
/** AES-GCM ciphertext (base64) — ClearKey KID */
|
||||
kidEnc String @map("kid_enc") @db.Text
|
||||
/** AES-GCM ciphertext (base64) — ClearKey KEY */
|
||||
keyEnc String @map("key_enc") @db.Text
|
||||
/** AES-GCM ciphertext (base64) — ClearKey KID; leeg = geen DRM */
|
||||
kidEnc String? @map("kid_enc") @db.Text
|
||||
/** AES-GCM ciphertext (base64) — ClearKey KEY; leeg = geen DRM */
|
||||
keyEnc String? @map("key_enc") @db.Text
|
||||
/** XMLTV channel id voor EPG-mapping */
|
||||
epgChannelId String? @map("epg_channel_id")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
|
|
|
|||
|
|
@ -195,11 +195,22 @@ export async function createLiveChannel(
|
|||
const mpdUrl = body.mpdUrl?.trim();
|
||||
const kid = body.kid?.trim();
|
||||
const key = body.key?.trim();
|
||||
if (!name || !mpdUrl || !kid || !key) {
|
||||
throw new AppError("INVALID_REQUEST", "name, mpdUrl, kid en key verplicht", 400);
|
||||
if (!name || !mpdUrl) {
|
||||
throw new AppError("INVALID_REQUEST", "name en mpdUrl verplicht", 400);
|
||||
}
|
||||
const kidNorm = normalizeHexKey(kid, "kid");
|
||||
const keyNorm = normalizeHexKey(key, "key");
|
||||
const hasKid = !!kid && kid !== "********";
|
||||
const hasKey = !!key && key !== "********";
|
||||
if (hasKid !== hasKey) {
|
||||
throw new AppError("INVALID_REQUEST", "KID en KEY samen invullen, of beide leeg laten", 400);
|
||||
}
|
||||
const kidEnc =
|
||||
hasKid && hasKey
|
||||
? encryptSecret(normalizeHexKey(kid!, "kid"), sessionSecret)
|
||||
: null;
|
||||
const keyEnc =
|
||||
hasKid && hasKey
|
||||
? encryptSecret(normalizeHexKey(key!, "key"), sessionSecret)
|
||||
: null;
|
||||
const maxOrder = await prisma.liveChannel.aggregate({
|
||||
where: { listId },
|
||||
_max: { sortOrder: true },
|
||||
|
|
@ -210,8 +221,8 @@ export async function createLiveChannel(
|
|||
name,
|
||||
logoUrl: body.logoUrl?.trim() || null,
|
||||
mpdUrl,
|
||||
kidEnc: encryptSecret(kidNorm, sessionSecret),
|
||||
keyEnc: encryptSecret(keyNorm, sessionSecret),
|
||||
kidEnc,
|
||||
keyEnc,
|
||||
epgChannelId: body.epgChannelId?.trim() || null,
|
||||
sortOrder: body.sortOrder ?? (maxOrder._max.sortOrder ?? 0) + 10,
|
||||
enabled: body.enabled !== false,
|
||||
|
|
@ -239,14 +250,21 @@ export async function updateLiveChannel(
|
|||
|
||||
const kidRaw = body.kid?.trim();
|
||||
const keyRaw = body.key?.trim();
|
||||
const kidEnc =
|
||||
kidRaw && kidRaw !== "********"
|
||||
? encryptSecret(normalizeHexKey(kidRaw, "kid"), sessionSecret)
|
||||
: undefined;
|
||||
const keyEnc =
|
||||
keyRaw && keyRaw !== "********"
|
||||
? encryptSecret(normalizeHexKey(keyRaw, "key"), sessionSecret)
|
||||
: undefined;
|
||||
const clearingKeys = kidRaw === "" && keyRaw === "";
|
||||
const hasKid = !!kidRaw && kidRaw !== "********";
|
||||
const hasKey = !!keyRaw && keyRaw !== "********";
|
||||
if ((hasKid || hasKey) && hasKid !== hasKey) {
|
||||
throw new AppError("INVALID_REQUEST", "KID en KEY samen invullen, of beide leeg laten", 400);
|
||||
}
|
||||
let kidEnc: string | null | undefined;
|
||||
let keyEnc: string | null | undefined;
|
||||
if (clearingKeys) {
|
||||
kidEnc = null;
|
||||
keyEnc = null;
|
||||
} else if (hasKid && hasKey) {
|
||||
kidEnc = encryptSecret(normalizeHexKey(kidRaw!, "kid"), sessionSecret);
|
||||
keyEnc = encryptSecret(normalizeHexKey(keyRaw!, "key"), sessionSecret);
|
||||
}
|
||||
|
||||
await prisma.liveChannel.update({
|
||||
where: { id: channelId },
|
||||
|
|
@ -254,8 +272,8 @@ export async function updateLiveChannel(
|
|||
...(body.name?.trim() ? { name: body.name.trim() } : {}),
|
||||
...(body.logoUrl !== undefined ? { logoUrl: body.logoUrl?.trim() || null } : {}),
|
||||
...(body.mpdUrl?.trim() ? { mpdUrl: body.mpdUrl.trim() } : {}),
|
||||
...(kidEnc ? { kidEnc } : {}),
|
||||
...(keyEnc ? { keyEnc } : {}),
|
||||
...(kidEnc !== undefined ? { kidEnc } : {}),
|
||||
...(keyEnc !== undefined ? { keyEnc } : {}),
|
||||
...(body.epgChannelId !== undefined
|
||||
? { epgChannelId: body.epgChannelId?.trim() || null }
|
||||
: {}),
|
||||
|
|
@ -401,8 +419,18 @@ export async function getCustomPlayUrl(
|
|||
});
|
||||
if (!channel) throw new AppError("FORBIDDEN", "Geen toegang tot deze zender", 403);
|
||||
|
||||
const kid = decryptSecret(channel.kidEnc, sessionSecret);
|
||||
const key = decryptSecret(channel.keyEnc, sessionSecret);
|
||||
const drm =
|
||||
channel.kidEnc && channel.keyEnc
|
||||
? {
|
||||
type: "clearkey" as const,
|
||||
keys: [
|
||||
{
|
||||
kid: decryptSecret(channel.kidEnc, sessionSecret),
|
||||
key: decryptSecret(channel.keyEnc, sessionSecret),
|
||||
},
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
streamId: customStreamId(channel.id),
|
||||
|
|
@ -412,10 +440,7 @@ export async function getCustomPlayUrl(
|
|||
format: "dash" as const,
|
||||
fallbackStreamUrl: null,
|
||||
fallbackFormat: null,
|
||||
drm: {
|
||||
type: "clearkey" as const,
|
||||
keys: [{ kid, key }],
|
||||
},
|
||||
drm,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue