feat(web): series, season and episode removal
Series detail exposes DELETE /api/series/{id} through the movie
removePanel, generalised over endpoints and file rollup instead of
copied. Season headers and on-disk episode rows get the settings
armed-delete control wired to #174's file endpoints, worded for what
they do: files go, wanted clears, the row stays listed.
Closes #175
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,68 @@ export async function setEpisodeWanted(episodeId: number, wanted: boolean): Prom
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- removal (issues 174 + 175) ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The §7.4 title folder these episode files share. Season subfolders differ
|
||||
* between files, so the shared folder is the one carrying the `[tmdbid-…]`
|
||||
* tag every §7.4 title folder name has. `null` when the files disagree,
|
||||
* which the panel then says instead of naming one folder falsely.
|
||||
*/
|
||||
export function seriesFolder(files: { path: string }[]): string | null {
|
||||
const folders = new Set<string>();
|
||||
for (const file of files) {
|
||||
const parts = file.path.split("/");
|
||||
const titleAt = parts.findIndex((part) => part.includes("[tmdbid-"));
|
||||
const folder =
|
||||
titleAt > 0
|
||||
? parts.slice(0, titleAt + 1).join("/")
|
||||
: file.path.slice(0, file.path.lastIndexOf("/"));
|
||||
if (folder === "") {
|
||||
return null;
|
||||
}
|
||||
folders.add(folder);
|
||||
}
|
||||
if (folders.size !== 1) {
|
||||
return null;
|
||||
}
|
||||
return [...folders][0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the series from the library. The row and its §7.4 title folder go.
|
||||
* Torrents keep seeding — the reaper owns that lifecycle (§7.3).
|
||||
*/
|
||||
export function removeSeries(seriesId: number): Promise<ActionOutcome> {
|
||||
return del(`/api/series/${seriesId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* #174: the season's files go and its episodes stop being wanted. The season
|
||||
* stays listed — TMDB owns that metadata and the next refresh would recreate
|
||||
* it anyway.
|
||||
*/
|
||||
export function removeSeasonFiles(seriesId: number, seasonNumber: number): Promise<ActionOutcome> {
|
||||
return del(`/api/series/${seriesId}/seasons/${seasonNumber}/files`);
|
||||
}
|
||||
|
||||
/** #174, one episode: the file goes and the episode stops being wanted. */
|
||||
export function removeEpisodeFiles(episodeId: number): Promise<ActionOutcome> {
|
||||
return del(`/api/episodes/${episodeId}/files`);
|
||||
}
|
||||
|
||||
async function del(url: string): Promise<ActionOutcome> {
|
||||
try {
|
||||
const response = await fetch(url, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
return { kind: "error", detail: await errorDetail(response) };
|
||||
}
|
||||
return { kind: "done" };
|
||||
} catch {
|
||||
return { kind: "error", detail: "daemon unreachable" };
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- manual triggers and decks ---------------------------------------- */
|
||||
|
||||
/** §6.2 manual search, one targeted sweep, for a season or an episode. */
|
||||
|
||||
Reference in New Issue
Block a user