- G1 createEntityFromTmdbCandidate (sources-identity service): entity + accepted
manual external identity + season/episode units, idempotent per TMDB id;
POST /files/entity-from-candidate and organize items with `external`
- G2 minSizeBytes for organize (excluded below_min_size) and identify
(skipped + excludedBySize)
- G3 onExisting passed to planImport; dryRun marks each file
(same/skip/replace/conflict); replace = explicit grant
- G4 naming template|keep; planImport namingMode/namingSource, frozen
plan.naming {mode, templateSource, templateVersion}; targetSegments
- G5 identify prior-record hints, GET /files/record-sources, organize
refreshMetadata, POST /files/scrape-batch (dedupe by entity)
- G8 GET /files/tree (parent, subdirs hasChildren, video/registered counts)
- G9 POST /files/suggest-name returns 501 until the naming engine lands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
35 lines
1.5 KiB
TypeScript
35 lines
1.5 KiB
TypeScript
import type { FilesEntityFromCandidateResult, FilesExternalCandidate } from "@umt/contracts";
|
|
import type { Principal } from "../../http/auth.ts";
|
|
import { CandidateError, createEntityFromTmdbCandidate } from "../sources-identity/service/entity-from-candidate.ts";
|
|
import { actorOfP, FilesError, type FmDeps } from "./common.ts";
|
|
import { audit } from "./ledger.ts";
|
|
|
|
// G1:整理向导选中"仅 TMDB 候选"作品 → 就地建本地作品 + 外部映射(accepted/manual)+ 季集单元,返回 entityId。
|
|
// 幂等(同 TMDB id 已有映射即复用);/files/organize 的 external 项与独立端点共用。
|
|
|
|
const actorOf = (p: Principal | undefined) => ({
|
|
type: p?.kind === "api_key" ? ("api_key" as const) : ("user" as const),
|
|
id: actorOfP(p),
|
|
});
|
|
|
|
export async function entityFromCandidate(
|
|
d: FmDeps,
|
|
principal: Principal | undefined,
|
|
ref: FilesExternalCandidate,
|
|
): Promise<FilesEntityFromCandidateResult> {
|
|
try {
|
|
const r = await createEntityFromTmdbCandidate(d.ctx.db, d.tmdb, ref, actorOf(principal));
|
|
if (r.created) await audit(d, principal, "entity_from_candidate", { ...ref, mediaEntityId: r.mediaEntityId });
|
|
return r;
|
|
} catch (e) {
|
|
if (e instanceof CandidateError)
|
|
throw new FilesError(
|
|
e.reason === "not_found" ? "not_found" : e.reason === "upstream" ? "upstream" : "bad_request",
|
|
e.message,
|
|
);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export const externalKey = (x: FilesExternalCandidate) => `${x.provider}:${x.kind}:${x.externalId}`;
|