feat(anime): queue episodes to play next across anime

- Episode rows gain Play/Queue actions (and matching context-menu items); queued rows show their place in line, with a queue count and Clear queue in the episode header
- Queue lives in the main process (`anime-browser-queue.ts`) so it survives the browser window closing and advances on mpv's end-file even when nobody is watching; streams resolve at play time so a signed URL cannot expire while queued
- Holds mpv's keep-open off while the queue waits and restores it once empty; queueing with nothing playing just plays immediately
- Adds anime-browser-queue and episode-queue unit tests, IPC channels/contracts, and doc updates
This commit is contained in:
2026-08-15 21:44:51 -07:00
parent 51fc9034f7
commit 3ca7dcd664
19 changed files with 838 additions and 7 deletions
+28
View File
@@ -0,0 +1,28 @@
import type { AnimeBrowserQueueEntry } from '../types/anime-browser';
/**
* Where each queued episode sits in line, keyed by source and episode url.
*
* The position is counted across the whole queue rather than within one anime:
* "3rd up" has to mean the same thing on every detail page, or the number is
* worse than no number at all.
*/
export function queuePositions(entries: AnimeBrowserQueueEntry[]): Map<string, number> {
const positions = new Map<string, number>();
entries.forEach((entry, index) => {
const key = queueKey(entry.sourceId, entry.episodeUrl);
// A duplicate should never reach the renderer, but if one did, the earlier
// position is the one that will actually play.
if (!positions.has(key)) positions.set(key, index + 1);
});
return positions;
}
export function queueKey(sourceId: string, episodeUrl: string): string {
return `${sourceId} ${episodeUrl}`;
}
/** "Next up" reads better than "#1" for the episode about to play. */
export function describeQueuePosition(position: number): string {
return position === 1 ? 'next up' : `#${position} in queue`;
}