import type { Video } from "./api"; // What the player does with its queue after the item it is PLAYING is removed from the list it was // opened from. The player's launch queue is frozen at mount (see PlayerModal), so nothing external // can take the item out for it: leaving it in would let prev/next step back onto a video that no // longer exists, and unmounting the player instead would drop you out of the list you are triaging. // // Pure, because the decision is the whole of the behaviour: which item plays next, and when there is // nothing left to play. export interface AfterRemoval { /** The queue with the removed item gone. */ queue: Video[]; /** The item to play next. */ playingId: string; /** Which way that item lies from the removed one: what it carries on along if it cannot play. */ way: 1 | -1; } /** `null` means nothing is left — the caller closes the player. * * The next item is the one that FOLLOWED the removed one (that is what "delete and keep going" * means when you are working down a list), the one before it when the last item goes. A video known * to be unplayable (`dead`, see `stepIndex`) is passed over, not loaded again: the nearest playable * one after, else before. None playable: the follower all the same, where the player shows why. */ export function afterRemoval( queue: Video[] | undefined, index: number, victimId: string, dead: ReadonlySet = new Set(), ): AfterRemoval | null { const rest = (queue ?? []).filter((v) => v.id !== victimId); if (rest.length === 0) return null; // The removed item's slot: what followed it now stands there (past the end when it was the last). const at = Math.min(Math.max(index, 0), rest.length); const pick = (j: number, way: 1 | -1) => ({ queue: rest, playingId: rest[j]!.id, way }); for (let j = at; j < rest.length; j++) if (!dead.has(rest[j]!.id)) return pick(j, 1); for (let j = at - 1; j >= 0; j--) if (!dead.has(rest[j]!.id)) return pick(j, -1); return at < rest.length ? pick(at, 1) : pick(rest.length - 1, -1); } /** The nearest item from `index` going `dir` that is not known to be unplayable, or -1 when there is * none that way. `wrap` goes round the ends (Loop "All"), never back onto `index` itself. * * A video YouTube refused to play (embed-blocked, removed) is remembered for the player session, so * Previous / Next pass over it instead of loading it again only to be bounced off it. */ export function stepIndex( queue: readonly Video[], index: number, dir: 1 | -1, dead: ReadonlySet, wrap = false, ): number { const n = queue.length; for (let k = 1; k < n; k++) { const raw = index + dir * k; if (!wrap && (raw < 0 || raw >= n)) return -1; const j = ((raw % n) + n) % n; if (!dead.has(queue[j]!.id)) return j; } return -1; }