A TypeScript web app for LG webOS TVs (webOS 5 / Chromium 68 and newer), driven by the remote: Movies and Series search with the Trakt lists, Library with status, title and file-property filters and download marks, seasons and episodes, release search and grab, Downloads, Calendar, Settings and Log. It talks to Radarr, Sonarr, SABnzbd and Trakt directly (they answer cross-origin requests); the arr-tui config is baked in at build time and can be changed on the TV. dev/screenshots/server.py answers cross-origin requests and accepts writes for it; webos/dev/drive.mjs drives the app in Chromium for screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.6 KiB
TypeScript
75 lines
3.6 KiB
TypeScript
// What can be done with a library title, shared by Movies, Series, Library
|
|
// and Calendar.
|
|
|
|
import { app } from "../app.ts";
|
|
import type { Movie } from "../api/radarr.ts";
|
|
import type { Series } from "../api/sonarr.ts";
|
|
import { titleYear } from "../describe.ts";
|
|
import { h } from "../ui/dom.ts";
|
|
import { actionSheet, button, Dialog, run, Toggle, type Action } from "../ui/widgets.ts";
|
|
import { openEpisodes } from "./episodes.ts";
|
|
import { openReleases } from "./releases.ts";
|
|
|
|
export function movieReleases(m: Movie): void {
|
|
openReleases("Releases for " + titleYear(m), (signal) => app.radarr.movieReleases(m.id, signal), (r) => app.radarr.grab(r));
|
|
}
|
|
|
|
// movieActions: changed runs after something changed the movie (reload).
|
|
export function movieActions(m: Movie, changed: () => void, removed?: () => void): Action[] {
|
|
const name = titleYear(m);
|
|
return [
|
|
{ label: "Search releases", hint: "interactive", run: () => movieReleases(m) },
|
|
{ label: "Automatic search", run: () => void run("Searching " + name, "Search started: " + name, () => app.radarr.searchMovie(m.id)) },
|
|
{
|
|
label: m.monitored ? "Unmonitor" : "Monitor",
|
|
run: () => void run(name + ": " + (m.monitored ? "unmonitoring" : "monitoring"), "Done: " + name, () => app.radarr.setMonitored(m.id, !m.monitored)).then((ok) => ok && changed()),
|
|
},
|
|
{ label: "Remove from library…", danger: true, run: () => removeDialog(name, false, (files, excl) => app.radarr.deleteMovie(m.id, files, excl), removed ?? changed) },
|
|
];
|
|
}
|
|
|
|
export function seriesActions(s: Series, changed: () => void, removed?: () => void): Action[] {
|
|
const name = titleYear(s);
|
|
return [
|
|
{ label: "Seasons and episodes", run: () => openEpisodes(s, changed) },
|
|
{
|
|
label: "Search all missing episodes",
|
|
run: () => actionSheet(`Search all missing episodes of ${name}?`, [
|
|
{ label: "Search", run: () => void run("Searching " + name, "Search started: " + name, () => app.sonarr.searchSeries(s.id)) },
|
|
]),
|
|
},
|
|
{
|
|
label: s.monitored ? "Unmonitor" : "Monitor",
|
|
run: () => void run(name + ": " + (s.monitored ? "unmonitoring" : "monitoring"), "Done: " + name, () => app.sonarr.setMonitored(s.id, !s.monitored)).then((ok) => ok && changed()),
|
|
},
|
|
{ label: "Remove from library…", danger: true, run: () => removeDialog(name, true, (files, excl) => app.sonarr.deleteSeries(s.id, files, excl), removed ?? changed) },
|
|
];
|
|
}
|
|
|
|
export function showMovieActions(m: Movie, changed: () => void, removed?: () => void): void {
|
|
actionSheet(titleYear(m), movieActions(m, changed, removed));
|
|
}
|
|
|
|
export function showSeriesActions(s: Series, changed: () => void, removed?: () => void): void {
|
|
actionSheet(titleYear(s), seriesActions(s, changed, removed));
|
|
}
|
|
|
|
// removeDialog asks before removing; files and the import exclusion are
|
|
// separate choices, both off.
|
|
function removeDialog(name: string, series: boolean, remove: (files: boolean, exclusion: boolean) => Promise<void>, removed: () => void): void {
|
|
const d = new Dialog("Remove " + name);
|
|
const files = new Toggle(series ? "Delete all episode files" : "Delete the movie's files", false);
|
|
const excl = new Toggle("Do not add again from import lists", false);
|
|
const cancel = button("Cancel", () => d.close(), "ghost");
|
|
d.add(
|
|
h("p.dialog-text", "The title leaves the library. Files stay on disk unless you tick the box."),
|
|
files.el,
|
|
excl.el,
|
|
h("div.row-buttons", button("Remove", () => {
|
|
d.close();
|
|
void run("Removing " + name, "Removed " + name, () => remove(files.value, excl.value)).then((ok) => ok && removed());
|
|
}, "danger"), cancel),
|
|
);
|
|
d.open(cancel);
|
|
}
|