All files / src/lib episodes.ts

100% Statements 32/32
90.47% Branches 38/42
100% Functions 7/7
100% Lines 26/26

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              8x 519x 175x 175x 175x 174x             8x 49x 49x 187x 187x 153x   34x 34x 34x     153x                         8x 25x 12x 12x 12x 12x 8x 13x   8x 5x    
// SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
 
import type { Channel } from "./types";
import { withSource } from "./sources";
 
/** Episodes ordered by season, then episode number. */
export const sortEpisodes = (eps: Channel[]): Channel[] =>
	[...eps].sort((a, b) => {
		const sa = a.season ?? 0,
			sb = b.season ?? 0;
		if (sa !== sb) return sa - sb;
		return (a.episode ?? 0) - (b.episode ?? 0);
	});
 
/**
 * One entry per season + episode. Duplicate entries (the same episode from
 * other providers or categories) are folded into the first one's `sources`.
 */
export const dedupeEpisodes = (episodes: Channel[]): Channel[] => {
	const seen = new Map<string, { ch: Channel; extraSources: string[] }>();
	for (const ep of episodes) {
		const key = `${ep.season ?? 0}x${ep.episode ?? ep.name}`;
		if (!seen.has(key)) {
			seen.set(key, { ch: { ...ep }, extraSources: [...ep.sources] });
		} else {
			const entry = seen.get(key)!;
			entry.extraSources.push(ep.url, ...ep.sources);
			if (!entry.ch.logoUrl && ep.logoUrl) entry.ch.logoUrl = ep.logoUrl;
		}
	}
	return Array.from(seen.values()).map(({ ch, extraSources }) => ({
		...ch,
		sources: extraSources,
	}));
};
 
/**
 * Find a previously watched episode in a series' episode list, e.g. to replay
 * it from watch history. Matches by id first, then by the SxxEyy in `name`
 * (episode ids are index-based and can shift when the provider refreshes).
 * The result carries every source of that episode, set to the one watched
 * when it's still available. Returns null when the episode isn't in the list.
 */
export const findEpisode = (episodes: Channel[], id: string, name: string): Channel | null => {
	const exact = episodes.find((e) => e.id === id);
	const m = name.match(/S(\d{1,3})E(\d{1,3})/i);
	const season = exact?.season ?? (m ? parseInt(m[1], 10) : undefined);
	const episode = exact?.episode ?? (m ? parseInt(m[2], 10) : undefined);
	if (season == null || episode == null) return exact ?? null;
	const merged = dedupeEpisodes(episodes).find(
		(e) => e.season === season && e.episode === episode
	);
	if (!merged) return exact ?? null;
	return exact ? withSource(merged, exact.url) : merged;
};