import type { SubviewUrl } from "../lib/history"; import { parsePositiveIntId } from "../lib/parseId"; // The Plex module's sub-view union and its address-bar mirror, extracted from PlexBrowse so the // encode/decode round-trip (and its guards) can be unit-tested without pulling the whole component // (and its lazy hls.js player) into the test graph. export type Sub = | { kind: "discover" } | { kind: "grid" } | { kind: "playlists" } // browse-all-playlists surface (header pill "Playlist" mode) | { kind: "collections" } // browse-all-collections surface (header pill "Collection" mode) | { kind: "show"; id: string } | { kind: "season"; showId: string; seasonId: string } | { kind: "player"; id: string; queue?: string[] } | { kind: "info"; id: string } | { kind: "person"; name: string } | { kind: "playlist"; id: number } | { kind: "collection"; id: string }; // the Collection info page (id = collection rating_key) // Mirror the Plex sub-view to ?v=: so a show / season / movie-info / person / playlist page // — and the flat browse grid — are bookmarkable. Discover is the ROOT landing (E5 S4), so it (and the // ephemeral player) is not mirrored (encode → null); the grid gets `?v=browse` so "Browse all" is a // distinct, shareable URL. The person name is the whole tail after the first ":", so a name containing // ":" round-trips. export const PLEX_SUB_URL: SubviewUrl = { key: "v", encode: (v) => { switch (v.kind) { case "grid": return "browse"; case "playlists": return "playlists"; case "collections": return "collections"; case "show": return `show:${v.id}`; case "season": return `season:${v.showId}:${v.seasonId}`; case "info": return `info:${v.id}`; case "person": return `person:${v.name}`; case "playlist": return `playlist:${v.id}`; case "collection": return `collection:${v.id}`; default: return null; // discover (root) + the ephemeral player are not URL-mirrored } }, decode: (raw) => { const i = raw.indexOf(":"); const kind = i === -1 ? raw : raw.slice(0, i); const arg = i === -1 ? "" : raw.slice(i + 1); switch (kind) { case "browse": return { kind: "grid" }; case "playlists": return { kind: "playlists" }; case "collections": return { kind: "collections" }; case "show": return arg ? { kind: "show", id: arg } : null; case "info": return arg ? { kind: "info", id: arg } : null; case "person": return arg ? { kind: "person", name: arg } : null; case "playlist": { // Positive-integer id only — a fractional/zero/negative ?v=playlist: would open a // sub-view for a non-existent id (shares parsePositiveIntId with readSelectedId's ?pl guard). const id = parsePositiveIntId(arg); return id != null ? { kind: "playlist", id } : null; } case "collection": return arg ? { kind: "collection", id: arg } : null; case "season": { const [showId, seasonId] = arg.split(":"); return showId && seasonId ? { kind: "season", showId, seasonId } : null; } default: return null; } }, };