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,
|
||||
keyCount: played.keys.length,
|
||||
promo,
|
||||
resolver: "viaplay-byguid-har-v6",
|
||||
resolver: "viaplay-byguid-har-v7",
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
guid,
|
||||
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,
|
||||
keyCount: played.keys.length,
|
||||
promo,
|
||||
resolver: "viaplay-byguid-har-v6",
|
||||
resolver: "viaplay-byguid-har-v7",
|
||||
lastVodPlay: getLastVodPlay(),
|
||||
};
|
||||
} catch (err) {
|
||||
|
|
@ -235,7 +235,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) {
|
|||
ok: false,
|
||||
guid,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
resolver: "viaplay-byguid-har-v6",
|
||||
resolver: "viaplay-byguid-har-v7",
|
||||
lastVodPlay: getLastVodPlay(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ async function main() {
|
|||
return {
|
||||
status: "ok",
|
||||
service: "master-api",
|
||||
version: "1.0.3-vod-assert-v6b",
|
||||
version: "1.0.4-dontgo-detect-v7",
|
||||
lastVodPlay: getLastVodPlay(),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -907,10 +907,42 @@ function playBodyGuid(body: Record<string, unknown>): string | 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.
|
||||
* Soft-fail “Don't go” komt vaak als 200 met lege titel / ontbrekende guid —
|
||||
* dat mag nooit door (anders plakt pageTitle over de promo-MPD).
|
||||
* HAR-success: guid + titel + product-link + echte media (geen Don't go soft-fail).
|
||||
* Soft-fail: 200 met juiste product-metadata maar placeholder-MPD “Don't go”
|
||||
* (vaak bij max devices / geen stream-recht).
|
||||
*/
|
||||
function assertVodPlayMatchesGuid(
|
||||
body: Record<string, unknown>,
|
||||
|
|
@ -931,6 +963,20 @@ function assertVodPlayMatchesGuid(
|
|||
if (isPromoVodTitle(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 productHref = asRecord(links?.["viaplay:product"])?.href;
|
||||
if (typeof productHref !== "string" || !productHref.trim()) {
|
||||
|
|
@ -958,13 +1004,11 @@ function assertVodPlayMatchesGuid(
|
|||
}
|
||||
const durationMs = Number(body.duration);
|
||||
if (Number.isFinite(durationMs) && durationMs > 0) {
|
||||
// Ultrashorte bumper/slate i.p.v. echte clip (highlights mogen ~30–60s zijn)
|
||||
if (wantsClip && durationMs < 15_000) {
|
||||
throw new Error(
|
||||
`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) {
|
||||
throw new Error(
|
||||
`Viaplay gaf te lange stream (${Math.round(durationMs / 60000)} min)`
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function recordVodPlay( partial: Omit<LastVodPlayDiag, "at" | "resolver"> ) {
|
|||
lastVodPlay = {
|
||||
...partial,
|
||||
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);
|
||||
}
|
||||
const config = parseConfig(row.configJson);
|
||||
let deviceIdsAssigned = false;
|
||||
const accounts = config.accounts
|
||||
.filter((a) => a.enabled !== false)
|
||||
.map((a) => {
|
||||
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 {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -475,6 +482,13 @@ export async function loadViaplaySettings(sessionSecret: string): Promise<Loaded
|
|||
400
|
||||
);
|
||||
}
|
||||
if (deviceIdsAssigned) {
|
||||
try {
|
||||
await persist(config, true);
|
||||
} catch {
|
||||
/* play mag door */
|
||||
}
|
||||
}
|
||||
return { accounts, config, wvdPath: await resolveWvdPath(config) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1084,7 +1084,7 @@ export function registerViewerRoutes(
|
|||
fallbackStreamUrl: null,
|
||||
fallbackFormat: null,
|
||||
/** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */
|
||||
resolver: "viaplay-byguid-har-v6",
|
||||
resolver: "viaplay-byguid-har-v7",
|
||||
mediaPath: (() => {
|
||||
try {
|
||||
const u = new URL(played.streamUrl);
|
||||
|
|
|
|||
Loading…
Reference in a new issue