import type { ChannelMediaStatus } from "yt-dlp-transcript-common/lib/channelMedia"; import type { ChannelRowMedia } from "yt-dlp-transcript-common/views/channelRow"; // THE ONE RENDERING OF "where is this channel's media, and can we reach it". // // Three surfaces draw it — the channel page's Storage panel, the /channels // table and the dashboard's channels table — and two of those are client // components, so this file must stay import-clean: the only thing it takes from // common/lib/channelMedia.ts is a TYPE, which is erased. Nothing here touches // the filesystem; the inspect() call that produces the location happens on the // server, once per row, and only its result travels. // // WHAT THE SEVEN STATUSES LOOK LIKE, and why there are only three appearances: // // in-place → NOTHING. The overwhelming majority of channels are in place, // and a badge on every row saying "normal" is noise that makes // the two that matter harder to see, not easier. // ok → neutral. Relocated and reachable is a fact worth stating (the // bytes are not on the corpus disk) but it is not a problem. // everything → red. unreachable, in-transition, inconsistent, stalled and // else legacy are all "do not trust what this channel's media says // right now": the first because the drive is not mounted, the // second because a move is half-done, the third because disk and // config disagree and nothing here is willing to guess which one // is right, the fourth because the drive is mounted and not // answering, the fifth because the channel is still on the // retired whole-directory layout and is held until // `archilyzer storage migrate-tier` runs (release 17). // // Release 17: "media" is the media TIER — `channels//media`, the big // files. A channel's text is on the corpus disk whatever this badge says. // // The `detail` string is the operator's prose from inspect() — the drive path, // the phase, the disagreement — and it goes on `title` so a row badge carries // the reason without spending a column on it. // The prop shape, deliberately narrower than ChannelMediaLocation: a row only // needs what it draws, so a server page can project three fields onto a client // component instead of serializing a whole location per channel. It is the // channel row's own media field (common/views/channelRow.ts), which is where // the in-place filter and the location label are decided, once. export type MediaBadgeInput = ChannelRowMedia; export type MediaBadgeTone = "neutral" | "danger"; export type MediaBadge = { label: string; title: string; tone: MediaBadgeTone; // What a table cell draws. Named here rather than recomputed at the call site // so the accessible name (always the full `label`) and the visible text // cannot drift apart. short: string; }; const LABELS: Record = { "in-place": null, ok: "Media relocated", unreachable: "Media unreachable", "in-transition": "Media moving", inconsistent: "Media inconsistent", stalled: "Media not answering", // Release 17: the retired whole-directory layout, held until // `archilyzer storage migrate-tier` runs. Danger, like every non-ok status; // the detail on the title names the command. legacy: "Media layout retired", }; // The one-word state, for the compact rendering of a NAMED location: "on // Platter — unreachable" says more in less room than "Media unreachable" plus a // hover, because the name is the half the operator already recognises. const SHORT_STATUS: Record = { "in-place": null, ok: null, unreachable: "unreachable", "in-transition": "moving", inconsistent: "inconsistent", stalled: "not answering", legacy: "layout retired", }; // Null means "draw nothing" — an in-place channel, or no location at all (a // caller that could not inspect). Both are the same instruction to a renderer. // // `locationLabel` IS THE NAME OF THE STORAGE LOCATION the channel's media sits // on — `locationOfDataDir(config.mediaDir, settings.storage.locations)`, resolved // on the SERVER where the settings are, and passed down as a string. Undefined // when it sits on none: a root the operator typed by hand, or a corpus with no // locations configured. // // IT ONLY EVER ADDS. The label keeps its existing words and gains " · on // Platter", so everything that addresses this badge by `media location: Media // relocated…` still finds it, while the row now names the drive in the // operator's own vocabulary instead of an absolute path. // // NEVER A PROBE. A table draws dozens of these and a probe is up to three // subprocesses; the name comes from a pure prefix match against the configured // roots. Whether that drive is reachable *right now* is `media.status`, which // inspect() already measured per channel — /storage is where a location's own // availability is reported. export function mediaBadgeOf( media: MediaBadgeInput | null | undefined, locationLabel?: string, ): MediaBadge | null { if (!media) return null; const label = LABELS[media.status] ?? null; if (!label) return null; const where = media.target ? ` to ${media.target}` : ""; const named = (locationLabel ?? "").trim(); const on = named ? ` · on ${named}` : ""; const shortStatus = SHORT_STATUS[media.status]; return { label: (media.status === "ok" ? `${label}${where}` : label) + on, short: named ? `on ${named}${shortStatus ? ` — ${shortStatus}` : ""}` : media.status === "ok" ? (LABELS.ok as string) : label, // The detail carries the reason; the target alone is the fallback so a // location written by an older inspect() still says where it points. title: media.detail ?? `${label}${where}`, tone: media.status === "ok" ? "neutral" : "danger", }; } const TONE_CLASS: Record = { neutral: "border-border bg-muted text-muted-foreground", danger: "border-destructive bg-destructive/10 text-destructive", }; // `compact` drops the target from the label: a table cell wants the four-word // state, and the full path is one hover away on the title. With a named // location that becomes "on Platter" / "on Platter — unreachable", which is // shorter AND says more. export function MediaLocationBadge({ media, compact = false, locationLabel, }: { media: MediaBadgeInput | null | undefined; compact?: boolean; locationLabel?: string; }) { // The explicit prop wins, for the Storage panel — it holds a whole // ChannelMediaLocation from inspect(), which has no room for a name. A table // row carries the name inside its projected `media` instead. const badge = mediaBadgeOf(media, locationLabel ?? media?.locationLabel); if (!badge) return null; const label = compact ? badge.short : badge.label; return ( {label} ); }