From 8cb03d23cc52112f2b1adc90e56df0d8fb1b07c9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:24:51 -0400 Subject: [PATCH] feat(my-videos): add pure video-health classifier (Phase 1, step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First piece of the "My Videos" management tab: classifyVideoHealth decides the maintenance status for one of the user's own videos from note-side data plus an optional URL probe result. Pure/synchronous so it's deterministic and testable; the controller will do the actual HEAD probe and pass the result in. Statuses: no-source (no url AND no magnet) -> error; hosted-URL-unreachable -> warning; deleted (tombstoned) -> info; otherwise ok. Key guardrail: only URLs under the user's storage publicBaseUrl can be flagged dead — external/CORS-opaque links are never cried wolf on. Deleted is checked before no-source since soft-delete scrubs url/magnet. Tests + mutation-verified: dropping the hosted guard (external false-alarm) and removing deleted-precedence each fail a scenario. Build + full lint green. Co-Authored-By: Claude Opus 4.8 --- js/ui/profileModal/myVideosHealth.js | 71 ++++++++++++++++++++++++++ tests/my-videos-health.test.mjs | 74 ++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 js/ui/profileModal/myVideosHealth.js create mode 100644 tests/my-videos-health.test.mjs diff --git a/js/ui/profileModal/myVideosHealth.js b/js/ui/profileModal/myVideosHealth.js new file mode 100644 index 00000000..e5efe0ba --- /dev/null +++ b/js/ui/profileModal/myVideosHealth.js @@ -0,0 +1,71 @@ +// Pure health classification for the "My Videos" management tab. +// +// Given a parsed video note (and optionally a URL probe result), decide the +// maintenance status shown to the user. Deliberately no network/IO here so it +// stays deterministic and testable; the controller performs the actual HEAD +// probe and passes the result in. Phase 2 (bucket reconciliation) layers +// storage-side statuses on top of this note-side classification. + +export const VIDEO_HEALTH_STATUS = Object.freeze({ + DELETED: "deleted", + NO_SOURCE: "no-source", + DEAD_URL: "dead-url", + OK: "ok", +}); + +function trimStr(value) { + return typeof value === "string" ? value.trim() : ""; +} + +/** + * Does `url` live under the user's storage public base URL? Only hosted URLs are + * ones we can meaningfully probe / reconcile against the bucket; third-party + * links are out of our control (and CORS-opaque), so they must never be flagged + * red just because we can't verify them. + */ +export function isUrlUnderBase(url, publicBaseUrl) { + const u = trimStr(url); + const base = trimStr(publicBaseUrl).replace(/\/+$/, ""); + if (!u || !base) { + return false; + } + return u === base || u.startsWith(`${base}/`); +} + +/** + * Classify the maintenance health of one of the user's own videos. + * + * @param {object} video parsed video note ({ url, magnet, deleted, ... }) + * @param {object} [opts] + * @param {string} [opts.publicBaseUrl] the user's storage public base URL + * @param {{ ok: boolean }|null} [opts.urlProbe] result of a HEAD probe on + * `video.url`, if one was performed (null = not probed / unverifiable) + * @returns {{ status: string, severity: "error"|"warning"|"info"|"ok", + * label: string, hosted: boolean }} + */ +export function classifyVideoHealth(video, { publicBaseUrl = "", urlProbe = null } = {}) { + const v = video && typeof video === "object" ? video : {}; + const url = trimStr(v.url); + const magnet = trimStr(v.magnet); + const hosted = isUrlUnderBase(url, publicBaseUrl); + + // A deleted (tombstoned) note is intentional, not a problem to fix here — it's + // a candidate for storage cleanup (Phase 2). Checked first because soft-delete + // scrubs url/magnet, which would otherwise read as "no source". + if (v.deleted === true) { + return { status: VIDEO_HEALTH_STATUS.DELETED, severity: "info", label: "Deleted on Nostr", hosted }; + } + + if (!url && !magnet) { + return { status: VIDEO_HEALTH_STATUS.NO_SOURCE, severity: "error", label: "No playable source", hosted }; + } + + // A dead URL is only a hard signal when it's a hosted URL we could actually + // probe. External links that fail (or can't be probed) stay "ok" so we don't + // cry wolf on CORS-opaque third-party hosts. + if (hosted && url && urlProbe && urlProbe.ok === false) { + return { status: VIDEO_HEALTH_STATUS.DEAD_URL, severity: "warning", label: "Hosted file unreachable", hosted }; + } + + return { status: VIDEO_HEALTH_STATUS.OK, severity: "ok", label: "OK", hosted }; +} diff --git a/tests/my-videos-health.test.mjs b/tests/my-videos-health.test.mjs new file mode 100644 index 00000000..a3160683 --- /dev/null +++ b/tests/my-videos-health.test.mjs @@ -0,0 +1,74 @@ +// The "My Videos" management tab flags maintenance issues per video so a user can +// keep a large library clean. classifyVideoHealth is the pure decision core: +// no-source -> error (red), hosted-and-unreachable -> warning, deleted -> info, +// otherwise ok. It must NOT flag external/unverifiable URLs as broken. + +import assert from "node:assert/strict"; +import test from "node:test"; +import { + classifyVideoHealth, + isUrlUnderBase, + VIDEO_HEALTH_STATUS, +} from "../js/ui/profileModal/myVideosHealth.js"; + +const BASE = "https://cdn.example.com"; + +test("a video with neither URL nor magnet is a red no-source error", () => { + const r = classifyVideoHealth({ url: "", magnet: "" }, { publicBaseUrl: BASE }); + assert.equal(r.status, VIDEO_HEALTH_STATUS.NO_SOURCE); + assert.equal(r.severity, "error"); +}); + +test("a magnet-only video is OK (no hosted URL needed)", () => { + const r = classifyVideoHealth({ url: "", magnet: "magnet:?xt=urn:btih:abc" }, { publicBaseUrl: BASE }); + assert.equal(r.status, VIDEO_HEALTH_STATUS.OK); + assert.equal(r.severity, "ok"); +}); + +test("a hosted URL with no probe is OK", () => { + const r = classifyVideoHealth({ url: `${BASE}/u/npub1/h/v.mp4` }, { publicBaseUrl: BASE }); + assert.equal(r.status, VIDEO_HEALTH_STATUS.OK); + assert.equal(r.hosted, true); +}); + +test("a deleted note is classified as deleted (info), not no-source", () => { + // Soft-delete scrubs url/magnet; deleted must win over no-source. + const r = classifyVideoHealth({ url: "", magnet: "", deleted: true }, { publicBaseUrl: BASE }); + assert.equal(r.status, VIDEO_HEALTH_STATUS.DELETED); + assert.equal(r.severity, "info"); +}); + +test("a hosted URL that fails its probe is a dead-url warning", () => { + const r = classifyVideoHealth( + { url: `${BASE}/u/npub1/h/v.mp4` }, + { publicBaseUrl: BASE, urlProbe: { ok: false } }, + ); + assert.equal(r.status, VIDEO_HEALTH_STATUS.DEAD_URL); + assert.equal(r.severity, "warning"); +}); + +test("an EXTERNAL URL that fails its probe is NOT flagged (unverifiable, no false alarm)", () => { + const r = classifyVideoHealth( + { url: "https://third-party.example/v.mp4" }, + { publicBaseUrl: BASE, urlProbe: { ok: false } }, + ); + assert.equal(r.status, VIDEO_HEALTH_STATUS.OK); + assert.equal(r.hosted, false); +}); + +test("a hosted URL whose probe succeeds is OK", () => { + const r = classifyVideoHealth( + { url: `${BASE}/u/npub1/h/v.mp4` }, + { publicBaseUrl: BASE, urlProbe: { ok: true } }, + ); + assert.equal(r.status, VIDEO_HEALTH_STATUS.OK); +}); + +test("isUrlUnderBase handles trailing slashes, external hosts, and empty input", () => { + assert.equal(isUrlUnderBase(`${BASE}/a/b.mp4`, BASE), true); + assert.equal(isUrlUnderBase(`${BASE}/a/b.mp4`, `${BASE}/`), true, "trailing slash on base"); + assert.equal(isUrlUnderBase("https://evil.example/a.mp4", BASE), false); + assert.equal(isUrlUnderBase(`${BASE}-lookalike.com/a.mp4`, BASE), false, "prefix must be a path boundary"); + assert.equal(isUrlUnderBase("", BASE), false); + assert.equal(isUrlUnderBase(`${BASE}/a.mp4`, ""), false); +});