Restore IPTV categories; only drop provider aggregate folders.
Keep one Alle zenders entry, backfill per-category counts when the bulk stream list is incomplete, and do not block the linked IPTV line on lastError. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a0311c031e
commit
318f613b40
2 changed files with 81 additions and 31 deletions
|
|
@ -153,10 +153,11 @@ struct LiveTvScreen: View {
|
||||||
error = nil
|
error = nil
|
||||||
do {
|
do {
|
||||||
let raw = try await app.api.loadIptvCategories()
|
let raw = try await app.api.loadIptvCategories()
|
||||||
// Geen synthetische "Alles"-chip; API heeft al "Alle zenders" e.d.
|
// Alleen provider-dubbel "Alles/All" weg; behoud onze "__all__" (Alle zenders)
|
||||||
categories = raw.filter { cat in
|
categories = raw.filter { cat in
|
||||||
|
if cat.id == "__all__" { return true }
|
||||||
let n = cat.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let n = cat.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
return n != "alles" && n != "all"
|
return n != "alles" && n != "all" && n != "alle"
|
||||||
}
|
}
|
||||||
|
|
||||||
let saved = UserDefaults.standard.string(forKey: Self.lastCategoryKey)
|
let saved = UserDefaults.standard.string(forKey: Self.lastCategoryKey)
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,20 @@ export async function resolveViewerIptvToken(viewerId: string): Promise<string |
|
||||||
});
|
});
|
||||||
if (!viewer?.enabled || !viewer.appAccess || !viewer.iptvAccess) return null;
|
if (!viewer?.enabled || !viewer.appAccess || !viewer.iptvAccess) return null;
|
||||||
|
|
||||||
|
// Gekoppelde lijn altijd proberen — lastError mag TV niet stilleggen
|
||||||
if (viewer.iptvAddonTokenId) {
|
if (viewer.iptvAddonTokenId) {
|
||||||
const linked = await prisma.addonToken.findFirst({
|
const linked = await prisma.addonToken.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: viewer.iptvAddonTokenId,
|
id: viewer.iptvAddonTokenId,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
iptvLine: { enableLive: true, lastError: null },
|
iptvLine: { enableLive: true },
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (linked) return linked.id;
|
if (linked) return linked.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallback = await prisma.iptvLine.findFirst({
|
const fallbackOk = await prisma.iptvLine.findFirst({
|
||||||
where: {
|
where: {
|
||||||
enableLive: true,
|
enableLive: true,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
|
@ -47,7 +48,17 @@ export async function resolveViewerIptvToken(viewerId: string): Promise<string |
|
||||||
select: { addonTokenId: true },
|
select: { addonTokenId: true },
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
});
|
});
|
||||||
return fallback?.addonTokenId ?? null;
|
if (fallbackOk) return fallbackOk.addonTokenId;
|
||||||
|
|
||||||
|
const fallbackAny = await prisma.iptvLine.findFirst({
|
||||||
|
where: {
|
||||||
|
enableLive: true,
|
||||||
|
addonToken: { revoked: false },
|
||||||
|
},
|
||||||
|
select: { addonTokenId: true },
|
||||||
|
orderBy: { updatedAt: "desc" },
|
||||||
|
});
|
||||||
|
return fallbackAny?.addonTokenId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getViewerIptvStatus(viewerId: string, config: Config) {
|
export async function getViewerIptvStatus(viewerId: string, config: Config) {
|
||||||
|
|
@ -83,39 +94,65 @@ export async function getViewerIptvCategories(viewerId: string, config: Config)
|
||||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Geen filter op channelCount>0: de volledige get_live_streams-lijst is soms
|
// Als de bulk-lijst incompleet is: per categorie bijtellen (gecached in XtreamClient)
|
||||||
// incompleet of category_id-format wijkt af, waardoor echte categorieën verdwenen.
|
const idsNeedingCount = cats
|
||||||
const categories = cats
|
.map((c) => normalizeIptvCategoryId(c.category_id))
|
||||||
.map((c) => {
|
.filter((id) => id && (counts.get(id) ?? 0) === 0);
|
||||||
const id = normalizeIptvCategoryId(c.category_id);
|
if (idsNeedingCount.length > 0) {
|
||||||
return {
|
await mapPool(idsNeedingCount, 8, async (id) => {
|
||||||
id,
|
const list = await xt.client.getLiveStreams(id).catch(() => null);
|
||||||
name: friendlyCategoryName(c.category_name),
|
if (Array.isArray(list) && list.length > 0) {
|
||||||
rawName: c.category_name,
|
counts.set(id, list.length);
|
||||||
channelCount: counts.get(id) ?? 0,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((c) => c.id.length > 0)
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Categorieën mét getelde zenders eerst, daarna alfabetisch
|
|
||||||
if ((a.channelCount > 0) !== (b.channelCount > 0)) {
|
|
||||||
return a.channelCount > 0 ? -1 : 1;
|
|
||||||
}
|
}
|
||||||
return a.name.localeCompare(b.name, "nl");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (streams.length > 0) {
|
|
||||||
categories.unshift({
|
|
||||||
id: "__all__",
|
|
||||||
name: "Alle zenders",
|
|
||||||
rawName: "Alle zenders",
|
|
||||||
channelCount: streams.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categories = cats
|
||||||
|
.map((c) => {
|
||||||
|
const id = normalizeIptvCategoryId(c.category_id);
|
||||||
|
const rawName = c.category_name ?? "";
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: friendlyCategoryName(rawName),
|
||||||
|
rawName,
|
||||||
|
channelCount: counts.get(id) ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// Alleen lege ids + provider-verzamelcategorieën weg (niet echte genre-planken)
|
||||||
|
.filter((c) => c.id.length > 0 && !isAggregateCategoryName(c.rawName))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name, "nl"));
|
||||||
|
|
||||||
|
// Één verzamelknop van ons — niet van de IPTV-provider
|
||||||
|
const totalChannels =
|
||||||
|
streams.length > 0
|
||||||
|
? streams.length
|
||||||
|
: [...counts.values()].reduce((a, b) => a + b, 0);
|
||||||
|
categories.unshift({
|
||||||
|
id: "__all__",
|
||||||
|
name: "Alle zenders",
|
||||||
|
rawName: "Alle zenders",
|
||||||
|
channelCount: totalChannels,
|
||||||
|
});
|
||||||
|
|
||||||
return { categories };
|
return { categories };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Alleen echte verzamelbakken (Alles / All / Alle zenders), geen Sport/Films/etc. */
|
||||||
|
export function isAggregateCategoryName(raw: string): boolean {
|
||||||
|
const n = friendlyCategoryName(raw).toLowerCase().replace(/\s+/g, " ").trim();
|
||||||
|
return (
|
||||||
|
n === "alles" ||
|
||||||
|
n === "all" ||
|
||||||
|
n === "alle" ||
|
||||||
|
n === "alle zenders" ||
|
||||||
|
n === "all channels" ||
|
||||||
|
n === "all channel" ||
|
||||||
|
n === "all categories" ||
|
||||||
|
n === "all cats" ||
|
||||||
|
n === "everything"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Xtream geeft category_id soms als number, "12" of "12.0". */
|
/** Xtream geeft category_id soms als number, "12" of "12.0". */
|
||||||
export function normalizeIptvCategoryId(raw: unknown): string {
|
export function normalizeIptvCategoryId(raw: unknown): string {
|
||||||
if (raw == null) return "";
|
if (raw == null) return "";
|
||||||
|
|
@ -125,6 +162,18 @@ export function normalizeIptvCategoryId(raw: unknown): string {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function mapPool<T>(items: T[], concurrency: number, fn: (item: T) => Promise<void>) {
|
||||||
|
const queue = [...items];
|
||||||
|
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const item = queue.shift();
|
||||||
|
if (item === undefined) return;
|
||||||
|
await fn(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
|
}
|
||||||
|
|
||||||
function sortChannels(streams: XtreamLiveStream[]): XtreamLiveStream[] {
|
function sortChannels(streams: XtreamLiveStream[]): XtreamLiveStream[] {
|
||||||
return [...streams].sort((a, b) => (a.num ?? a.stream_id) - (b.num ?? b.stream_id));
|
return [...streams].sort((a, b) => (a.num ?? a.stream_id) - (b.num ?? b.stream_id));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue