Reject Viaplay Don't go soft-fail and keep stable deviceIds.
Don't go is a placeholder MPD (mediaGuid=unknown), often when the account hit the 5-device limit. Fail closed and stop minting a new deviceId on every decrypt.
This commit is contained in:
parent
f20b29362e
commit
c1378aa1d5
7 changed files with 71 additions and 13 deletions
|
|
@ -1928,14 +1928,14 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
|
||||||
streamUrl: played.streamUrl,
|
streamUrl: played.streamUrl,
|
||||||
keyCount: played.keys.length,
|
keyCount: played.keys.length,
|
||||||
promo,
|
promo,
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
guid,
|
guid,
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
mediaPath,
|
mediaPath,
|
||||||
keyCount: played.keys.length,
|
keyCount: played.keys.length,
|
||||||
promo,
|
promo,
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
lastVodPlay: getLastVodPlay(),
|
lastVodPlay: getLastVodPlay(),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -235,7 +235,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
||||||
ok: false,
|
ok: false,
|
||||||
guid,
|
guid,
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
lastVodPlay: getLastVodPlay(),
|
lastVodPlay: getLastVodPlay(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ async function main() {
|
||||||
return {
|
return {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
service: "master-api",
|
service: "master-api",
|
||||||
version: "1.0.3-vod-assert-v6b",
|
version: "1.0.4-dontgo-detect-v7",
|
||||||
lastVodPlay: getLastVodPlay(),
|
lastVodPlay: getLastVodPlay(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -907,10 +907,42 @@ function playBodyGuid(body: Record<string, unknown>): string | null {
|
||||||
return typeof system?.guid === "string" ? system.guid.trim() : null;
|
return typeof system?.guid === "string" ? system.guid.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bekende Don't go soft-fail CDN-asset (titel/guid kloppen, media niet). */
|
||||||
|
const VIAPLAY_DONT_GO_MEDIA_RE = /\/7c4cd64\/88000100-1778484912807\.ism\b/i;
|
||||||
|
|
||||||
|
function cseMediaGuid(body: Record<string, unknown>): string | null {
|
||||||
|
try {
|
||||||
|
const cse = asRecord(body.cseReporting);
|
||||||
|
const link = asRecord(asRecord(cse?.link)?.reportingUrl);
|
||||||
|
const href = typeof link?.href === "string" ? link.href : "";
|
||||||
|
const m = href.match(/\/viaplay\/([^/$]+)/);
|
||||||
|
if (!m?.[1]) return null;
|
||||||
|
const payload = JSON.parse(
|
||||||
|
Buffer.from(m[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
|
||||||
|
) as { mediaGuid?: string };
|
||||||
|
return typeof payload.mediaGuid === "string" ? payload.mediaGuid : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mediaHrefFromPlayBody(body: Record<string, unknown>): string | null {
|
||||||
|
const links = asRecord(body._links);
|
||||||
|
for (const key of [
|
||||||
|
"viaplay:media",
|
||||||
|
"viaplay:playlist",
|
||||||
|
"viaplay:encryptedPlaylist",
|
||||||
|
] as const) {
|
||||||
|
const href = asRecord(links?.[key])?.href;
|
||||||
|
if (typeof href === "string" && href.trim()) return href.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HAR-success: guid + titel + product-link + productType.
|
* HAR-success: guid + titel + product-link + echte media (geen Don't go soft-fail).
|
||||||
* Soft-fail “Don't go” komt vaak als 200 met lege titel / ontbrekende guid —
|
* Soft-fail: 200 met juiste product-metadata maar placeholder-MPD “Don't go”
|
||||||
* dat mag nooit door (anders plakt pageTitle over de promo-MPD).
|
* (vaak bij max devices / geen stream-recht).
|
||||||
*/
|
*/
|
||||||
function assertVodPlayMatchesGuid(
|
function assertVodPlayMatchesGuid(
|
||||||
body: Record<string, unknown>,
|
body: Record<string, unknown>,
|
||||||
|
|
@ -931,6 +963,20 @@ function assertVodPlayMatchesGuid(
|
||||||
if (isPromoVodTitle(played.title)) {
|
if (isPromoVodTitle(played.title)) {
|
||||||
throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`);
|
throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mediaHref = mediaHrefFromPlayBody(body) || played.mpdUrl;
|
||||||
|
if (VIAPLAY_DONT_GO_MEDIA_RE.test(mediaHref)) {
|
||||||
|
throw new Error(
|
||||||
|
"Viaplay soft-fail: Don't go-placeholder i.p.v. stream (vaak te veel apparaten — log uit op viaplay.com → Apparaten beheren)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const streamMediaGuid = cseMediaGuid(body);
|
||||||
|
if (streamMediaGuid === "unknown") {
|
||||||
|
throw new Error(
|
||||||
|
"Viaplay soft-fail: mediaGuid=unknown (geen echte stream — check apparaatlimiet of abonnement)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const links = asRecord(body._links);
|
const links = asRecord(body._links);
|
||||||
const productHref = asRecord(links?.["viaplay:product"])?.href;
|
const productHref = asRecord(links?.["viaplay:product"])?.href;
|
||||||
if (typeof productHref !== "string" || !productHref.trim()) {
|
if (typeof productHref !== "string" || !productHref.trim()) {
|
||||||
|
|
@ -958,13 +1004,11 @@ function assertVodPlayMatchesGuid(
|
||||||
}
|
}
|
||||||
const durationMs = Number(body.duration);
|
const durationMs = Number(body.duration);
|
||||||
if (Number.isFinite(durationMs) && durationMs > 0) {
|
if (Number.isFinite(durationMs) && durationMs > 0) {
|
||||||
// Ultrashorte bumper/slate i.p.v. echte clip (highlights mogen ~30–60s zijn)
|
|
||||||
if (wantsClip && durationMs < 15_000) {
|
if (wantsClip && durationMs < 15_000) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Viaplay gaf te korte stream (${Math.round(durationMs / 1000)}s) — promo-slate?`
|
`Viaplay gaf te korte stream (${Math.round(durationMs / 1000)}s) — promo-slate?`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Feature-lengte i.p.v. clip
|
|
||||||
if (wantsClip && durationMs > 2.5 * 60 * 60 * 1000) {
|
if (wantsClip && durationMs > 2.5 * 60 * 60 * 1000) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Viaplay gaf te lange stream (${Math.round(durationMs / 60000)} min)`
|
`Viaplay gaf te lange stream (${Math.round(durationMs / 60000)} min)`
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ function recordVodPlay( partial: Omit<LastVodPlayDiag, "at" | "resolver"> ) {
|
||||||
lastVodPlay = {
|
lastVodPlay = {
|
||||||
...partial,
|
...partial,
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -454,11 +454,18 @@ export async function loadViaplaySettings(sessionSecret: string): Promise<Loaded
|
||||||
throw new AppError("INVALID_REQUEST", "Viaplay-integratie is uitgeschakeld", 400);
|
throw new AppError("INVALID_REQUEST", "Viaplay-integratie is uitgeschakeld", 400);
|
||||||
}
|
}
|
||||||
const config = parseConfig(row.configJson);
|
const config = parseConfig(row.configJson);
|
||||||
|
let deviceIdsAssigned = false;
|
||||||
const accounts = config.accounts
|
const accounts = config.accounts
|
||||||
.filter((a) => a.enabled !== false)
|
.filter((a) => a.enabled !== false)
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
try {
|
try {
|
||||||
return decryptAccount(a, sessionSecret);
|
const creds = decryptAccount(a, sessionSecret);
|
||||||
|
// Stabiel deviceId bewaren — random per decrypt verbrandt Viaplay's 5-device limiet.
|
||||||
|
if (!a.deviceId && creds.deviceId) {
|
||||||
|
a.deviceId = creds.deviceId;
|
||||||
|
deviceIdsAssigned = true;
|
||||||
|
}
|
||||||
|
return creds;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -475,6 +482,13 @@ export async function loadViaplaySettings(sessionSecret: string): Promise<Loaded
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (deviceIdsAssigned) {
|
||||||
|
try {
|
||||||
|
await persist(config, true);
|
||||||
|
} catch {
|
||||||
|
/* play mag door */
|
||||||
|
}
|
||||||
|
}
|
||||||
return { accounts, config, wvdPath: await resolveWvdPath(config) };
|
return { accounts, config, wvdPath: await resolveWvdPath(config) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1084,7 +1084,7 @@ export function registerViewerRoutes(
|
||||||
fallbackStreamUrl: null,
|
fallbackStreamUrl: null,
|
||||||
fallbackFormat: null,
|
fallbackFormat: null,
|
||||||
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
||||||
resolver: "viaplay-byguid-har-v6",
|
resolver: "viaplay-byguid-har-v7",
|
||||||
mediaPath: (() => {
|
mediaPath: (() => {
|
||||||
try {
|
try {
|
||||||
const u = new URL(played.streamUrl);
|
const u = new URL(played.streamUrl);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue