Files
UHD_Media_Tool/packages/contracts/src/cache.ts
T
rootandClaude Opus 5.5 7862573944 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]>
2026-09-28 22:26:29 +08:00

78 lines
2.5 KiB
TypeScript

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}`;
}