diff --git a/apps/master-api/src/odido/client.ts b/apps/master-api/src/odido/client.ts index 4db84a0..d41dff5 100644 --- a/apps/master-api/src/odido/client.ts +++ b/apps/master-api/src/odido/client.ts @@ -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; 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; diff --git a/apps/master-api/src/odido/play.ts b/apps/master-api/src/odido/play.ts index 854f771..c7e57c1 100644 --- a/apps/master-api/src/odido/play.ts +++ b/apps/master-api/src/odido/play.ts @@ -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, diff --git a/apps/master-api/src/odido/sync.ts b/apps/master-api/src/odido/sync.ts index 764a557..c6314ed 100644 --- a/apps/master-api/src/odido/sync.ts +++ b/apps/master-api/src/odido/sync.ts @@ -116,16 +116,18 @@ export async function syncOdidoList( const perAccount: Array<{ accountId: string; label: string; channels: number }> = []; const cookiesByAccount = new Map(); - // 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 {