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