Fix Odido sync: sequential per-account play and stop disabling package channels.
This commit is contained in:
parent
62435927f5
commit
2d50d8948a
1 changed files with 124 additions and 87 deletions
|
|
@ -26,6 +26,8 @@ export type OdidoSyncSummary = {
|
|||
skipped: number;
|
||||
failed: number;
|
||||
disabled: number;
|
||||
reenabled: number;
|
||||
playOk: number;
|
||||
epgMatched: number;
|
||||
epgUnmatched: number;
|
||||
accounts: number;
|
||||
|
|
@ -38,7 +40,11 @@ export type OdidoSyncSummary = {
|
|||
function logoUrl(channel: OdidoChannel): string | null {
|
||||
const fromPictureIcons = channel.picture?.icons?.find((url) => !!url?.trim());
|
||||
if (fromPictureIcons) return fromPictureIcons.trim();
|
||||
if (typeof channel.logo === "object" && channel.logo && Array.isArray((channel.logo as { icons?: string[] }).icons)) {
|
||||
if (
|
||||
typeof channel.logo === "object" &&
|
||||
channel.logo &&
|
||||
Array.isArray((channel.logo as { icons?: string[] }).icons)
|
||||
) {
|
||||
const icon = (channel.logo as { icons?: string[] }).icons?.find((url) => !!url?.trim());
|
||||
if (icon) return icon.trim();
|
||||
}
|
||||
|
|
@ -55,8 +61,13 @@ function channelSortOrder(channel: OdidoChannel, apiIndex: number): number {
|
|||
|
||||
function contentId(channel: OdidoChannel): string | null {
|
||||
const physical = channel.physicalChannels?.[0];
|
||||
return channel.contentID ?? channel.contentId ??
|
||||
physical?.contentID ?? physical?.contentId ?? null;
|
||||
return (
|
||||
channel.contentID ??
|
||||
channel.contentId ??
|
||||
physical?.contentID ??
|
||||
physical?.contentId ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function parseKey(value: string): { kid: string; key: string } | null {
|
||||
|
|
@ -71,28 +82,37 @@ async function mapLimit<T>(
|
|||
worker: (value: T) => Promise<void>
|
||||
): Promise<void> {
|
||||
let cursor = 0;
|
||||
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, async () => {
|
||||
for (;;) {
|
||||
const index = cursor++;
|
||||
if (index >= values.length) return;
|
||||
await worker(values[index]);
|
||||
}
|
||||
}));
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(limit, values.length) }, async () => {
|
||||
for (;;) {
|
||||
const index = cursor++;
|
||||
if (index >= values.length) return;
|
||||
await worker(values[index]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
type UnionEntry = {
|
||||
channel: OdidoChannel;
|
||||
apiIndex: number;
|
||||
accountIds: string[];
|
||||
/** accountId die PlayChannel succesvol deed (voor keys/cache) */
|
||||
playAccountId?: string;
|
||||
cookies?: OdidoCookies;
|
||||
playUrl?: string;
|
||||
licenseUrl?: string | null;
|
||||
licenseToken?: string | null;
|
||||
playContentId?: string | null;
|
||||
playOk: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync (sequentieel per account):
|
||||
* 1) login + QueryAllChannel → unie
|
||||
* 2) per account één login + PlayChannel voor diens zenders
|
||||
* 3) alles in unie → enabled=true (play-fout zet niet meer uit)
|
||||
* 4) alleen zenders buiten alle pakketten → enabled=false
|
||||
*/
|
||||
export async function syncOdidoList(
|
||||
listId: string,
|
||||
sessionSecret: string,
|
||||
|
|
@ -114,9 +134,7 @@ export async function syncOdidoList(
|
|||
|
||||
const union = new Map<string, UnionEntry>();
|
||||
const perAccount: Array<{ accountId: string; label: string; channels: number }> = [];
|
||||
const cookiesByAccount = new Map<string, OdidoCookies>();
|
||||
|
||||
// 1) Per account sequentieel: login → QueryAllChannel → unie
|
||||
for (const account of accounts) {
|
||||
let cookies: OdidoCookies;
|
||||
try {
|
||||
|
|
@ -126,8 +144,7 @@ export async function syncOdidoList(
|
|||
tweaks.sessionCacheMinutes * 60_000,
|
||||
{ force: true }
|
||||
);
|
||||
cookiesByAccount.set(account.id, cookies);
|
||||
console.log(`[odido] sync account ${account.label || account.username}: login ok`);
|
||||
console.log(`[odido] sync ${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 });
|
||||
|
|
@ -151,29 +168,26 @@ export async function syncOdidoList(
|
|||
const mediaId = channel.physicalChannels?.[0]?.ID?.trim();
|
||||
if (!externalId || !mediaId) continue;
|
||||
count += 1;
|
||||
const existing = union.get(externalId);
|
||||
if (existing) {
|
||||
if (!existing.accountIds.includes(account.id)) {
|
||||
existing.accountIds.push(account.id);
|
||||
}
|
||||
if (!logoUrl(existing.channel) && logoUrl(channel)) existing.channel = channel;
|
||||
const prev = union.get(externalId);
|
||||
if (prev) {
|
||||
if (!prev.accountIds.includes(account.id)) prev.accountIds.push(account.id);
|
||||
if (!logoUrl(prev.channel) && logoUrl(channel)) prev.channel = channel;
|
||||
if (
|
||||
(existing.channel.channelNO == null || existing.channel.channelNO === "") &&
|
||||
(prev.channel.channelNO == null || prev.channel.channelNO === "") &&
|
||||
channel.channelNO != null
|
||||
) {
|
||||
existing.channel = { ...existing.channel, channelNO: channel.channelNO };
|
||||
prev.channel = { ...prev.channel, channelNO: channel.channelNO };
|
||||
}
|
||||
} else {
|
||||
union.set(externalId, {
|
||||
channel,
|
||||
apiIndex,
|
||||
accountIds: [account.id],
|
||||
playOk: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[odido] sync account ${account.label || account.username}: ${count} VIDEO_CHANNEL`
|
||||
);
|
||||
console.log(`[odido] sync ${account.label || account.username}: ${count} VIDEO_CHANNEL`);
|
||||
perAccount.push({ accountId: account.id, label: account.label, channels: count });
|
||||
}
|
||||
|
||||
|
|
@ -190,15 +204,6 @@ export async function syncOdidoList(
|
|||
.filter((channel) => channel.externalId)
|
||||
.map((channel) => [channel.externalId!, channel])
|
||||
);
|
||||
const successful = new Set<string>();
|
||||
const counts = {
|
||||
added: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
epgMatched: 0,
|
||||
epgUnmatched: 0,
|
||||
};
|
||||
|
||||
let epgIdSet = new Set<string>();
|
||||
let epgByName = new Map<string, string>();
|
||||
|
|
@ -215,67 +220,79 @@ 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);
|
||||
const playConcurrency = Math.min(Math.max(1, tweaks.syncConcurrency), 3);
|
||||
for (const account of accounts) {
|
||||
const owned = [...union.entries()].filter(
|
||||
([, entry]) => entry.accountIds.includes(account.id) && !entry.playOk
|
||||
);
|
||||
if (!owned.length) continue;
|
||||
|
||||
// 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];
|
||||
const mediaId = physical?.ID?.trim();
|
||||
if (!mediaId) {
|
||||
counts.failed += 1;
|
||||
return;
|
||||
let cookies: OdidoCookies;
|
||||
try {
|
||||
cookies = await login(
|
||||
account.username,
|
||||
account.pin,
|
||||
tweaks.sessionCacheMinutes * 60_000,
|
||||
{ force: true }
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`[odido] play-login mislukt voor ${account.username}:`, err);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tryOrder = [
|
||||
...entry.accountIds,
|
||||
...allAccountIds.filter((id) => !entry.accountIds.includes(id)),
|
||||
];
|
||||
console.log(
|
||||
`[odido] sync ${account.label || account.username}: PlayChannel x${owned.length}`
|
||||
);
|
||||
|
||||
let playOk = false;
|
||||
for (const accountId of tryOrder) {
|
||||
const account = accountById.get(accountId);
|
||||
if (!account) continue;
|
||||
await mapLimit(owned, playConcurrency, async ([externalId, entry]) => {
|
||||
if (entry.playOk) return;
|
||||
const mediaId = entry.channel.physicalChannels?.[0]?.ID?.trim();
|
||||
if (!mediaId) return;
|
||||
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.playOk = true;
|
||||
entry.playAccountId = account.id;
|
||||
entry.cookies = play.cookies;
|
||||
entry.playUrl = play.playUrl;
|
||||
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 {
|
||||
/* probeer volgend account */
|
||||
/* later play-on-demand */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const channelOwners: OdidoChannelOwners = {};
|
||||
const counts = {
|
||||
added: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
playOk: 0,
|
||||
epgMatched: 0,
|
||||
epgUnmatched: 0,
|
||||
};
|
||||
|
||||
for (const [externalId, entry] of union) {
|
||||
channelOwners[externalId] = [...entry.accountIds];
|
||||
const physical = entry.channel.physicalChannels?.[0];
|
||||
const mediaId = physical?.ID?.trim();
|
||||
if (!mediaId) {
|
||||
counts.failed += 1;
|
||||
continue;
|
||||
}
|
||||
if (!playOk) {
|
||||
counts.skipped += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.playOk) counts.playOk += 1;
|
||||
else counts.skipped += 1;
|
||||
|
||||
const previous = existing.get(externalId);
|
||||
let kidEnc = previous?.kidEnc ?? null;
|
||||
let keyEnc = previous?.keyEnc ?? null;
|
||||
|
||||
if (
|
||||
tweaks.syncFetchKeysIfMissing &&
|
||||
entry.playOk &&
|
||||
entry.licenseUrl &&
|
||||
entry.licenseToken &&
|
||||
entry.playUrl &&
|
||||
|
|
@ -299,12 +316,14 @@ export async function syncOdidoList(
|
|||
keyEnc = encryptSecret(key.key, sessionSecret);
|
||||
} catch {
|
||||
counts.failed += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const name = entry.channel.name?.trim() || entry.channel.channelName?.trim() ||
|
||||
physical?.mediaName?.trim() || externalId;
|
||||
const name =
|
||||
entry.channel.name?.trim() ||
|
||||
entry.channel.channelName?.trim() ||
|
||||
physical?.mediaName?.trim() ||
|
||||
externalId;
|
||||
const resolvedContentId = entry.playContentId ?? contentId(entry.channel);
|
||||
const now = new Date();
|
||||
|
||||
|
|
@ -346,6 +365,7 @@ export async function syncOdidoList(
|
|||
? { streamUrlCache: entry.playUrl, streamUrlCachedAt: now }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (previous) {
|
||||
await prisma.liveChannel.update({ where: { id: previous.id }, data });
|
||||
counts.updated += 1;
|
||||
|
|
@ -355,19 +375,32 @@ export async function syncOdidoList(
|
|||
});
|
||||
counts.added += 1;
|
||||
}
|
||||
successful.add(externalId);
|
||||
});
|
||||
}
|
||||
|
||||
const inPackage = new Set(union.keys());
|
||||
const disableIds = list.channels
|
||||
.filter((channel) => channel.externalId && !successful.has(channel.externalId))
|
||||
.filter((channel) => channel.externalId && !inPackage.has(channel.externalId))
|
||||
.map((channel) => channel.id);
|
||||
const disabled = disableIds.length
|
||||
? (await prisma.liveChannel.updateMany({
|
||||
where: { id: { in: disableIds }, enabled: true },
|
||||
data: { enabled: false },
|
||||
})).count
|
||||
? (
|
||||
await prisma.liveChannel.updateMany({
|
||||
where: { id: { in: disableIds }, enabled: true },
|
||||
data: { enabled: false },
|
||||
})
|
||||
).count
|
||||
: 0;
|
||||
|
||||
const reenabled = (
|
||||
await prisma.liveChannel.updateMany({
|
||||
where: {
|
||||
listId,
|
||||
enabled: false,
|
||||
externalId: { in: [...inPackage] },
|
||||
},
|
||||
data: { enabled: true },
|
||||
})
|
||||
).count;
|
||||
|
||||
let overlapChannels = 0;
|
||||
for (const ids of Object.values(channelOwners)) {
|
||||
if (ids.length > 1) overlapChannels += 1;
|
||||
|
|
@ -376,12 +409,16 @@ export async function syncOdidoList(
|
|||
const summary: OdidoSyncSummary = {
|
||||
...counts,
|
||||
disabled,
|
||||
reenabled,
|
||||
accounts: accounts.length,
|
||||
uniqueChannels: successful.size,
|
||||
uniqueChannels: union.size,
|
||||
overlapChannels,
|
||||
perAccount,
|
||||
durationMs: Date.now() - started,
|
||||
};
|
||||
console.log(
|
||||
`[odido] sync klaar: ${union.size} uniek, playOk=${counts.playOk}, noPlay=${counts.skipped}, disabled=${disabled}, reenabled=${reenabled}`
|
||||
);
|
||||
await saveOdidoSyncResult(summary, channelOwners);
|
||||
return summary;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue