feat(library): show stored versions per episode and movie (LIBV-1)
- domain versionSpecOf: file name + ffprobe + ingest quality -> version spec;
summarizeVersions; matchVersions (satisfied / missing / upgradeable, single target)
- GET /library/{id}/versions: per-season/episode versions, summaries, matches[]
- organize writes full spec into media_asset.delivery (specVersion 1)
- scripts/asset-spec-backfill.ts: dry-run by default, --probe local only, --apply
- web: season summary line, one-line spec per episode, N versions expand,
tech info details (TODO: DEAI-2 TechInfo), movie versions panel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3763c86481
commit
bf9e73e86e
19 files changed
+2060
-9
No files matched your search
@@ -37,6 +37,7 @@ import {
|
||||
useRefreshMetadata,
|
||||
useUnlockField,
|
||||
} from "~/api/library.ts";
|
||||
import { useLibraryVersions } from "~/api/library-versions.ts";
|
||||
import { useSubscriptionCoverage, useSubscriptions } from "~/api/subscriptions.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
@@ -62,6 +63,12 @@ import {
|
||||
import { Chip, ChipList, Panel, ProgressLine, posterPathOf, relTime, Tone } from "~/components/library/parts.tsx";
|
||||
import { SlotCard } from "~/components/library/slot-card.tsx";
|
||||
import { finalOf, UnitFieldDialog } from "~/components/library/unit-field-dialog.tsx";
|
||||
import {
|
||||
EpisodeVersionList,
|
||||
MovieVersionsPanel,
|
||||
seasonLine,
|
||||
UnplacedVersions,
|
||||
} from "~/components/library/versions.tsx";
|
||||
import { externalLinks } from "~/components/media/external-links.ts";
|
||||
import {
|
||||
AIR_STATUS,
|
||||
@@ -645,6 +652,15 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
.sort((a, b) => a.episodeNumber! - b.episodeNumber! || a.createdAt.localeCompare(b.createdAt));
|
||||
const covered = new Set(detail.assets.filter((a) => a.asset.status === "active").flatMap((a) => a.contentUnitIds));
|
||||
const lookup = useMemo(() => coverageLookup(cov.data, rowIdx), [cov.data, rowIdx]);
|
||||
// LIBV-1:每集的入库版本,比对跟随上方选中的收录版本
|
||||
const versions = useLibraryVersions(e.id, cov.data?.rows[rowIdx]?.slotId ?? undefined);
|
||||
const versionSeasons = versions.data?.seasons ?? [];
|
||||
// 选中「库内任一版本」时不按收录版本比对
|
||||
const anyVersion = cov.data?.rows[rowIdx] !== undefined && cov.data.rows[rowIdx]?.slotId === null;
|
||||
const versionsOf = (u: ContentUnit) => {
|
||||
const s = versionSeasons.find((x) => x.seasonNumber === u.seasonNumber);
|
||||
return s?.episodes.find((x) => x.unitId === u.id) ?? s?.episodes.find((x) => x.episodeNumber === u.episodeNumber);
|
||||
};
|
||||
const seasonNums = [
|
||||
...new Set([...seasonUnits.map((u) => u.seasonNumber!), ...episodes.map((u) => u.seasonNumber!)]),
|
||||
].sort((a, b) => (a || 999) - (b || 999));
|
||||
@@ -748,6 +764,10 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
episodeCount: total,
|
||||
posterPath: typeof posterV === "string" ? posterV : null,
|
||||
overview: typeof ovV === "string" ? ovV : null,
|
||||
detail: (() => {
|
||||
const vs = versionSeasons.find((x) => x.seasonNumber === n);
|
||||
return vs ? seasonLine(vs, !anyVersion) : null;
|
||||
})(),
|
||||
summary: (
|
||||
<StatusBadge tone={total && have >= total ? "success" : "neutral"}>
|
||||
已入库 {have}/{total ?? "?"}
|
||||
@@ -781,6 +801,15 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
status: statusOf(u),
|
||||
locked: u.fieldLocks.length > 0,
|
||||
onEdit: () => setEditing(u),
|
||||
extra: (() => {
|
||||
const ev = versionsOf(u);
|
||||
return ev ? (
|
||||
<EpisodeVersionList
|
||||
versions={ev.versions}
|
||||
match={anyVersion ? undefined : ev.matches?.[0]}
|
||||
/>
|
||||
) : null;
|
||||
})(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -792,6 +821,7 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<UnplacedVersions versions={versions.data?.unplaced ?? []} />
|
||||
<UnitFieldDialog entityId={e.id} unit={editing} onOpenChange={(o) => !o && setEditing(null)} />
|
||||
</Panel>
|
||||
);
|
||||
@@ -1056,6 +1086,7 @@ export default function LibraryDetailPage() {
|
||||
</Panel>
|
||||
) : null}
|
||||
{e.kind !== "movie" ? <EpisodesPanel detail={d} units={entity.data?.contentUnits ?? []} /> : null}
|
||||
{e.kind === "movie" ? <MovieVersionsPanel entityId={e.id} /> : null}
|
||||
<SlotsPanel detail={d} units={entity.data?.contentUnits ?? []} />
|
||||
<div className="grid gap-5 xl:grid-cols-2">
|
||||
<IdentityPanel detail={d} />
|
||||
|
||||
Reference in new issue
Block a user