Probe proved byguid works with a fresh session cookie; expired tray tokens were the real failure mode. Also drop stale ClearKey on clear VOD and add admin vod-probe.
214 lines
6.4 KiB
JavaScript
214 lines
6.4 KiB
JavaScript
/**
|
|
* Refresh Viaplay session via persistentLogin, then probe byguid.
|
|
* Writes results to stdout as JSON lines + summary.
|
|
*/
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const { chromium } = require(
|
|
path.resolve(__dirname, "../../token-tray/node_modules/playwright")
|
|
);
|
|
|
|
const UA =
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0";
|
|
const DEVICE_KEY = "pcdash-nl";
|
|
const PROFILES_ROOT = path.join(
|
|
process.env.APPDATA || "",
|
|
"@media-cluster/token-tray/browser-profiles/viaplay"
|
|
);
|
|
const GUID =
|
|
(process.argv.find((a) => a.startsWith("--guid=")) || "").slice(7) ||
|
|
"vp-sports-clip-d9a3b8a7-0804-44d";
|
|
|
|
async function refreshFromPersistentLogin(persistentLoginValue) {
|
|
const url =
|
|
"https://login.viaplay.com/api/persistentLogin/v1?deviceKey=pcdash-nl&returnurl=https%3A%2F%2Fcontent.viaplay.com%2Fpcdash-nl";
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
"User-Agent": UA,
|
|
Origin: "https://viaplay.com",
|
|
Referer: "https://viaplay.com/",
|
|
Cookie: `persistentLogin=${persistentLoginValue}`,
|
|
},
|
|
});
|
|
const jar = {};
|
|
for (const sc of res.headers.getSetCookie?.() || []) {
|
|
const m = sc.match(/^([^=]+)=([^;]*)/);
|
|
if (m) jar[m[1]] = m[2];
|
|
}
|
|
const body = await res.json().catch(() => ({}));
|
|
return {
|
|
status: res.status,
|
|
success: !!body.success,
|
|
session: jar.session ? decodeURIComponent(jar.session) : null,
|
|
accessToken: jar.accessToken || body.userData?.accessToken || null,
|
|
username: body.userData?.username || null,
|
|
raw: body,
|
|
};
|
|
}
|
|
|
|
function buildHarUrl(guid, deviceId, profileId) {
|
|
const u = new URL("https://play.viaplay.com/api/stream/byguid");
|
|
u.searchParams.set("deviceId", deviceId);
|
|
u.searchParams.set("deviceName", "web");
|
|
u.searchParams.set("deviceType", "pc");
|
|
u.searchParams.set("userAgent", UA);
|
|
u.searchParams.set("deviceKey", DEVICE_KEY);
|
|
u.searchParams.set("cse", "true");
|
|
u.searchParams.set("guid", guid);
|
|
u.searchParams.set(
|
|
"producturl",
|
|
`https://content.viaplay.com/${DEVICE_KEY}/sport/${guid}?partial=true`
|
|
);
|
|
u.searchParams.set(
|
|
"returnurl",
|
|
`https://content.viaplay.com/${DEVICE_KEY}/sport`
|
|
);
|
|
u.searchParams.set("sectionPath", "/sport");
|
|
u.searchParams.set(
|
|
"templatedproducturl",
|
|
`https://content.viaplay.com/{deviceKey}/sport/${guid}?partial=true`
|
|
);
|
|
u.searchParams.set(
|
|
"contextualv2url",
|
|
`https://content.viaplay.com/${DEVICE_KEY}/sport/${guid}?contextualV2=true&productType=clip`
|
|
);
|
|
u.searchParams.set("defaultAvailabilityContext", "svod");
|
|
u.searchParams.set("win10edge", "true");
|
|
if (profileId) u.searchParams.set("profileId", profileId);
|
|
return u.toString();
|
|
}
|
|
|
|
async function play(session, accessToken, profileId, guid, label) {
|
|
const headers = {
|
|
Accept: "*/*",
|
|
Origin: "https://viaplay.com",
|
|
Referer: "https://viaplay.com/",
|
|
"User-Agent": UA,
|
|
Cookie: [
|
|
`session=${session}`,
|
|
`accessToken=${accessToken}`,
|
|
profileId ? `viaplay_profileId=${profileId}` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("; "),
|
|
Authorization: `VIAPLAY-AT ${accessToken}`,
|
|
};
|
|
const url = buildHarUrl(guid, "probe-device-1", profileId);
|
|
const res = await fetch(url, { headers });
|
|
const body = await res.json().catch(() => ({}));
|
|
const title = body.product?.content?.title || null;
|
|
const media = body._links?.["viaplay:media"]?.href || null;
|
|
const respGuid = body.product?.system?.guid || null;
|
|
const promo = /don'?t\s*go|viaplay\s*presents|documentary/i.test(
|
|
String(title || "")
|
|
);
|
|
const out = {
|
|
label,
|
|
status: res.status,
|
|
title,
|
|
respGuid,
|
|
productType: body.product?.productType || null,
|
|
durationMs: body.duration ?? null,
|
|
media,
|
|
promo,
|
|
message: body.message || body.userMessage || null,
|
|
ok: res.status === 200 && !!media && !promo && respGuid === guid,
|
|
};
|
|
console.log(JSON.stringify(out));
|
|
return out;
|
|
}
|
|
|
|
async function main() {
|
|
const dirs = fs
|
|
.readdirSync(PROFILES_ROOT)
|
|
.map((name) => ({
|
|
name,
|
|
full: path.join(PROFILES_ROOT, name),
|
|
mtime: fs.statSync(path.join(PROFILES_ROOT, name)).mtimeMs,
|
|
}))
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
|
|
let best = null;
|
|
for (const d of dirs.slice(0, 5)) {
|
|
let ctx;
|
|
try {
|
|
ctx = await chromium.launchPersistentContext(d.full, {
|
|
headless: true,
|
|
channel: "msedge",
|
|
viewport: { width: 800, height: 600 },
|
|
});
|
|
const cookies = await ctx.cookies();
|
|
const pl = cookies.find((c) => c.name === "persistentLogin");
|
|
const profileId =
|
|
cookies.find((c) => c.name === "viaplay_profileId")?.value || null;
|
|
if (!pl?.value) {
|
|
console.log(JSON.stringify({ skip: d.name, reason: "no persistentLogin" }));
|
|
continue;
|
|
}
|
|
const refreshed = await refreshFromPersistentLogin(pl.value);
|
|
console.log(
|
|
JSON.stringify({
|
|
profile: d.name,
|
|
refreshStatus: refreshed.status,
|
|
refreshOk: refreshed.success,
|
|
username: refreshed.username,
|
|
hasSession: !!refreshed.session,
|
|
hasAT: !!refreshed.accessToken,
|
|
})
|
|
);
|
|
if (!refreshed.success || !refreshed.session || !refreshed.accessToken) {
|
|
continue;
|
|
}
|
|
const result = await play(
|
|
refreshed.session,
|
|
refreshed.accessToken,
|
|
profileId,
|
|
GUID,
|
|
`profile:${d.name}`
|
|
);
|
|
if (result.ok) {
|
|
best = result;
|
|
// MPD check
|
|
const mpd = await fetch(result.media, {
|
|
headers: { "User-Agent": UA, Referer: "https://viaplay.com/" },
|
|
});
|
|
const xml = await mpd.text();
|
|
console.log(
|
|
JSON.stringify({
|
|
mpdStatus: mpd.status,
|
|
mpdBytes: xml.length,
|
|
isMpd: /<MPD[\s>]/i.test(xml),
|
|
protections: (xml.match(/ContentProtection/g) || []).length,
|
|
})
|
|
);
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
console.log(
|
|
JSON.stringify({ profile: d.name, error: String(err.message || err) })
|
|
);
|
|
} finally {
|
|
if (ctx) await ctx.close().catch(() => {});
|
|
}
|
|
}
|
|
|
|
if (!best) {
|
|
console.log(JSON.stringify({ summary: "FAIL", guid: GUID }));
|
|
process.exitCode = 2;
|
|
} else {
|
|
console.log(
|
|
JSON.stringify({
|
|
summary: "OK",
|
|
guid: GUID,
|
|
title: best.title,
|
|
media: best.media,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|