feat(cache): TMDB image proxy, posters, cache admin API and settings cards
GET /img/tmdb/{size}/{file} ported from MP ImageHelper / fetch_image
(sanitized cache path under DATA_DIR/cache/images, GLOBAL_IMAGE_CACHE
read switch, md5 ETag / 304, day-based cleanup job). Cache module adds
cache.sweep / cache.images_clean jobs and /cache/tmdb, /cache/images
list and clear endpoints. Library list/detail and the search palette
show posters with the neutral placeholder as fallback; metadata settings
get TMDB cache and image cache cards. Runbook docs/runbook/cache.md and
porting notes docs/porting/moviepilot-tmdb-cache.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
946fca5e55
commit
7862573944
16 files changed
+1028
-9
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { toast } from "sonner";
|
||||
import { useClearImageCache, useClearTmdbCache, useImageCache, useTmdbCache } from "~/api/cache.ts";
|
||||
import { useUpdateSettings } from "~/api/settings.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { str } from "./resource-section.tsx";
|
||||
import { type CardSpec, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
|
||||
// 元数据:TMDB API Key(只写,存 secret)与 Base URL
|
||||
|
||||
@@ -67,6 +68,128 @@ function TmdbKeyStatus({ saved }: { saved: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// 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.tmdbTtlHours", label: "有效期(小时)", kind: "number" },
|
||||
{ name: "cache.tmdbMaxSize", label: "条数上限", kind: "number", hint: "请求缓存和识别缓存各自计数" },
|
||||
{
|
||||
name: "cache.emptyResultTtlSec",
|
||||
label: "空结果有效期(秒)",
|
||||
kind: "number",
|
||||
hint: "搜索无结果、未识别时的短缓存",
|
||||
},
|
||||
],
|
||||
fromSettings: (s) => ({
|
||||
"cache.tmdbTtlHours": n(s.cache.tmdbTtlHours),
|
||||
"cache.tmdbMaxSize": n(s.cache.tmdbMaxSize),
|
||||
"cache.emptyResultTtlSec": n(s.cache.emptyResultTtlSec),
|
||||
}),
|
||||
toPatch: (v) => ({
|
||||
cache: {
|
||||
tmdbTtlHours: Number(v["cache.tmdbTtlHours"]),
|
||||
tmdbMaxSize: Number(v["cache.tmdbMaxSize"]),
|
||||
emptyResultTtlSec: Number(v["cache.emptyResultTtlSec"]),
|
||||
},
|
||||
}),
|
||||
extra: () => <TmdbCacheStatus />,
|
||||
};
|
||||
|
||||
function TmdbCacheStatus() {
|
||||
const q = useTmdbCache();
|
||||
const clear = useClearTmdbCache();
|
||||
const c = q.data?.counts;
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-meta text-muted-foreground">
|
||||
<span className="tabular-nums">
|
||||
{c ? `识别 ${c.recognize} 条 · 请求 ${c.request} 条 · 上限各 ${q.data?.maxSize} 条` : "读取中…"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="hover:text-destructive"
|
||||
disabled={clear.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const r = await clear.mutateAsync();
|
||||
toast.success(`已清空 ${r.deleted} 条 TMDB 缓存`);
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "清空失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 图片代理与缓存(对照 MP GLOBAL_IMAGE_CACHE / GLOBAL_IMAGE_CACHE_DAYS)
|
||||
const IMAGES_CARD: CardSpec = {
|
||||
anchor: "images",
|
||||
title: "图片代理与缓存",
|
||||
description: "海报经服务端从 TMDB 取回。开启缓存后保存到数据目录,下次直接读取。",
|
||||
fields: [
|
||||
{ name: "cache.globalImageCache", label: "缓存图片", kind: "switch" },
|
||||
{ name: "cache.globalImageCacheDays", label: "保留天数", kind: "number", hint: "超过天数的图片每天清理一次" },
|
||||
],
|
||||
fromSettings: (s) => ({
|
||||
"cache.globalImageCache": s.cache.globalImageCache,
|
||||
"cache.globalImageCacheDays": n(s.cache.globalImageCacheDays),
|
||||
}),
|
||||
toPatch: (v) => ({
|
||||
cache: {
|
||||
globalImageCache: v["cache.globalImageCache"] === true,
|
||||
globalImageCacheDays: Number(v["cache.globalImageCacheDays"]),
|
||||
},
|
||||
}),
|
||||
extra: () => <ImageCacheStatus />,
|
||||
};
|
||||
|
||||
function ImageCacheStatus() {
|
||||
const q = useImageCache();
|
||||
const clear = useClearImageCache();
|
||||
const d = q.data;
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-meta text-muted-foreground">
|
||||
<span className="tabular-nums">
|
||||
{d ? `${d.files} 张 · ${(d.usedBytes / 1024 / 1024).toFixed(1)} MiB` : "读取中…"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="hover:text-destructive"
|
||||
disabled={clear.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const r = await clear.mutateAsync();
|
||||
toast.success(`已删除 ${r.deleted} 张缓存图片`);
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "清空失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetadataSection() {
|
||||
return <SettingsGate>{(s) => <SettingsCard section="metadata" spec={TMDB_CARD} settings={s} />}</SettingsGate>;
|
||||
return (
|
||||
<SettingsGate>
|
||||
{(s) => (
|
||||
<>
|
||||
<SettingsCard section="metadata" spec={TMDB_CARD} settings={s} />
|
||||
<SettingsCard section="metadata" spec={TMDB_CACHE_CARD} settings={s} />
|
||||
<SettingsCard section="metadata" spec={IMAGES_CARD} settings={s} />
|
||||
</>
|
||||
)}
|
||||
</SettingsGate>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user