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
@@ -3,6 +3,7 @@ import * as agent from "./agent.ts";
|
||||
import * as assets from "./assets.ts";
|
||||
import * as auth from "./auth.ts";
|
||||
import * as browser from "./browser.ts";
|
||||
import * as cache from "./cache.ts";
|
||||
import { CapabilityDescriptor, KnownCapabilityId } from "./capability.ts";
|
||||
import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } from "./common.ts";
|
||||
import * as content from "./content.ts";
|
||||
@@ -1611,6 +1612,39 @@ export const Api = {
|
||||
}),
|
||||
},
|
||||
|
||||
// ── 缓存(C2-A,docs/runbook/cache.md) ──
|
||||
cache: {
|
||||
tmdb: ep({
|
||||
method: "get",
|
||||
path: "/cache/tmdb",
|
||||
scope: "admin",
|
||||
summary: "TMDB 缓存条目(请求响应与识别结果:键、过期时间、大小)与上限 / TTL",
|
||||
query: cache.TmdbCacheQuery,
|
||||
response: cache.TmdbCacheList,
|
||||
}),
|
||||
tmdbClear: ep({
|
||||
method: "post",
|
||||
path: "/cache/tmdb/clear",
|
||||
scope: "admin",
|
||||
summary: "清空 TMDB 缓存(请求响应与识别结果)",
|
||||
response: cache.CacheClearResult,
|
||||
}),
|
||||
images: ep({
|
||||
method: "get",
|
||||
path: "/cache/images",
|
||||
scope: "admin",
|
||||
summary: "图片代理缓存用量",
|
||||
response: cache.ImageCacheStatus,
|
||||
}),
|
||||
imagesClear: ep({
|
||||
method: "post",
|
||||
path: "/cache/images/clear",
|
||||
scope: "admin",
|
||||
summary: "清空图片代理缓存",
|
||||
response: cache.CacheClearResult,
|
||||
}),
|
||||
},
|
||||
|
||||
// ── 设置 ──
|
||||
settings: {
|
||||
get: ep({ method: "get", path: "/settings", scope: "read", summary: "全局设置", response: GlobalSettings }),
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { z } from "zod";
|
||||
import { NonNegInt, Timestamp } from "./common.ts";
|
||||
|
||||
// C2-A 缓存管理(docs/runbook/cache.md):TMDB 缓存(请求响应 + 识别结果,对照 MP TmdbCache)、图片代理缓存。
|
||||
|
||||
export const TmdbCacheKind = z.enum(["request", "recognize"]);
|
||||
export type TmdbCacheKind = z.infer<typeof TmdbCacheKind>;
|
||||
|
||||
/** 一条 TMDB 缓存(对照 MP list_items:识别条目带 tmdbId / 标题 / 年份 / 类型 / 海报) */
|
||||
export const TmdbCacheItem = z.object({
|
||||
kind: TmdbCacheKind,
|
||||
key: z.string(),
|
||||
expiresAt: Timestamp,
|
||||
updatedAt: Timestamp,
|
||||
/** jsonb 存储字节数 */
|
||||
bytes: NonNegInt,
|
||||
/** 识别条目:TMDB id,0 = 负缓存(未识别) */
|
||||
tmdbId: z.number().int().nullable(),
|
||||
title: z.string().nullable(),
|
||||
year: z.string().nullable(),
|
||||
mediaType: z.enum(["movie", "tv", "unknown"]).nullable(),
|
||||
posterPath: z.string().nullable(),
|
||||
});
|
||||
export type TmdbCacheItem = z.infer<typeof TmdbCacheItem>;
|
||||
|
||||
export const TmdbCacheQuery = z.object({
|
||||
kind: TmdbCacheKind.optional(),
|
||||
limit: z.coerce.number().int().min(1).max(5000).default(500),
|
||||
});
|
||||
|
||||
export const TmdbCacheList = z.object({
|
||||
items: z.array(TmdbCacheItem),
|
||||
counts: z.object({ request: NonNegInt, recognize: NonNegInt }),
|
||||
/** 每类条数上限(cache.tmdbMaxSize) */
|
||||
maxSize: NonNegInt,
|
||||
ttlHours: NonNegInt,
|
||||
emptyResultTtlSec: NonNegInt,
|
||||
});
|
||||
export type TmdbCacheList = z.infer<typeof TmdbCacheList>;
|
||||
|
||||
export const CacheClearResult = z.object({ deleted: NonNegInt });
|
||||
export type CacheClearResult = z.infer<typeof CacheClearResult>;
|
||||
|
||||
/** 图片代理缓存用量 */
|
||||
export const ImageCacheStatus = z.object({
|
||||
/** cache.globalImageCache */
|
||||
enabled: z.boolean(),
|
||||
/** cache.globalImageCacheDays */
|
||||
days: NonNegInt,
|
||||
usedBytes: NonNegInt,
|
||||
files: NonNegInt,
|
||||
dir: z.string(),
|
||||
});
|
||||
export type ImageCacheStatus = z.infer<typeof ImageCacheStatus>;
|
||||
|
||||
/** TMDB 图片尺寸白名单(与 TMDB configuration.images 一致) */
|
||||
export const TMDB_IMAGE_SIZES = [
|
||||
"w92",
|
||||
"w154",
|
||||
"w185",
|
||||
"w300",
|
||||
"w342",
|
||||
"w500",
|
||||
"w780",
|
||||
"w1280",
|
||||
"h632",
|
||||
"original",
|
||||
] as const;
|
||||
export type TmdbImageSize = (typeof TMDB_IMAGE_SIZES)[number];
|
||||
|
||||
/** 海报 / 背景图路径 → 图片代理 URL(相对 API_PREFIX 之外的完整路径,前端直接用作 img src) */
|
||||
export function tmdbImageUrl(path: string | null | undefined, size: TmdbImageSize = "w342"): string | null {
|
||||
if (!path) return null;
|
||||
const p = path.startsWith("/") ? path.slice(1) : path;
|
||||
if (!/^[A-Za-z0-9_-]+\.(jpg|jpeg|png|webp)$/.test(p)) return null;
|
||||
return `/api/v1/img/tmdb/${size}/${p}`;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export * from "./api.ts";
|
||||
export * from "./assets.ts";
|
||||
export * from "./auth.ts";
|
||||
export * from "./browser.ts";
|
||||
export * from "./cache.ts";
|
||||
export * from "./capabilities-io.ts";
|
||||
export * from "./capability.ts";
|
||||
export * from "./common.ts";
|
||||
|
||||
@@ -340,6 +340,8 @@ export const LibraryItem = z.object({
|
||||
slotCounts: SlotStatusCounts,
|
||||
activeDownloads: NonNegInt,
|
||||
lastOrganizedAt: Timestamp.nullable(),
|
||||
/** TMDB 海报路径(metadata.posterPath);前端经图片代理显示,没有时显示无彩占位 */
|
||||
posterPath: z.string().nullable().optional(),
|
||||
});
|
||||
export type LibraryItem = z.infer<typeof LibraryItem>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user