All files / src/lib channels.ts

100% Statements 30/30
100% Branches 16/16
100% Functions 9/9
100% Lines 27/27

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();