Fix Odido multi-account sync: unique device IDs and fresh login before play.

This commit is contained in:
Jos Vooges | STH 2026-09-22 01:25:27 +02:00
parent e3601710f8
commit 62435927f5
3 changed files with 54 additions and 12 deletions

View file

@ -1,10 +1,21 @@
import { createHash } from "node:crypto";
const BASE_URL = "https://tv.odido.nl";
export const ODIDO_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
/** Fallback als er geen username is; liever deviceIdForUsername(). */
const DEFAULT_DEVICE_ID = "MzkzNjIwMTU1NjAxMDNydA==";
const DEFAULT_SESSION_CACHE_MS = 20 * 60_000;
/** Stabiele, per-account device-id zodat sessies elkaar niet wegtrappen. */
export function deviceIdForUsername(username: string): string {
const hash = createHash("sha256")
.update(`odido-mediacluster:${username.trim()}`)
.digest("base64url");
return hash.slice(0, 24);
}
export type OdidoCookies = Record<string, string>;
export type OdidoChannel = {
@ -153,7 +164,11 @@ export async function login(
const cached = sessions.get(username);
if (cached && cached.expiresAt > Date.now()) return cached.cookies;
}
const cookies = await loginWithDevice(username, password, DEFAULT_DEVICE_ID);
const cookies = await loginWithDevice(
username,
password,
deviceIdForUsername(username) || DEFAULT_DEVICE_ID
);
const ttl = Number.isFinite(sessionCacheMs) && sessionCacheMs > 0
? sessionCacheMs
: DEFAULT_SESSION_CACHE_MS;

View file

@ -145,10 +145,13 @@ export async function refreshChannelMpd(
let lastError: Error | null = null;
for (const account of ordered) {
try {
const cookies =
opts?.cookies && opts.account?.id === account.id
? opts.cookies
: await login(account.username, account.pin, tweaks.sessionCacheMinutes * 60_000);
// Altijd verse login: multi-account deelt geen betrouwbare sessie-cache.
const cookies = await login(
account.username,
account.pin,
tweaks.sessionCacheMinutes * 60_000,
{ force: true }
);
const play = await playChannel(cookies, externalId, mediaId);
const keys = await fetchKeysIfNeeded(
play,

View file

@ -116,16 +116,18 @@ export async function syncOdidoList(
const perAccount: Array<{ accountId: string; label: string; channels: number }> = [];
const cookiesByAccount = new Map<string, OdidoCookies>();
// 1) Per account: login + zenderlijst → unie (geen dubbele LiveChannel-rijen)
// 1) Per account sequentieel: login → QueryAllChannel → unie
for (const account of accounts) {
let cookies: OdidoCookies;
try {
cookies = await login(
account.username,
account.pin,
tweaks.sessionCacheMinutes * 60_000
tweaks.sessionCacheMinutes * 60_000,
{ force: true }
);
cookiesByAccount.set(account.id, cookies);
console.log(`[odido] sync account ${account.label || account.username}: login ok`);
} catch (err) {
console.warn(`[odido] login mislukt voor ${account.username}:`, err);
perAccount.push({ accountId: account.id, label: account.label, channels: 0 });
@ -154,7 +156,6 @@ export async function syncOdidoList(
if (!existing.accountIds.includes(account.id)) {
existing.accountIds.push(account.id);
}
// Behoud rijkste metadata (logo/channelNO)
if (!logoUrl(existing.channel) && logoUrl(channel)) existing.channel = channel;
if (
(existing.channel.channelNO == null || existing.channel.channelNO === "") &&
@ -170,6 +171,9 @@ export async function syncOdidoList(
});
}
}
console.log(
`[odido] sync account ${account.label || account.username}: ${count} VIDEO_CHANNEL`
);
perAccount.push({ accountId: account.id, label: account.label, channels: count });
}
@ -213,8 +217,12 @@ export async function syncOdidoList(
const channelOwners: OdidoChannelOwners = {};
const entries = [...union.entries()];
const accountById = new Map(accounts.map((a) => [a.id, a]));
const allAccountIds = accounts.map((a) => a.id);
// 2) PlayChannel per unieke zender — eerste account dat lukt (keys/cache)
// 2) PlayChannel per unieke zender.
// Belangrijk: verse login per poging — na multi-account QueryAllChannel zijn
// eerdere cookies vaak ongeldig; probeer owners eerst, daarna overige accounts.
await mapLimit(entries, tweaks.syncConcurrency, async ([externalId, entry]) => {
channelOwners[externalId] = [...entry.accountIds];
const physical = entry.channel.physicalChannels?.[0];
@ -224,11 +232,23 @@ export async function syncOdidoList(
return;
}
const tryOrder = [
...entry.accountIds,
...allAccountIds.filter((id) => !entry.accountIds.includes(id)),
];
let playOk = false;
for (const accountId of entry.accountIds) {
const cookies = cookiesByAccount.get(accountId);
if (!cookies) continue;
for (const accountId of tryOrder) {
const account = accountById.get(accountId);
if (!account) continue;
try {
const cookies = await login(
account.username,
account.pin,
tweaks.sessionCacheMinutes * 60_000,
{ force: true }
);
cookiesByAccount.set(accountId, cookies);
const play = await playChannel(cookies, externalId, mediaId);
entry.playAccountId = accountId;
entry.cookies = play.cookies;
@ -236,6 +256,10 @@ export async function syncOdidoList(
entry.licenseUrl = play.licenseUrl;
entry.licenseToken = play.licenseToken;
entry.playContentId = play.contentId;
if (!entry.accountIds.includes(accountId)) {
entry.accountIds.push(accountId);
channelOwners[externalId] = [...entry.accountIds];
}
playOk = true;
break;
} catch {