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,4 +1,4 @@
|
||||
import type { LocalMediaHit, SearchAction, TmdbCandidate } from "@umt/contracts";
|
||||
import { type LocalMediaHit, type SearchAction, type TmdbCandidate, tmdbImageUrl } from "@umt/contracts";
|
||||
import { ArrowRight, CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Plus, Search } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
@@ -33,6 +33,8 @@ type Item = {
|
||||
key: string;
|
||||
group: string;
|
||||
icon: LucideIcon;
|
||||
/** TMDB 海报路径:有则以小海报代替图标(失败回退图标) */
|
||||
posterPath?: string | null | undefined;
|
||||
title: ReactNode;
|
||||
meta?: ReactNode;
|
||||
/** 右侧动作提示 */
|
||||
@@ -216,7 +218,7 @@ function PaletteBody({ initial }: { initial: string }) {
|
||||
i === active ? "bg-foreground/[0.06]" : "",
|
||||
)}
|
||||
>
|
||||
<it.icon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<ItemIcon it={it} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm text-foreground">{it.title}</div>
|
||||
{it.meta ? <div className="truncate text-meta text-muted-foreground">{it.meta}</div> : null}
|
||||
@@ -287,12 +289,30 @@ function localItem(h: LocalMediaHit, run: () => void): Item {
|
||||
};
|
||||
}
|
||||
|
||||
function ItemIcon({ it }: { it: Item }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const src = failed ? null : tmdbImageUrl(it.posterPath, "w92");
|
||||
if (src) {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onError={() => setFailed(true)}
|
||||
className="h-9 w-6 shrink-0 rounded-tight border border-border object-cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <it.icon className="h-4 w-4 shrink-0 text-muted-foreground" />;
|
||||
}
|
||||
|
||||
function tmdbItem(c: TmdbCandidate, view: () => void, subscribe: (c: TmdbCandidate) => void): Item {
|
||||
const local = !!c.localEntityId;
|
||||
return {
|
||||
key: `tmdb-${c.entityType}-${c.externalId}`,
|
||||
group: "TMDB",
|
||||
icon: local ? Globe : Plus,
|
||||
posterPath: c.posterPath,
|
||||
title: (
|
||||
<span>
|
||||
{c.title}
|
||||
|
||||
Reference in new issue
Block a user