Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 13x 53x 53x 119x 53x 13x 87x 87x 634x 634x 87x 165x 83x 13x 118x 13x 120x 120x 10235x 10235x 660x 9575x 9575x 9575x 120x 13x 4020x | // SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
import type { Category, Channel } from "@/lib/types";
/**
* Categories in the order their groups first appear, with ids cat-0, cat-1
* and so on. This is the list useChannels exposes.
*/
export const deriveCategories = (channels: Channel[]): Category[] => {
const counts = new Map<string, number>();
for (const ch of channels) {
counts.set(ch.groupTitle, (counts.get(ch.groupTitle) ?? 0) + 1);
}
return Array.from(counts.entries()).map(([name, channelCount], i) => ({
id: `cat-${i}`,
name,
channelCount,
}));
};
/**
* The channel list's categories. Each id is the group name, a channel with
* no group counts under "", and the largest group comes first.
*/
export const categoriesByCount = (channels: Channel[]): Category[] => {
const counts: Record<string, number> = {};
for (const ch of channels) {
const key = ch.groupTitle || "";
counts[key] = (counts[key] ?? 0) + 1;
}
return Object.entries(counts)
.map(([name, channelCount]) => ({ id: name, name, channelCount }))
.sort((a, b) => b.channelCount - a.channelCount);
};
/** How many channels the named group holds, or 0 when it is not listed. */
export const channelCountFor = (categories: Category[], groupName: string): number =>
categories.find((c) => c.name === groupName)?.channelCount ?? 0;
/**
* Merges movies that share a title, so a film offered by several providers
* shows once with the other streams as alternate sources.
*/
export const dedupeByTitle = (channels: Channel[]): Channel[] => {
const seen = new Map<string, Channel>();
for (const ch of channels) {
const existing = seen.get(ch.name);
if (!existing) {
seen.set(ch.name, { ...ch, sources: [...ch.sources] });
} else {
existing.sources.push(ch.url);
existing.sources.push(...ch.sources);
if (!existing.logoUrl && ch.logoUrl) existing.logoUrl = ch.logoUrl;
}
}
return Array.from(seen.values());
};
/**
* The show a series episode belongs to. The channel's own seriesTitle wins,
* otherwise the name up to its SxxExx code, so "Breaking Bad S01E02 Cat's in
* the Bag" belongs to "Breaking Bad".
*/
export const seriesTitleOf = (ch: Pick<Channel, "name" | "seriesTitle">): string =>
ch.seriesTitle ?? ch.name.replace(/\s+S\d{1,3}E\d{1,3}.*/i, "").trim();
|