feat(organize): 整理、季包选文件、刮削、文件管理识别统一套用集数映射;单集沿用资源判定,待确认文件不按原号入库
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
691671a1d2
commit
fe597187dd
12 files changed
+848
-21
No files matched your search
@@ -23,6 +23,7 @@ import { localIdentityCandidate } from "@umt/mapping";
|
||||
import type { Principal } from "../../http/auth.ts";
|
||||
import { successfulOrganizeCounts } from "../acquisition-organize/services/organize-history.ts";
|
||||
import { EXTERNAL_INVOKE_TIMEOUT_MS, sharedInvokeLimiter } from "../agent/invoke-limits.ts";
|
||||
import { createEntityEpisodeMapper, type EntityEpisodeMapper } from "../sources-identity/service/episode-resolution.ts";
|
||||
import { actorOfP, errMessage, FilesError, type FmDeps, resolveIn } from "./common.ts";
|
||||
import { kindOfName } from "./list.ts";
|
||||
|
||||
@@ -226,6 +227,7 @@ export async function identifyCore(
|
||||
const localCache = new Map<string, IdentityCandidate[]>();
|
||||
const remoteCache = new Map<string, { list: IdentityCandidate[]; warning: string | null; pending?: boolean }>();
|
||||
const entityKinds = new Map<string, string>();
|
||||
const mappers = new Map<string, EntityEpisodeMapper>();
|
||||
// G8:候选海报(TMDB poster_path;本地作品取投影后的 metadata.posterPath)
|
||||
const posters = new Map<string, string>();
|
||||
|
||||
@@ -446,10 +448,28 @@ export async function identifyCore(
|
||||
const best = candidates[0];
|
||||
let contentUnitId: string | null = null;
|
||||
let contentUnit: string | null = null;
|
||||
const season = parsed.season ?? (parsed.episodes ? 1 : undefined);
|
||||
// 集数映射(批次 3,只读):识别到本地剧集作品后,展示与建议单元用该作品映射后的季集;待确认的不给单元
|
||||
let shown = parsed;
|
||||
let unresolved = false;
|
||||
if (best?.mediaEntityId && parsed.episodes) {
|
||||
const kind = entityKinds.get(best.mediaEntityId);
|
||||
if (kind === "series" || kind === "variety") {
|
||||
let map = mappers.get(best.mediaEntityId);
|
||||
if (!map) {
|
||||
map = await createEntityEpisodeMapper(db, best.mediaEntityId, kind);
|
||||
mappers.set(best.mediaEntityId, map);
|
||||
}
|
||||
const m = map(parsed);
|
||||
if (m.resolution.method === "unresolved") {
|
||||
unresolved = true;
|
||||
warnings.push("集数对不上作品的季集,需要人工指定");
|
||||
} else shown = m.parsed;
|
||||
}
|
||||
}
|
||||
const season = unresolved ? undefined : (shown.season ?? (shown.episodes ? 1 : undefined));
|
||||
if (season !== undefined) {
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
contentUnit = parsed.episodes ? `S${pad(season)}E${pad(parsed.episodes.start)}` : `S${pad(season)}`;
|
||||
contentUnit = shown.episodes ? `S${pad(season)}E${pad(shown.episodes.start)}` : `S${pad(season)}`;
|
||||
if (best?.mediaEntityId) {
|
||||
let q = db
|
||||
.selectFrom("content_unit")
|
||||
@@ -457,7 +477,7 @@ export async function identifyCore(
|
||||
.where("media_entity_id", "=", best.mediaEntityId)
|
||||
.where("season_number", "=", season)
|
||||
.where("status", "<>", "retired");
|
||||
q = parsed.episodes ? q.where("episode_number", "=", parsed.episodes.start) : q.where("kind", "=", "season");
|
||||
q = shown.episodes ? q.where("episode_number", "=", shown.episodes.start) : q.where("kind", "=", "season");
|
||||
contentUnitId = (await q.executeTakeFirst())?.id ?? null;
|
||||
}
|
||||
}
|
||||
@@ -474,7 +494,7 @@ export async function identifyCore(
|
||||
items.push({
|
||||
root: v.root as FileIdentifyItem["root"],
|
||||
path: v.rel,
|
||||
parsed: parsedOut(parsed),
|
||||
parsed: parsedOut(shown),
|
||||
candidates,
|
||||
suggestion: { mediaEntityId: best?.mediaEntityId ?? null, contentUnitId, contentUnit },
|
||||
existing: { assetId: existing?.media_asset_id ?? null },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { extname } from "node:path";
|
||||
import type { FilesSuggestNameRequest, FilesSuggestNameResult } from "@umt/contracts";
|
||||
import { buildNamingVars, normalizeTitle, parseReleasePath, renderNamingPath } from "@umt/domain";
|
||||
import { effectiveNaming, loadNamingEntity } from "../acquisition-organize/services/naming.ts";
|
||||
import { createEntityEpisodeMapper } from "../sources-identity/service/episode-resolution.ts";
|
||||
import { FilesError, type FmDeps, resolveIn } from "./common.ts";
|
||||
|
||||
// G9 推荐名:按命名模板(作品 > 库根 > 全局)对选中文件/目录渲染名字(与整理用同一引擎与变量来源)。
|
||||
@@ -76,7 +77,14 @@ export async function suggestName(d: FmDeps, body: FilesSuggestNameRequest): Pro
|
||||
const namingEntity = await loadNamingEntity(db, entity, { libraryRoot: root?.name });
|
||||
const kind = namingEntity.kind ?? "movie";
|
||||
const ext = isDir ? "" : extname(r.rel);
|
||||
const vars = buildNamingVars(namingEntity, parsed, isDir ? {} : { fileExt: ext });
|
||||
// 集数映射(批次 3,只读):文件名的季集按作品季表 + 规则换成规范季集再命名;待确认的不给推荐名
|
||||
let named = parsed;
|
||||
if (!isDir && parsed.episodes && (entity.kind === "series" || entity.kind === "variety")) {
|
||||
const m = (await createEntityEpisodeMapper(db, entity.id, entity.kind))(parsed);
|
||||
if (m.resolution.method === "unresolved") return empty("episode_unresolved", entityId);
|
||||
named = m.parsed;
|
||||
}
|
||||
const vars = buildNamingVars(namingEntity, named, isDir ? {} : { fileExt: ext });
|
||||
const out = renderNamingPath(eff.templates[kind], vars, isDir ? { mode: "dirs" } : {});
|
||||
const segments = out.segments.map((s) => ({ text: s.text, role: s.role }));
|
||||
if (!out.ok)
|
||||
|
||||
Reference in new issue
Block a user