import { DEFAULT_SETTINGS } from "@umt/contracts";
import { toast } from "sonner";
import { useClearImageCache, useClearTmdbCache, useImageCache, useTmdbCache } from "~/api/cache.ts";
import { useEnrichStats } from "~/api/identity.ts";
import { DataTable } from "~/components/console/data-table.tsx";
import { ENRICH_SOURCE_KIND } from "~/components/library/labels.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { secretPatch } from "~/lib/secret.ts";
import { str } from "./resource-section.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
// 元数据:TMDB API Key(只写,存 secret)、Base URL、语言回退链
const DEFAULT_LANGUAGES = DEFAULT_SETTINGS.metadata.languages;
/** "zh-CN, zh_sg en" → ["zh-CN","zh-SG","en"];不合法的项丢弃,全空回缺省 */
function parseLanguages(v: string): string[] {
const out = v
.split(/[,,、\s]+/)
.map((x) => x.trim().replace("_", "-"))
.filter(Boolean)
.map((x) => {
const [l, r] = x.split("-");
return r ? `${l!.toLowerCase()}-${r.toUpperCase()}` : l!.toLowerCase();
})
.filter((x) => /^[a-z]{2,3}(-[A-Z]{2})?$/.test(x));
return out.length ? [...new Set(out)].slice(0, 8) : [...DEFAULT_LANGUAGES];
}
const TMDB_CARD: CardSpec = {
anchor: "tmdb",
title: "TMDB",
description: "留空时用环境变量 TMDB_API_KEY / TMDB_ACCESS_TOKEN / TMDB_BASE_URL。",
fields: [
{
name: "metadata.tmdbApiKey",
label: "API Key",
kind: "secret",
secret: { set: (v) => v["metadata.tmdbApiKey:set"] === true, clearable: true },
hint: "v3 API Key 或 v4 Read Access Token",
wide: true,
},
{
name: "metadata.tmdbBaseUrl",
label: "Base URL",
kind: "url",
placeholder: "https://api.themoviedb.org",
hint: "自建反代时填写;留空用官方地址",
wide: true,
},
{
name: "metadata.languages",
label: "语言回退链",
kind: "text",
placeholder: DEFAULT_LANGUAGES.join(", "),
hint: "标题和每集信息按顺序取第一个有值的语言,简介默认只取链里的中文;中文名取链中第一个中文值。逗号分隔,如 zh-CN, zh-SG",
wide: true,
},
{
name: "metadata.overviewNonChineseFallback",
label: "简介回退到非中文",
kind: "switch",
hint: "没有中文简介时,按回退链取英文等语言。关闭时留空,标题不受影响",
wide: true,
},
{
name: "metadata.includeSpecials",
label: "包含特别篇(季 0)",
kind: "switch",
hint: "全量刷新和元数据回刷时,TMDB 有季 0 就一并拉取每集信息。特别篇不计入正片季的总集数和订阅完成判定",
wide: true,
},
],
fromSettings: (s) => ({
"metadata.tmdbApiKey": "",
"metadata.tmdbApiKey:set": !!s.metadata.tmdbApiKeySecretRef,
"metadata.tmdbBaseUrl": s.metadata.tmdbBaseUrl ?? "",
"metadata.languages": (s.metadata.languages ?? DEFAULT_LANGUAGES).join(", "),
"metadata.includeSpecials": s.metadata.includeSpecials !== false,
"metadata.overviewNonChineseFallback": s.metadata.overviewNonChineseFallback === true,
}),
toPatch: (v) => {
const key = secretPatch(v["metadata.tmdbApiKey"]);
return {
metadata: {
tmdbBaseUrl: str(v["metadata.tmdbBaseUrl"]) || null,
languages: parseLanguages(str(v["metadata.languages"])),
includeSpecials: v["metadata.includeSpecials"] === true,
overviewNonChineseFallback: v["metadata.overviewNonChineseFallback"] === true,
...(key !== undefined ? { tmdbApiKey: key } : {}),
},
};
},
};
// TMDB 缓存(对照 MP:META_CACHE_EXPIRE / CONF.tmdb / EMPTY_RESULT_CACHE_TTL)
const TMDB_CACHE_CARD: CardSpec = {
anchor: "tmdb-cache",
title: "TMDB 缓存",
description:
"请求响应与识别结果存在数据库里,三个进程共用。超过条数上限时删最久未用的条目;有效期在「自动化 → 存储与保留」里设。",
fields: [{ name: "cache.tmdbMaxSize", label: "条数上限", kind: "number", hint: "请求缓存和识别缓存各自计数" }],
fromSettings: (s) => ({
"cache.tmdbMaxSize": n(s.cache.tmdbMaxSize),
}),
toPatch: (v) => ({
cache: {
tmdbMaxSize: Number(v["cache.tmdbMaxSize"]),
},
}),
extra: () =>
{d ? `最近 ${d.days} 天元数据补全 ${d.total} 次,出了建议 ${d.suggested} 次${d.tracked < d.total ? `;有来源明细的 ${d.tracked} 次` : ""}` : "读取补全命中率…"}
{d && d.tracked > 0 ? (