Files
nilsandClaude Opus 5.5 7f6d4d07b9 webOS: a TV app with the views of arr-tui
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>
2026-10-25 16:51:48 +01:00

57 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { deepStrictEqual, strictEqual } from "node:assert";
import { test } from "node:test";
import { errorMessage, queueName, queueProgress, scaledVariant, tmdbSized, type QueueItem } from "../src/api/arr.ts";
import { compact, decodeList, movieLists, refKey } from "../src/api/trakt.ts";
import { formatReleaseAge, formatSize } from "../src/format.ts";
import { episodeStatus, summarizeSeasons } from "../src/seasons.ts";
import type { Episode } from "../src/api/sonarr.ts";
test("poster URLs", () => {
strictEqual(scaledVariant("/MediaCover/1/poster.jpg?lastWrite=5", 500), "/MediaCover/1/poster-500.jpg?lastWrite=5");
strictEqual(scaledVariant("/MediaCover/1/fanart.jpg", 500), "/MediaCover/1/fanart.jpg");
strictEqual(tmdbSized("https://image.tmdb.org/t/p/original/abc.jpg", true), "https://image.tmdb.org/t/p/w780/abc.jpg");
strictEqual(tmdbSized("https://image.tmdb.org/t/p/original/abc.jpg", false), "https://image.tmdb.org/t/p/w342/abc.jpg");
strictEqual(tmdbSized("https://artworks.thetvdb.com/banners/1.jpg", true), "https://artworks.thetvdb.com/banners/1.jpg");
});
test("error messages", () => {
strictEqual(errorMessage('{"message":"Movie exists"}'), "Movie exists");
strictEqual(errorMessage('[{"propertyName":"Path","errorMessage":"Bad path"},{"errorMessage":"Other"}]'), "Bad path; Other");
strictEqual(errorMessage(" plain "), "plain");
});
test("queue items", () => {
const q = { id: 1, title: "rel", status: "", size: 100, sizeleft: 25, series: { title: "Show" }, episode: { seasonNumber: 1, episodeNumber: 2, title: "x" } } as QueueItem;
strictEqual(queueName(q), "Show S01E02");
strictEqual(queueProgress(q), 75);
});
test("trakt", () => {
strictEqual(compact(950), "950");
strictEqual(compact(1234), "1.2k");
strictEqual(compact(2000), "2k");
strictEqual(compact(34567), "34k");
strictEqual(compact(1_500_000), "1.5M");
strictEqual(compact(48_000_000), "48M");
const entries = decodeList([{ watchers: 1234, movie: { title: "Dune", year: 2021, ids: { trakt: 1, tmdb: 438631 } } }], movieLists[0]);
deepStrictEqual(entries, [{ title: "Dune", year: 2021, ids: { trakt: 1, tmdb: 438631 }, count: 1234 }]);
const bare = decodeList([{ title: "Heat", year: 1995, ids: { trakt: 2, tmdb: 949 } }], movieLists[3]);
strictEqual(bare[0].title, "Heat");
strictEqual(refKey({ kind: "show", tvdb: 81189 }), "show:tvdb:81189");
});
test("formatting", () => {
strictEqual(formatSize(1.5 * 1024 ** 3), "1.5 GB");
strictEqual(formatSize(0), "–");
strictEqual(formatReleaseAge(30), "30m");
strictEqual(formatReleaseAge(300), "5h");
strictEqual(formatReleaseAge(5000), "3d");
});
test("seasons", () => {
const ep = (s: number, n: number, hasFile: boolean) => ({ id: s * 100 + n, seriesId: 1, seasonNumber: s, episodeNumber: n, title: "", hasFile, monitored: true }) as Episode;
deepStrictEqual(summarizeSeasons([ep(0, 1, false), ep(1, 1, true), ep(2, 1, false), ep(2, 2, true)]).map((s) => [s.number, s.have, s.total]), [[2, 1, 2], [1, 1, 1], [0, 0, 1]]);
deepStrictEqual(episodeStatus({ ...ep(1, 1, false), airDate: "2026-01-01" }, "2026-02-01"), ["missing", "warn"]);
deepStrictEqual(episodeStatus({ ...ep(1, 1, false), airDate: "2026-03-01" }, "2026-02-01"), ["unaired", "muted"]);
});